mit einem Klick
api-add-gql-mutation
// Add a new GraphQL mutation to an existing schema in daily-api with validation, resolvers, and tests
// Add a new GraphQL mutation to an existing schema in daily-api with validation, resolvers, and tests
Add a new GraphQL query endpoint with type safety, GraphORM integration, and tests
Create a new background worker in daily-api with full type safety, infrastructure config, and tests
Wraps any prompt with a reminder to respect CLAUDE.md files
Format TypeORM migrations with beautifully formatted SQL code. Use this skill after generating migrations to ensure consistent SQL formatting.
Local environment management - run SQL queries, set up fake payments, reset test data. Use when the user needs help with local database operations or test data setup.
| name | api-add-gql-mutation |
| description | Add a new GraphQL mutation to an existing schema in daily-api with validation, resolvers, and tests |
| argument-hint | [schema name and mutation purpose] |
You are adding a new mutation to an existing GraphQL schema file in daily-api. Follow this skill step by step.
Before writing any code, read these files for code style rules and conventions:
AGENTS.md (project root) — code style, architecture, best practicessrc/graphorm/AGENTS.md — GraphORM is read-only; mutations use TypeORM repositoriesThese are the source of truth for all code style decisions. Do not deviate from them.
Before writing any code, ask the user the following questions in this order:
Which existing
src/schema/<domain>.tsfile should receive this mutation?
Existing schema files for reference:
achievements, actions, alerts, autocompletes, bookmarks, campaigns, comments, common, compatibility, contentPreference, devcards, feedback, feeds, gear, integrations, keywords, leaderboard, njord, notifications, opportunity, organizations, paddle, personalAccessTokens, posts, profile, prompts, search, settings, sourceRequests, sourceStack, sources, submissions, tags, trace, urlShortener, userHotTake, userStack, userWorkspacePhoto, users
What is the mutation name and what does it do?
What arguments/input type does it accept? Describe the fields, their types, and which are required vs optional.
What does it return? Options:
EmptyResponse(fromsrc/schema/common.ts— return{ _: true })- An existing type already defined in the target schema
- A new custom type (describe it)
What auth level is needed? Options:
@auth— standard authenticated user@auth(requires: [MODERATOR])— system moderator only@rateLimit(limit: N, duration: N)— rate-limited (combine with@auth)
What kind of writes? Options:
- Single write (simple
repo.update()/repo.save())- Multiple writes (needs
con.transaction())- JSONB flag update (use
updateFlagsStatement)
Before writing any code, check for types and utilities that can be reused:
typeDefs — look for existing input/output typessrc/schema/common.ts for shared types:
EmptyResponse — for mutations with no meaningful returnGQLDataInput<T>, GQLIdInput, GQLDataIdInput<T> — standard input wrappersDateTime, JSONObject)src/common/ for shared utilities:
toGQLEnum() from src/common/utils.ts — expose TypeScript enums as GraphQL enumsupdateFlagsStatement from src/common/utils.ts — atomic JSONB flag updatessrc/entity/ for the relevant domain — understand database column types and relationssrc/common/schema/ for existing Zod schemas that validate similar dataReport findings to the user before proceeding.
In the target schema file (src/schema/<domain>.ts), add to the typeDefs template literal:
input MyMutationInput {
field1: String!
field2: Int
}
type MyMutationResult {
id: ID!
status: String!
}
Add inside extend type Mutation { }:
extend type Mutation {
"""
Brief description of what this mutation does
"""
myMutation(data: MyMutationInput!): MyMutationResult! @auth
}
Directive examples:
@auth — standard auth@auth(requires: [MODERATOR]) — moderator only@rateLimit(limit: 5, duration: 60) @auth — rate-limitedAdd type declarations (prefixed with GQL) for any new input/output types. Use type over interface per code style rules:
type GQLMyMutationInput = {
field1: string;
field2?: number;
};
Add the resolver in the resolvers export object under the Mutation key. The file should already have traceResolvers() wrapping all resolvers.
Mutation: {
myMutation: async (
_,
{ data }: { data: GQLMyMutationInput },
{ con, userId }: AuthContext,
): Promise<GQLMyMutationResult> => {
// implementation
},
},
Always prefer Zod for input validation. Place Zod schemas in src/common/schema/<domain>.ts (create the file if it doesn't exist for this domain).
This project uses Zod 4.x (currently 4.3.5) — use the v4 API:
z.email() not z.string().email(), z.uuid() not z.string().uuid(), z.url() not z.string().url()z.literal([...]) supports arrays for enum-like validation; z.enum([...]) also works.nullish() instead of .nullable().optional()Schema suffix (e.g., myMutationInputSchema)z.infer<typeof schema> at point of use// src/common/schema/<domain>.ts
import z from 'zod';
export const myMutationInputSchema = z.object({
field1: z.string().min(1),
field2: z.number().int().positive().nullish(),
email: z.email(),
});
// In the resolver
import { myMutationInputSchema } from '../common/schema/<domain>';
const result = myMutationInputSchema.safeParse(data);
if (!result.success) {
throw new ValidationError(result.error.issues.map((e) => e.message).join(', '));
}
Only fall back to a simple manual check for trivially obvious single-field cases.
If checks are needed beyond the @auth directive (e.g., ownership, role-based):
const entity = await con.getRepository(Entity).findOneBy({ id });
if (!entity) {
throw new NotFoundError('Entity not found');
}
if (entity.userId !== userId) {
throw new ForbiddenError('Access denied');
}
Single write:
await con.getRepository(Entity).update({ id }, { field: value });
Multiple writes (transaction):
return con.transaction(async (manager) => {
await manager.getRepository(EntityA).update({ id }, { field: value });
await manager.getRepository(EntityB).save({ ... });
});
JSONB flag update:
import { updateFlagsStatement } from '../common';
await con.getRepository(Entity).update({ id }, {
flags: updateFlagsStatement<Entity>({ newField: value }),
});
EmptyResponse: return { _: true }! non-null assertions — use explicit checks and throw errorslogger.info for success paths — errors propagate naturallyindex.ts re-exportsconst arrow functions; prefer single props-style argument ({ a, b }) over positional argsConfirm the target schema is already imported and registered in src/graphql.ts. Since you are adding to an existing schema file, it should already be there. Verify by checking that:
import * as <domain> from './schema/<domain>'typeDefs are included in the typeDefs arrayresolvers are merged via merge() in the resolvers objectIf somehow missing (unlikely for existing schemas), add the import and register both typeDefs and resolvers.
Add tests in the existing __tests__/<domain>.ts test file. If it doesn't exist, create it.
import { DataSource } from 'typeorm';
import createOrGetConnection from '../src/db';
import {
GraphQLTestClient,
GraphQLTestingState,
MockContext,
disposeGraphQLTesting,
initializeGraphQLTesting,
saveFixtures,
testMutationErrorCode,
} from './helpers';
let con: DataSource;
let state: GraphQLTestingState;
let client: GraphQLTestClient;
let loggedUser: string | null = null;
beforeAll(async () => {
con = await createOrGetConnection();
state = await initializeGraphQLTesting(() => new MockContext(con, loggedUser));
client = state.client;
});
beforeEach(async () => {
loggedUser = null;
// setup fixtures as needed
});
afterAll(() => disposeGraphQLTesting(state));
const MUTATION = `
mutation MyMutation($data: MyMutationInput!) {
myMutation(data: $data) {
id
status
}
}
`;
1. Auth check — unauthenticated request:
it('should not allow unauthenticated user', () =>
testMutationErrorCode(
client,
{ mutation: MUTATION, variables: { data: validInput } },
'UNAUTHENTICATED',
));
2. Validation — invalid input:
it('should throw validation error for invalid input', async () => {
loggedUser = '1';
return testMutationErrorCode(
client,
{ mutation: MUTATION, variables: { data: invalidInput } },
'GRAPHQL_VALIDATION_FAILED',
);
});
3. Success path — mutation succeeds and DB state is correct:
it('should successfully perform the mutation', async () => {
loggedUser = '1';
const res = await client.mutate(MUTATION, {
variables: { data: validInput },
});
expect(res.errors).toBeFalsy();
expect(res.data.myMutation).toMatchObject({ status: 'expected' });
// Verify database state
const entity = await con.getRepository(Entity).findOneBy({ id });
expect(entity.field).toEqual('expected');
});
4. Edge cases (domain-specific):
NODE_ENV=test npx jest __tests__/<domain>.ts --testEnvironment=node --runInBand
| Purpose | Path |
|---|---|
| Code style & architecture | AGENTS.md (root) |
| GraphORM read-only constraint | src/graphorm/AGENTS.md |
| Common GQL types | src/schema/common.ts |
| Schema registration | src/graphql.ts |
| Auth context types | src/Context.ts |
| Entity definitions | src/entity/<domain>.ts |
| Zod schemas | src/common/schema/<domain>.ts |
| Test helpers | __tests__/helpers.ts |
| Example: simple mutation | src/schema/settings.ts — updateUserSettings |
| Example: transactional mutation | src/schema/campaigns.ts — startCampaign |
| Example: mutation tests | __tests__/settings.ts |
/format-migration skill/api-create-worker skillWhen the user invokes this skill:
AGENTS.md and src/graphorm/AGENTS.md for context