From opensaas-migration
Migrate context.graphql.run/raw and context.query.* calls to context.db.* across a project. Invoke as a forked subagent when these patterns are detected, passing the project root path as arguments.
How this skill is triggered — by the user, by Claude, or both
Slash command
/opensaas-migration:migrate-context-callsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Search for and migrate all `context.graphql` and `context.query` calls in the project described below. OpenSaaS Stack has no GraphQL — use `context.db.{listName}.{method}()` directly.
Search for and migrate all context.graphql and context.query calls in the project described below. OpenSaaS Stack has no GraphQL — use context.db.{listName}.{method}() directly.
$ARGUMENTS
| Keystone | OpenSaaS Stack |
|---|---|
context.graphql.run({ query, variables }) | context.db.{list}.{method}(args) |
context.graphql.raw({ query, variables }) | context.db.{list}.{method}(args) |
context.query.PostList.findMany(...) | context.db.post.findMany(...) |
context.query.PostList.count(...) | context.db.post.count(...) |
context.sudo().graphql.run(...) | context.sudo().db.post.findMany() |
List names are camelCase: Post → context.db.post, BlogPost → context.db.blogPost, AuthUser → context.db.authUser.
Access control is automatically enforced by context.db. Use context.sudo().db.* to bypass it (equivalent to Keystone's context.sudo()).
Null handling: context.db returns null (single item) or [] (list) on access denial — never throws. Check for null after writes.
// Before
const { posts } = await context.graphql.run({
query: `query GetPosts($authorId: ID!) {
posts(where: { author: { id: { equals: $authorId } } }, orderBy: [{ createdAt: desc }], take: 10) {
id title createdAt
}
}`,
variables: { authorId },
})
// After
const posts = await context.db.post.findMany({
where: { authorId: { equals: authorId } },
orderBy: { createdAt: 'desc' },
take: 10,
})
// Before
const { post } = await context.graphql.run({
query: `query { post(where: { id: $id }) { id title content } }`,
variables: { id: postId },
})
// After
const post = await context.db.post.findUnique({ where: { id: postId } })
// Before
const { createPost } = await context.graphql.run({
query: `mutation { createPost(data: $data) { id title } }`,
variables: { data: { title: 'Hello', content: '...' } },
})
// After
const post = await context.db.post.create({ data: { title: 'Hello', content: '...' } })
// Before
await context.graphql.run({
query: `mutation { updatePost(where: { id: $id }, data: $data) { id } }`,
variables: { id: postId, data: { title: 'Updated' } },
})
// After
const updated = await context.db.post.update({ where: { id: postId }, data: { title: 'Updated' } })
if (!updated) {
/* access denied or not found */
}
// Before
await context.graphql.run({
query: `mutation { deletePost(where: { id: $id }) { id } }`,
variables: { id: postId },
})
// After
await context.db.post.delete({ where: { id: postId } })
// Before
const { postsCount } = await context.graphql.run({
query: `query { postsCount(where: { status: { equals: published } }) }`,
})
// After
const count = await context.db.post.count({ where: { status: { equals: 'published' } } })
OpenSaaS Stack provides defineFragment for composable, type-safe queries that include related data in a single call — the closest equivalent to Keystone's GraphQL fragments. Pass the fragment directly to context.db operations using the query parameter.
// Before — one GraphQL query with nested author and tags
const { posts } = await context.graphql.run({
query: `
fragment AuthorFields on User { id name }
query GetPosts {
posts(where: { published: true }) {
id title author { ...AuthorFields } tags { id name }
}
}
`,
})
// After — define fragments once, compose and reuse them
import type { User, Post, Tag } from '.prisma/client'
import { defineFragment, type ResultOf } from '@opensaas/stack-core'
const authorFragment = defineFragment<User>()({ id: true, name: true } as const)
const tagFragment = defineFragment<Tag>()({ id: true, name: true } as const)
const postFragment = defineFragment<Post>()({
id: true,
title: true,
author: authorFragment, // nested fragment → loaded via Prisma include
tags: tagFragment, // many relationship
} as const)
// Type-inferred — no codegen needed
type PostData = ResultOf<typeof postFragment>
// → { id: string; title: string; author: { id: string; name: string } | null; tags: { id: string; name: string }[] }
// Primary API: pass query fragment to context.db.findMany
const posts = await context.db.post.findMany({
query: postFragment,
where: { published: true },
orderBy: { publishedAt: 'desc' },
})
// posts: PostData[]
For single-record queries:
const post = await context.db.post.findUnique({
where: { id: postId },
query: postFragment,
})
if (!post) return notFound()
// post: PostData
For nested relationship filtering (e.g., only load approved comments):
const commentFragment = defineFragment<Comment>()({ id: true, body: true } as const)
const postWithComments = defineFragment<Post>()({
id: true,
title: true,
comments: {
query: commentFragment,
where: { approved: true }, // filter nested relationship
orderBy: { createdAt: 'desc' },
take: 5,
},
} as const)
const posts = await context.db.post.findMany({ query: postWithComments })
Standalone runQuery / runQueryOne helpers are also available for use in hooks or utilities where context.db is available but direct method call is inconvenient:
import { runQuery, runQueryOne } from '@opensaas/stack-core'
const posts = await runQuery(context, 'Post', postFragment, { where: { published: true } })
const post = await runQueryOne(context, 'Post', postFragment, { id: postId })
If you only need one level of nesting without fragment reuse, separate calls are fine:
// Before — one query with nested author
const { post } = await context.graphql.run({
query: `query { post(where: { id: $id }) { id title author { id name } } }`,
variables: { id: postId },
})
// After — separate calls
const post = await context.db.post.findUnique({ where: { id: postId } })
const author = post?.authorId
? await context.db.user.findUnique({ where: { id: post.authorId } })
: null
const authorName = author?.name
// Before
const allPosts = await context.sudo().graphql.run({ query: '...' })
// After
const allPosts = await context.sudo().db.post.findMany()
context.graphql, context.query, and context.sudo().graphql in the project (search .ts, .tsx files, exclude node_modules)context.db.{list}.findMany/findUnique({ query: fragment }) with defineFragment (see pattern above)context.db.{list}.findMany() / findUnique()context.db.{list}.create() / update() / delete()context.db.{list}.count()
c. Identify the list name (convert to camelCase for context.db)
d. Rewrite using the appropriate pattern above
e. For fragment-based rewrites: create a shared fragments.ts file and import from itimport ... from '@keystone-6/core' imports used only for graphql types are removed or reduced; also remove any GraphQL codegen type imports (replace with ResultOf<typeof fragment>)npx claudepluginhub opensaasau/stack --plugin opensaas-migrationGuides safe, reversible database schema changes and data migrations with blast radius analysis, migration patterns, and safety checks for PostgreSQL, MySQL, and ORMs.
Implements safe Convex schema migrations: add optional fields, backfill data with batch mutations, remove deprecated fields, zero-downtime deploys.
Generates database migrations with schema changes and rollback, detecting framework (Knex, Prisma, TypeORM, raw SQL) and validating safety.