@vantreeseba/graphql-mocks
Generate realistic, graph-connected mock data from a GraphQL schema using faker.
Relationships are wired as actual object references — todo.user is the same object as mocks.User[i], not a copy. Useful for tests, Storybook stories, and demos.
Install
npm install @vantreeseba/graphql-mocks
# peer deps
npm install graphql @faker-js/faker
# optional — only for the /apollo link export
npm install @apollo/client
Usage
Basic
import { buildMocks } from '@vantreeseba/graphql-mocks';
import { schema } from './schema'; // your GraphQLSchema
const mocks = buildMocks(schema);
mocks.User // unknown[] — 5 User objects
mocks.Todo // unknown[] — 5 Todo objects, each .user points into mocks.User
SDL string input
const mocks = buildMocks(`
type User { id: ID!, name: String!, email: String! }
type Todo { id: ID!, title: String!, user: User! }
type Query { users: [User!]! }
`);
Count, seed, nullChance
const mocks = buildMocks(schema, {
seed: 42, // deterministic output
count: { User: 10, Todo: 50, _default: 5 }, // per-type counts
nullChance: 0.1, // 10% chance nullable fields are null
});
Custom scalar mockers
import { faker } from '@faker-js/faker';
const mocks = buildMocks(schema, {
faker,
scalars: {
DateTime: (f) => f.date.recent().toISOString(),
CityName: (f) => f.location.city(),
Rating: (f) => f.number.int({ min: 1, max: 5 }),
},
});
Field overrides
const mocks = buildMocks(schema, {
seed: 42,
overrides: {
User: {
name: () => 'Alice',
avatar: (faker) => faker.image.avatar(), // receives the same seeded faker
},
},
});
Override functions are passed the generator's faker instance, so they stay deterministic under seed without importing a separate faker, plus the site they're firing at — { index, typeName, fieldName }, where index is the instance's position in its own pool (the same number stableIds uses). That makes per-instance cohorts a one-liner:
const mocks = buildMocks(schema, {
overrides: {
User: { loginCount: (faker, { index }) => (index === 0 ? 0 : faker.number.int(500)) },
},
});
__typename and stable ids
Every object gets a __typename by default (the Apollo cache needs it). Turn it off with addTypename: false. Enable stableIds to give each object with an id field a readable, collision-free TypeName-<index> id instead of a random scalar:
const mocks = buildMocks(schema, { stableIds: true });
mocks.User[0]; // { __typename: 'User', id: 'User-0', ... }
An explicit overrides entry for id still wins over stableIds.
Interfaces / unions
const mocks = buildMocks(schema, {
resolveType: (abstractTypeName) => {
if (abstractTypeName === 'SearchResult') return 'Post';
return 'User';
},
});
Helpers
// Find a specific item. With a typed map (see Typed pools) the item is inferred:
const user = mocks.find('User', (u) => u.id === targetId);
// Without a typed map, pass the type explicitly:
const user2 = mocks.find<User>('User', (u) => u.id === targetId);
// Apollo Server / GraphQL Yoga mock resolvers
const resolvers = mocks.toResolvers();
// { User: () => <random User from pool>, Todo: () => <random Todo>, ... }
addMocksToSchema({ schema, mocks: resolvers });
// Resolve a query/mutation against the graph — data is shaped to the selection set.
// Root fields are picked from the pools by return type; nested fields follow the
// already-wired references. No need to assemble the response by hand.
const data = mocks.dataForOperation(UserByIdQuery);
// { user: { id, name, posts: [{ id, author: { id } }] } } — exactly the fields queried
dataForOperation understands lists, fragments, and interface/union fields (resolved via each mock's __typename). Variables are optional — any required ones are auto-filled with placeholders just so execution succeeds. By default they don't influence which mocks are chosen; turn on argument matching to make them select data. With a TypedDocumentNode the return type is inferred from the document.
Apollo MockedProvider
These helpers turn a TypedDocumentNode into an entry for Apollo's MockedProvider mocks array — no hand-written request/result boilerplate.
Call them off the mocks graph (mocks.mockOperation) and you need no data argument at all: the result is resolved straight from the graph (via dataForOperation), so the query's own selection set decides which mocks come back. The pool is already captured, so you just pass the document:
import { MockedProvider } from '@apollo/client/testing';
import { buildMocks } from '@vantreeseba/graphql-mocks';
import { AwardByIdQuery } from './graphql';
const mocks = buildMocks<SchemaTypeMap>(schema);
render(
<MockedProvider mocks={[mocks.mockOperation(AwardByIdQuery)]}>
<AwardCard />
</MockedProvider>,
);
Prefer to supply the data yourself? The standalone mockOperation takes the result data directly; the result/variables types are inferred from the document, so data is type-checked against the operation's result type:
import { mockOperation } from '@vantreeseba/graphql-mocks';
mockOperation(AwardByIdQuery, { award: mockAwards[0], __typename: 'Query' });
By default a mock matches any variables and may be used any number of times (maxUsageCount: Infinity). Override per call when you need exact matching, a delay, or an error (the same options apply to every helper here):
mocks.mockOperation(AwardByIdQuery, {
variables: { id: 'Award-0' }, // exact match (or a predicate (vars) => boolean)
delay: 50,
maxUsageCount: 1,
});
For the common "success / loading / error" trio, mockOperationVariants returns all three at once — mocks.mockOperationVariants resolves the success data from the graph, while the standalone mockOperationVariants(operation, data) takes it directly:
const m = mocks.mockOperationVariants(AwardByIdQuery);
m.withResults; // resolves with data drawn from the pool
m.withLongLoadTime; // stays pending — drive loading states
m.withError; // rejects with an error naming the operation
@graphql-typed-document-node/core (bundled with Apollo Client and graphql-codegen) provides the TypedDocumentNode type; it's an optional peer, only needed if you use these helpers.
Argument matching
By default arguments don't influence which mocks come back — user(id: "abc") returns a random pooled user. Set matchArguments (on buildMocks, on a handler, or per call) and arguments that land in one of three narrow buckets start selecting data:
const mocks = buildMocks(schema, { matchArguments: true, stableIds: true });
mocks.dataForOperation(parse('{ user(id: "User-2") { id name } }'));
// { user: { id: 'User-2', … } }
mocks.dataForOperation(parse('{ users(skip: 10, limit: 5) { id } }'));
// the 11th–15th pooled users, in stable order
mocks.dataForOperation(parse('{ posts(titleContains: "graph") { id title } }'));
// only posts whose title contains "graph"
| Bucket | Matches | Example |
|---|---|---|
| Equality | An argument named exactly like a scalar/enum field on the return type | todos(priority: HIGH), user(id: …) |
| Search | search, query, q, filter, searchTerm, term, or <field>Contains / <field>_contains |
users(search: "ana") |
| Paging | skip/offset plus limit/first/take, on list return types |
users(skip: 10, limit: 5) |
There is deliberately no fuzzy matching — no authorId → author.id, no snake/camel bridging, no suffix stripping. One inference is allowed: a list argument whose name minus a trailing s names a non-list scalar field (ids: [ID!] → id) becomes an in match. Anything else is ignored, exactly as with the flag off, because a wrong guess produces a silently empty screen.
Every list name is configurable, and matching can be narrowed by bucket:
buildMocks(schema, {
matchArguments: {
paging: true,
search: true,
equality: false,
nested: true, // user { posts(first: 2) } — on by default
limitArgs: ['limit', 'pageSize'],
ignoreArgs: ['locale'],
onMiss: { singular: 'fallback', list: 'empty' },
},
});
Variables you didn't supply are ignored. Required variables are auto-filled so execution can run, and any argument bound to one of those invented values is dropped — so mocks.mockOperation(UserByIdQuery) with no variables still returns a random pooled user, exactly as with matching off. A variable you pass, a literal, or a schema/document default counts as intent and is applied.
When nothing matches:
- A list returns
[]. An empty result is a wanted state — the most common empty-state story — and falling back would return rows that visibly contradict the filter. - A nullable singular field falls back to the random pick. A miss means "you named an id we never generated";
nullwould turn a working screen into an unrequested not-found path. SetonMiss: { singular: 'empty' }if you want the not-found path. - A non-null singular field always falls back, whatever
onMisssays —nullthere is a GraphQL error plus a warning, which is strictly worse than a random item. - Paging never falls back:
skip: 100over 5 items legitimately yields[].
Paging switches the source from a random subset to the whole pool in stable order, so pages line up. Pools hold count items (default 5) and lists draw listSize items (default 1–5) — raise both when you need more than one page:
buildMocks(schema, { count: 50, listSize: { min: 10, max: 20 }, matchArguments: true });
Known limitation. When a root field returns a wrapper type ({ totalCount, results }), the entity list sits one level below the arguments and the engine cannot connect them. Use a resolver function plus the exported paginate / searchItems there:
import { paginate, searchItems } from '@vantreeseba/graphql-mocks';
mocks.mockOperation(SearchUsersQuery, {
dynamic: true,
transform: (data, vars) => ({
searchUsers: {
...data.searchUsers,
results: paginate(searchItems(mocks.User, vars.term), vars),
},
}),
});
Resolver-function mocks
mockOperation and mockOperationVariants also take a function of the incoming variables, so one mock answers many variable combinations instead of one envelope per case:
mockOperation(UserByIdQuery, (vars) => ({ user: usersById[vars.id] }));
// → { request, result: (vars) => ({ data }) }
The static overload is unchanged: passing plain data still yields result: { data }, so existing mock.result?.data reads keep working and keep their types.
On the graph-bound form, pass dynamic: true to resolve from the graph per request — which is what lets real incoming variables reach argument matching even though request.variables matches anything:
mocks.mockOperation(SearchUsersQuery, { dynamic: true, matchArguments: true });
transform: (data, variables) => data post-processes whichever path runs, and matchArguments overrides the graph-wide setting for this operation only.
A transport for any operation
mocks.toRequestHandler() answers any operation from the graph — no per-operation registration, so one handler covers a whole screen:
const handler = mocks.toRequestHandler();
const { data } = await handler({ query: UsersQuery });
const { data: one } = await handler({ query: UserByIdQuery, variables: { id } });
Results are memoized per document + variables, so a refetch or a second identical query in the same render tree returns the same rows instead of a fresh random draw (memoize: false to opt out). Only values produced by execution are returned — never a pooled object or anything reachable from one — so results are acyclic and safe to clone.
Overrides force specific operations into a state, first match wins:
const handler = mocks.toRequestHandler({
delay: { min: 20, max: 80 },
overrides: [
{ match: 'UserById', loading: true }, // never settles, schedules no timer
{ match: TodosQuery, errors: 'Something went wrong' }, // { data: null, errors }
{ match: (op) => op.operationType === 'mutation', networkError: 'offline' }, // rejects
{ match: 'Users', data: (op, fromGraph) => ({ users: fromGraph.users.slice(0, 1) }) },
{ match: 'Users', errors: 'first time only', once: true },
],
});
handler.calls; // every operation seen, in order: name, type, variables, document
handler.reset(); // clears the memo, the calls, and consumed `once` overrides
Mutations run through the same path and never mutate the pool — that would make stories order-dependent across re-renders and HMR, and real write semantics are app-specific. Close an override's data function over your own state when you need a write to stick.
Apollo
@vantreeseba/graphql-mocks/apollo wraps a graph (or a handler) in an ApolloLink. @apollo/client is an optional peer (>=3.8 <5), so the root entry stays dependency-free:
import { ApolloClient, ApolloProvider, InMemoryCache } from '@apollo/client';
import { buildMocks } from '@vantreeseba/graphql-mocks';
import { mockLink } from '@vantreeseba/graphql-mocks/apollo';
const mocks = buildMocks(schema, { seed: 1, stableIds: true });
const client = new ApolloClient({ cache: new InMemoryCache(), link: mockLink(mocks) });
render(
<ApolloProvider client={client}>
<Screen />
</ApolloProvider>,
);
Pass a handler instead of a graph when you want its spy surface:
const handler = mocks.toRequestHandler({ overrides: [{ match: 'Users', loading: true }] });
const client = new ApolloClient({ cache: new InMemoryCache(), link: mockLink(handler) });
// …assert on handler.calls
Story states
mockScenarios builds the three states a component is usually exercised in, from one base config:
import { mockScenarios } from '@vantreeseba/graphql-mocks';
const states = mockScenarios({ matchArguments: true });
// states.default | states.loading | states.errored — each MockHandlerOptions
Pass a target to put only some operations into the loading/error state, leaving the rest resolving normally — what a screen with one failing panel needs:
mockScenarios({}, 'UserById'); // one operation
mockScenarios({}, ['UserById', TodosQuery]); // several
mockScenarios({}, (op) => op.operationType === 'mutation');
There's no Storybook dependency and no CSF types here — the parameter key and the spread into a story belong to your Storybook addon, which churns across majors. A decorator is a few lines:
// .storybook/preview.tsx
import { ApolloClient, ApolloProvider, InMemoryCache } from '@apollo/client';
import { buildMocks } from '@vantreeseba/graphql-mocks';
import { mockLink } from '@vantreeseba/graphql-mocks/apollo';
import { schema } from './schema';
export const decorators = [
(Story, context) => {
const mocks = buildMocks(schema, { seed: 1, stableIds: true, matchArguments: true });
const client = new ApolloClient({
cache: new InMemoryCache(),
link: mockLink(mocks, context.parameters.graphqlMocks ?? {}),
});
return (
<ApolloProvider client={client}>
<Story />
</ApolloProvider>
);
},
];
// SomeScreen.stories.tsx
const states = mockScenarios({}, 'UserById');
export const Default = { parameters: { graphqlMocks: states.default } };
export const Loading = { parameters: { graphqlMocks: states.loading } };
export const Errored = { parameters: { graphqlMocks: states.errored } };
The same shape works in component tests:
import { type MockHandlerOptions, buildMocks } from '@vantreeseba/graphql-mocks';
function renderWithMocks(ui: React.ReactElement, options: MockHandlerOptions = {}) {
const mocks = buildMocks(schema, { seed: 1, stableIds: true, matchArguments: true });
const handler = mocks.toRequestHandler(options);
const client = new ApolloClient({ cache: new InMemoryCache(), link: mockLink(handler) });
return { mocks, handler, ...render(<ApolloProvider client={client}>{ui}</ApolloProvider>) };
}
Addressing pooled data
mocks.ids('User'); // ['User-0', 'User-1', …] in generation order
mocks.at('User', 0); // the first pooled User
mocks.byId('User', 'User-2'); // looked up by id, compared as strings
ids needs no TTypes map to come back typed, which at('User', 0)?.id does under noUncheckedIndexedAccess:
const id = mocks.ids('User')[0] as string;
mocks.mockOperation(UserByIdQuery, { variables: { id }, matchArguments: true });
Pair it with stableIds: true for readable, stable values.
Deriving mocks from a document module
import * as operations from './queries.generated';
const opMocks = mocks.mockOperationsFrom(operations);
opMocks.UserByIdDocument.withResults;
opMocks.TodosDocument.withError;
Keys are the module's export names, not operation names — operation names live only in the runtime AST, so keying by them would make the type unsound. Non-document exports are skipped. Entries are built lazily on first read, so a fifty-document module costs nothing at import time; spreading the map forces all of them, Object.keys does not.
Collection helpers
The same primitives the argument engine uses, exported for the cases it can't reach:
import { paginate, searchItems } from '@vantreeseba/graphql-mocks';
paginate(mocks.User, { skip: 10, limit: 5 }); // also offset/first/take
searchItems(mocks.User, 'ana'); // every string field
searchItems(mocks.User, 'ana', ['name']); // named fields only
Absent or null arguments are no-ops, so they're safe to apply unconditionally.
QA mode
Mocks are realistic by default, and realistic data never finds the bug where a 400-character
product name blows out a flex row, or an empty list renders a blank panel instead of an empty
state. The qa option swaps the generators for deliberately out-of-norm ones, so the same
buildMocks call your story already makes can produce the data that breaks it.
// a named preset
const mocks = buildMocks(schema, { seed: 42, qa: 'longText' });
// or tune the dimensions yourself
const mocks = buildMocks(schema, {
qa: { text: 'unicode', lists: 'huge', nulls: 'mixed', numbers: 'boundary' },
});
Presets
buildQaSets generates one mock pool per preset — the shape Storybook and MockedProvider
want, one variant per row:
import { buildQaSets } from '@vantreeseba/graphql-mocks';
const sets = buildQaSets(schema, { seed: 42 });
// [{ name: 'emptyText', qa: { text: 'empty' }, mocks }, { name: 'whitespaceText', ... }, ...]
// or just the ones you care about
const sets = buildQaSets(schema, { seed: 42, profiles: ['emptyText', 'hugeLists'] });
| Preset | Config | What it stresses |
|---|---|---|
emptyText |
{ text: 'empty' } |
Empty strings — labels, headings, alt text |
whitespaceText |
{ text: 'whitespace' } |
Spaces, tabs, newlines, non-breaking spaces |
longText |
{ text: 'long' } |
1k-char unbroken tokens and long prose — overflow, truncation |
unicodeText |
{ text: 'unicode' } |
ZWJ emoji, RTL, bidi, CJK, combining marks, zalgo |
injectionText |
{ text: 'injection' } |
<script>, template syntax, path traversal — escaping |
emptyLists |
{ lists: 'empty' } |
Empty states |
singleItemLists |
{ lists: 'single' } |
"1 item" grammar, single-row layouts |
hugeLists |
{ lists: 'huge' } |
100-item lists — virtualization, pagination, perf |
allNulls |
{ nulls: 'all' } |
Every nullable field null |
mixedNulls |
{ nulls: 'mixed' } |
Partial nulls — the realistic failure mode |
zeroNumbers |
{ numbers: 'zero' } |
0 everywhere — division, percentages, empty totals |
negativeNumbers |
{ numbers: 'negative' } |
Negative counts, prices, durations |
boundaryNumbers |
{ numbers: 'boundary' } |
Int 32-bit limits, MAX_SAFE_INTEGER, -0, 0.1 + 0.2 |
extremeDates |
{ dates: 'mixed' } |
Epoch, far past/future, leap day, DST transitions |
kitchenSink |
all of the above | Everything at once |
QA_PROFILE_NAMES and QA_PROFILES are exported if you want to build the list yourself.
With Storybook + Apollo
const sets = buildQaSets(schema, { seed: 42, profiles: ['emptyText', 'longText', 'emptyLists'] });
export const QaVariants = sets.map((set) => ({
name: set.name,
parameters: { apolloClient: { mocks: [set.mocks.mockOperation(UsersQuery)] } },
}));
Each set is generated from its own faker instance seeded with seed, so a set reproduces
identically no matter which other presets ran alongside it — when one variant breaks, rerunning
just that preset gives you the same data back.
Notes
scalarsandoverridesstill win. QA only replaces the generators you haven't defined yourself, so a field you pinned stays pinned.IDis left alone. Ids are graph identity and Apollo cache keys; mangling them would break wiring rather than test it.- Values stay serializable by the built-in scalars (
Intis clamped to its 32-bit range, for instance), but custom scalar constraints are deliberately not respected — a negativeNonNegativeIntis the point, not a bug. lists: 'huge'raises the defaultcounttolistSize(100), because relationship lists are sampled from the pools without replacement. An explicitcountstill wins, which caps how long those lists can get.qa: falsedisables QA, handy when the preset comes from a variable.- Pair it with a scenario to vary the state as well as the kind of data;
buildMatrixcrosses the two axes for you.
Scenarios
QA mode varies the kind of data. Scenarios vary the state: a user who just signed up and has
nothing, a power user with 200 todos, an empty workspace. That's about amounts and about which
things are connected to which — so alongside count and overrides, there's relations.
relations
relations shapes relationship fields after every pool exists, which is what overrides
structurally cannot do (overrides run before the other pools are built).
const mocks = buildMocks(schema, {
relations: {
User: { todos: 0, posts: { min: 1, max: 2 } }, // exact size, or a range
Post: { comments: 'all', author: ({ pool }) => pool[0] },
Query: { users: 3 }, // root fields too
_default: { min: 1, max: 5 }, // fallback for everything else
},
});
A spec is a number, a { min, max } range, null (empty the field), 'all' (the whole target
pool), or a function that picks the value outright:
relations: { User: { todos: ({ pool, index }) => pool.filter((t) => t.ownerIndex === index) } }
The function receives { pool, faker, index, instance, typeName, fieldName, isList }, where
pool is the target type's pool and instance is the owner as built so far.
Lookup goes most specific first: [type][field] → [type]._default → _default → the flat
top-level form (relations: 0 empties every relationship in the graph). Ranges live under a
key; a bare object is always a map — so a top-level range is written _default: { min, max }.
Notes:
- An explicit spec beats both
nullChanceand the QAlistsprofile — the per-field lever is the more specific one. Anoverridesentry for the same field still wins overrelations. - The pools grow to meet demand:
{ User: { todos: 20 } }mocks at least 20 todos, since lists are sampled without replacement. An explicitcountstill wins, and caps the list. - Config errors throw rather than producing a broken graph: an unknown type or field, a spec on
a scalar field, or emptying a non-null singular field (
Todo: { user: null }againstuser: User!) is aTypeError; a negative or non-integer size is aRangeError. A catch-all that would empty a non-null singular field is coerced back to one instead, sorelations: 0means "as empty as the schema allows" and never yields an unexecutable graph. - Wiring is one-directional by default:
user.todos[0].useris some other user. Setrelations: { _reciprocal: true }to have each reference written back into its inverse field where one exists unambiguously. It's lossy in one direction — a todo in two users' lists can only point at one owner, and the last write wins.
Named scenarios
A scenario is a named partial buildMocks config. defineScenarios is an identity function that
keeps the literal keys; satisfies ScenarioMap<SchemaTypeMap> adds schema-checked type and field
names.
import { buildMocks, defineScenarios } from '@vantreeseba/graphql-mocks';
export const scenarios = defineScenarios({
newUser: {
description: 'signed up, has done nothing yet',
count: { User: 1 },
relations: { User: { todos: null, posts: null } },
overrides: { User: { loginCount: () => 0 } },
},
powerUser: {
relations: { User: { todos: 200, posts: { min: 20, max: 40 } } },
},
});
const mocks = buildMocks(schema, { scenario: scenarios.newUser, seed: 42 });
scenario also takes an array, applied left to right with the explicit options merged last:
buildMocks(schema, { scenario: [scenarios.newUser, scenarios.offline], count: 3, seed: 42 });
composeScenarios(a, b) does the same fold eagerly and hands back an ordinary scenario, so it can
be composed further. Maps merge key by key — count per type, overrides and relations per
type then per field, scalars by scalar name, qa per dimension — and everything else is
last-one-wins. faker and seed are build-level only; reproducibility stays the caller's.
One precedence wrinkle: a scalars entry always outranks the QA generator for that scalar,
whichever layer each came from. A later qa layer therefore can't reach a scalar an earlier
layer pinned — the merge warns when that happens rather than silently doing the surprising thing.
buildMatrix
Cross the scenarios with the QA presets and get one flat array of cells — one story, one test case, one row each:
import { buildMatrix } from '@vantreeseba/graphql-mocks';
const cells = buildMatrix(schema, {
scenarios,
qaPresets: [false, 'longText', 'hugeLists'],
seed: 42,
});
// [{ name: 'newUser × noQa', scenario: 'newUser', qa: 'noQa', options, mocks }, ...]
export const Variants = cells.map((cell) => ({
name: cell.name,
parameters: { apolloClient: { mocks: [cell.mocks.mockOperation(UsersQuery)] } },
}));
Either axis may be omitted; qaPresets also takes a map ({ baseline: false, huge: { lists: 'huge' } }) when you want your own cell names. Each cell gets its own faker seeded from seed,
so a cell reproduces identically no matter which other cells were requested — seedPerCell: true
opts out when you'd rather the cells differ. With stableIds, each cell's ids are prefixed with a
slug of its name so pools from different cells don't collide; set idPrefix yourself to override.
buildQaSets is the QA-only shorthand for the same engine.
Typed pools
Pools are unknown[] by default — the type names and shapes only exist at runtime (in the schema), so they can't be inferred from the schema argument. Pass an optional TTypes map to declare them and the matching pools come back typed, no cast needed:
const mocks = buildMocks<{ User: User; Todo: Todo }>(schema);
mocks.User // User[]
mocks.Todo // Todo[]
mocks.Other // still unknown[] — any type not in the map falls back
Auto-typing with GraphQL Code Generator
Rather than hand-maintaining the map, generate it from the schema so every type is typed automatically. Add a tiny custom plugin that emits a name → type map alongside the standard typescript plugin:
// codegen/type-map-plugin.cjs
const { isObjectType } = require('graphql');
module.exports.plugin = (schema) => {
// Exclude root operation types — you don't mock Query/Mutation/Subscription as pools.
const roots = new Set(
[schema.getQueryType(), schema.getMutationType(), schema.getSubscriptionType()]
.filter(Boolean)
.map((t) => t.name),
);
const names = Object.values(schema.getTypeMap())
.filter((t) => isObjectType(t) && !t.name.startsWith('__') && !roots.has(t.name))
.map((t) => t.name)
.sort();
return {
content: `export type SchemaTypeMap = {\n${names
.map((n) => ` ${n}: ${n};`)
.join('\n')}\n};\n`,
};
};
Run it right after typescript so the referenced types are defined in the same file:
// codegen.ts
import type { CodegenConfig } from '@graphql-codegen/cli';
const config: CodegenConfig = {
schema: './schema.graphql',
generates: {
'./src/generated/graphql.ts': {
plugins: ['typescript', './codegen/type-map-plugin.cjs'],
},
},
};
export default config;
This produces:
export type SchemaTypeMap = {
Todo: Todo;
User: User;
// ...every object type
};
Use it as the default type parameter on your own wrapper so callers get typed pools with zero annotation:
import { buildMocks, type BuildMocksOptions, type MockResult } from '@vantreeseba/graphql-mocks';
import type { SchemaTypeMap } from './generated/graphql';
export function getMocks<
TTypes extends Record<string, unknown> = SchemaTypeMap,
>(options?: BuildMocksOptions<TTypes>): MockResult<TTypes> {
return buildMocks<TTypes>(schemaSDL, options);
}
getMocks().User // User[] — no generic, no cast
getMocks<{ User: UserFragment }>().User // override per-call when you want a fragment shape
The generated typescript types add __typename?: 'User' by default and wrap nullable fields as Maybe<T>, which lines up with the mock output (with nullChance: 0, nothing is null). For typed one-off lookups without the map, find<User>('User', …) also works.
Options
| Option | Type | Default | Description |
|---|---|---|---|
count |
number | { [type]: number, _default?: number } |
5 |
Instances per type |
faker |
Faker |
internal | Custom faker instance (e.g. locale) |
seed |
number |
— | Seed faker for deterministic output |
nullChance |
number |
0 |
Probability (0–1) nullable fields are null |
scalars |
Record<string, (faker) => unknown> |
— | Custom scalar mockers (merged over defaults) |
overrides |
Record<type, Record<field, (faker, ctx) => unknown>> |
— | Per-field replacement functions (receive the seeded faker and { index, typeName, fieldName }). With a TTypes map, type/field keys autocomplete and each return type is bound to the field's type |
resolveType |
(abstractType: string) => string |
— | Concrete type for interface/union fields. With a TTypes map, the return is constrained to the map's type names |
addTypename |
boolean |
true |
Add __typename to every object (Apollo cache needs it) |
stableIds |
boolean |
false |
Give id fields stable TypeName-<index> values |
idPrefix |
string |
'' |
Prefix for stableIds ids (<prefix>User-0), so pools built in one run don't collide |
listSize |
number | { min: number, max: number } |
{ min: 1, max: 5 } |
How many items generated list fields hold, unless a QA lists profile or a relations entry says otherwise |
qa |
QaProfileName | QaConfig | false |
— | QA mode — generate deliberately out-of-norm data (empty/long/unicode text, empty/huge lists, nulls, boundary numbers and dates) |
relations |
RelationsConfig |
— | Shape relationships — sizes, ranges, null, 'all', or a function picking the related objects |
scenario |
Scenario | Scenario[] |
— | Scenario layers to build on, applied left to right with these options last |
matchArguments |
boolean | ArgMatchingOptions |
false |
Let field arguments select data — see Argument matching |