Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 139 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,145 @@ Adds `__typename` property to mock data

Changes enums to TypeScript string union types

### generateOperationFactories (`boolean`, default: `true`)

When the codegen `documents` config is non-empty, the plugin emits one factory per named operation, shaped to that operation's selection-set type. This is useful with [msw](https://mswjs.io/) or any tooling that needs operation-shaped data without hand-rolling the response literal.

Requires `typesFile` to be set. Set `generateOperationFactories: false` to disable the feature.

Assume the schema and operations below for the rest of this section:

```graphql
interface Node {
id: ID!
}

type User implements Node {
id: ID!
name: String!
friends: [User!]!
}

type Document implements Node {
id: ID!
title: String!
}

union SearchHit = User | Document

type Query {
user(id: ID!): User
node(id: ID!): Node
search(q: String!): [SearchHit!]!
}
```

```graphql
query GetUser($id: ID!) {
user(id: $id) {
id
name
friends {
id
name
}
}
}

query GetNode($id: ID!) {
node(id: $id) {
__typename
... on User {
id
name
}
... on Document {
id
title
}
}
}

query Search($q: String!) {
search(q: $q) {
__typename
... on User {
id
name
}
... on Document {
id
title
}
}
}
```

#### Basic usage

The factory returns a fully-populated response shaped to the operation's selection set. Pass a partial override to customize specific fields:

```ts
import { aGetUserQueryResponse } from './mocks.generated';

const response = aGetUserQueryResponse({
user: { name: 'Alice' },
});
// response.user.id is auto-generated; response.user.name is 'Alice'
// response.user.friends is populated with default-shaped User entries
```

#### Lists

Arrays of object/scalar elements take either a literal array or a `(make) => Element[]` callback. The callback's `make` argument produces one default-populated element per call, and accepts a partial override:

```ts
aGetUserQueryResponse({
user: {
friends: (make) => [
make(), // default User
make({ name: 'Bob' }), // overridden name
make({ id: 'pinned-friend' }),
],
},
});
```

A literal array replaces the defaults entirely. Use this form when you already have fully-built objects.

#### Union and interface fields

When an operation's union/interface field selects `__typename` and two or more concrete branches via inline fragments, the override slot accepts a branch-keyed callback. Each key is a concrete type name; each value is a factory for that branch's operation-shaped subset:

```ts
import { aGetNodeQueryResponse } from './mocks.generated';

// Default branch: alphabetically-first concrete type (Document here).
aGetNodeQueryResponse();

// Object override targets the default branch.
aGetNodeQueryResponse({
node: { title: 'Quarterly Report' },
});

// Branch callback picks a non-default branch with full type inference.
aGetNodeQueryResponse({
node: ({ User }) => User({ name: 'Alice' }),
});
```

For lists of a union/interface, the array slot likewise accepts a branch-keyed callback. Build the array directly by calling the per-branch factories:

```ts
import { aSearchQueryResponse } from './mocks.generated';

aSearchQueryResponse({
search: ({ User, Document }) => [User({ name: 'Alice' }), Document({ title: 'Q3 Report' }), User()],
});
```

If the operation does **not** select `__typename`, branch dispatch isn't generated for that field. The override slot falls back to a partial of the alphabetically-first branch. Add `__typename` to the operation if you want callback-style branch overrides.

### includedTypes (`string[]`, defaultValue: `undefined`)

Specifies an array of types to **include** in the mock generation. When provided, only the types listed in this array will have mock data generated.
Expand Down
36 changes: 35 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { sentenceCase } from 'sentence-case';
import a from 'indefinite';
import { printSchemaWithDirectives } from '@graphql-tools/utils';
import { setupFunctionTokens, setupMockValueGenerator } from './mockValueGenerator';
import { buildOperationFactories } from './operationFactories';

type NamingConvention = 'change-case-all#pascalCase' | 'keep' | string;

Expand Down Expand Up @@ -446,7 +447,7 @@ const getNamedType = (opts: Options<NamedTypeNode | ObjectTypeDefinitionNode>):
}
};

const generateMockValue = (opts: Options): string | number | boolean => {
export const generateMockValue = (opts: Options): string | number | boolean => {
switch (opts.currentType.kind) {
case 'NamedType':
return getNamedType({
Expand Down Expand Up @@ -542,6 +543,7 @@ const getImportTypes = ({
enumsAsTypes,
useTypeImports,
typeNamesMapping,
extraTypeImports,
}: {
typeNamesConvention: NamingConvention;
definitions: any;
Expand All @@ -553,6 +555,7 @@ const getImportTypes = ({
enumsAsTypes: boolean;
useTypeImports: boolean;
typeNamesMapping?: Record<string, string>;
extraTypeImports?: string[];
}) => {
const typenameConverter = createNameConverter(typeNamesConvention, transformUnderscore);
const typeImports = typesPrefix?.endsWith('.')
Expand All @@ -571,6 +574,10 @@ const getImportTypes = ({
renamedTypeImports.push(...enumTypes);
}

if (extraTypeImports && extraTypeImports.length > 0) {
renamedTypeImports.push(...extraTypeImports);
}

function onlyUnique(value, index, self) {
return self.indexOf(value) === index;
}
Expand Down Expand Up @@ -635,6 +642,7 @@ export interface TypescriptMocksPluginConfig {
typeNamesMapping?: Record<string, string>;
includedTypes?: string[];
excludedTypes?: string[];
generateOperationFactories?: boolean;
}

interface TypeItem {
Expand Down Expand Up @@ -912,6 +920,29 @@ export const plugin: PluginFunction<TypescriptMocksPluginConfig> = (schema, docu
);
const typesFile = config.typesFile ? config.typesFile.replace(/\.[\w]+$/, '') : null;

// Generate operation factories first to collect operation type imports
let operationOutput = '';
let operationTypeImports: string[] = [];
const generateOps = config.generateOperationFactories !== false && documents && documents.length > 0;
if (generateOps) {
if (!config.typesFile) {
throw new Error(
'Plugin "typescript-mock-data" requires `typesFile` to be set when generating operation factories. ' +
'Either set `typesFile: <path>` or `generateOperationFactories: false`.',
);
}
const { output, operationTypeImports: opTypeImports } = buildOperationFactories({
schema,
documents,
typesFile: config.typesFile,
listElementCount,
prefix: config.prefix,
sharedGenerateMockOpts,
});
operationOutput = output;
operationTypeImports = opTypeImports;
}

const typesFileImport = getImportTypes({
typeNamesConvention,
definitions,
Expand All @@ -923,6 +954,7 @@ export const plugin: PluginFunction<TypescriptMocksPluginConfig> = (schema, docu
useTypeImports: config.useTypeImports,
enumsAsTypes,
typeNamesMapping,
extraTypeImports: operationTypeImports,
});
// Function that will generate the mocks.
// We generate it after having visited because we need to distinct types from enums
Expand All @@ -940,5 +972,7 @@ export const plugin: PluginFunction<TypescriptMocksPluginConfig> = (schema, docu
mockFile += mockFns;
if (dynamicValues) mockFile += `\n\n${functionTokens.seedFunction}`;
mockFile += '\n';
mockFile += operationOutput;

return mockFile;
};
Loading