Operation response factories#205
Conversation
When `documents` is set in codegen config, emit one factory per named
operation, shaped to that operation's selection-set type. Useful with
msw and other tooling that needs operation-shaped data.
```ts
import { aGetUserQueryResponse } from './mocks.generated';
const data = aGetUserQueryResponse({
user: { name: 'Alice' },
});
```
Leaf values flow through the existing mockValueGenerator, so scalar
config, locale, and seed match the schema-typed factories. Aliases,
__typename, nested objects, lists, and named fragment spreads are walked
from the operation document.
Union and interface fields with __typename selected and two or more
inline-fragment branches generate per-branch closures plus a dispatcher.
The override slot accepts an object override (applied to the
alphabetically-first branch) or a branch-keyed callback:
```ts
aGetNodeQueryResponse({
node: ({ User }) => User({ name: 'Alice' }),
});
```
Lists of a union/interface follow the same shape:
```ts
aSearchQueryResponse({
search: ({ User, Document }) => [
User({ name: 'Alice' }),
Document({ title: 'Q3 Report' }),
],
});
```
Activated by `generateOperationFactories: true` (default) when documents
are present and `typesFile` is set. Set the option to false to disable.
|
Hi @stevecrozz, thanks for putting this together! I'm testing this out in in the codebases that we maintain, so far I haven't noticed any major limitations or issues. @ardeois just letting you'd know that we would also get good value out of this enhancement |
| : [T] extends [Array<infer U>] | ||
| ? _HasMultipleBranches<NonNullable<U>> extends true | ||
| ? T | ((b: Branches<NonNullable<U>>) => U[]) | ||
| : T | ((make: (o?: DeepPartial<U>) => U) => U[]) |
There was a problem hiding this comment.
Wondering if this should be:
| : T | ((make: (o?: DeepPartial<U>) => U) => U[]) | |
| : Array<DeepPartial<U>> | ((make: (o?: DeepPartial<U>) => U) => U[]) |
For example, with a schema like this
type Query {
users: UserConnection!
}
type UserConnection {
nodes: [User!]!
totalCount: Int!
}
type User {
id: ID!
name: String!
email: String!
}
and a query
query ListUsers {
users {
nodes { id name email }
totalCount
}
}
This works:
aListUsersQueryResponse({
users: {
nodes: (make) => [make({ id: "u1" })],
},
});
but this gives me a TS error
aListUsersQueryResponse({
users: {
nodes: [{ id: "u1" }], // missing `name`, `email` → TS error
},
});
There was a problem hiding this comment.
@jkasala-jobber thank you. I believe you are right. I added this change and a regression test that I think represents your scenario.
Array overrides flow through applyArrayOverride, which calls makeDefault(el) on each element, so partial elements work at runtime. The DeepPartial type was stricter than the runtime, requiring complete U for every element. Switch to Array<DeepPartial<U>> so partial element overrides type-check.
Adds a typeCheck spec that runs the TypeScript compiler over the generated factory plus a hand-written types file, asserting that partial element overrides for object lists compile cleanly and that unknown element fields are still rejected. While wiring up the test, applyArrayOverride's `override` parameter needed the same Array<DeepPartial<T>> loosening as the user-facing DeepPartial; otherwise the generated factory body fails to type-check when forwarding the now-permissive override into the helper.
| const entries: string[] = []; | ||
| const seenFragments = new Set<string>(); | ||
| const seenFields = new Set<string>(); | ||
| collectFieldSelections(selectionSet, concrete, ctx, overrideAccess, entries, seenFragments, seenFields); |
There was a problem hiding this comment.
I don't think the factories are adding __typename when addTypename: true has been configured. I had some tests failing because of cache resolution issues unless I explicitly added __typename to the mock response data. This fixed it for me:
| collectFieldSelections(selectionSet, concrete, ctx, overrideAccess, entries, seenFragments, seenFields); | |
| // after adding `addTypename` to WalkContext | |
| if (ctx.addTypename) { | |
| entries.push(`__typename: '${concrete.name}' as const`); | |
| seenFields.add('__typename'); | |
| } | |
| collectFieldSelections(selectionSet, concrete, ctx, overrideAccess, entries, seenFragments, seenFields); |
let me know if I can provide more information
There was a problem hiding this comment.
The idea I have here is that the operation response mock should return exactly what was selected in the operation query, meaning a 1:1 match for the shape that would be returned from a successful query to a the real graphql endpoint. In this case, injecting __typename when it wasn't requested, would cause the mock to differ from the real API.
Please do push back if you think I'm missing something, but if your client needs __typename, shouldn't you request the __typename field in the operation? If you do, then it will be there both in this mock and in the actual graphql API you're using.
There was a problem hiding this comment.
but if your client needs __typename, shouldn't you request the __typename field in the operation
We are using the addTypename Apollo config set to true (the default). This makes Apollo automatically add __typename to all GQL documents, so all of the real server data includes __typename even though our documents as written don't explicitly request it.
This plugin also has an addTypename config which we also have set to true. So all mock data from aUser, etc, has __typename included as well. I think I'd expect your mock operation builders to do the same thing.
There was a problem hiding this comment.
If this patch compiled in your environemt, something in your setup must be adding __typename to operation types. Possibly documentTransforms?
The trouble is, not everyone does this. For me, I get:
Object literal may only specify known properties, and '__typename' does not exist in type 'GetUserQuery'
Because typescript-operations defines the types we're returning and it doesn't add __typename or even have a config for that. I think you could fix this by adding another documentTransforms. Something like:
import type { CodegenConfig } from '@graphql-codegen/cli';
import { addTypenameToDocument } from '@apollo/client/utilities';
const transformWithTypename = {
transform: ({ documents }) =>
documents.map((d) => ({ ...d, document: addTypenameToDocument(d.document) })),
};
const config: CodegenConfig = {
schema: '...',
documents: 'src/**/*.graphql',
generates: {
'src/generated/types.ts': {
plugins: ['typescript', 'typescript-operations'],
documentTransforms: [transformWithTypename], // maybe you already have this
},
'src/generated/mocks.ts': {
plugins: ['typescript-mock-data'],
config: { typesFile: './types' },
documentTransforms: [transformWithTypename], // and maybe you simply need this too
},
},
};The way I see it, the advantage of these operation factories is their return types is typescript-operations's ${OperationType}, producing a value that satisfies the type that the plugin generated from the document. aGetUserQueryResponse returns a GetUserQuery, the same type the your hook returns, the same type your components consume.
Here's a feature I've been working on to generate response objects for specific queries in a shape that exactly matches a specific query. I have had good luck testing it with a few of the cases in the actual application I maintain, but my testing so far has not been extensive. I am motivated by the desire to use test responses that more closely match the ones the real API will provide.
What I'd love to hear:
On to the description!
When documents is set in codegen config, emit one factory per named operation, shaped to that operation's selection-set type. Useful with msw and other tooling that needs operation-shaped data.
Leaf values flow through the existing mockValueGenerator, so scalar config, locale, and seed match the schema-typed factories. Aliases, __typename, nested objects, lists, and named fragment spreads are walked from the operation document.
Union and interface fields with __typename selected and two or more inline-fragment branches generate per-branch closures plus a dispatcher. The override slot accepts an object override (applied to the alphabetically-first branch) or a branch-keyed callback:
Lists of a union/interface follow the same shape:
Activated by generateOperationFactories: true (default) when documents are present and typesFile is set. Set the option to false to disable.