mizdra / graphql-codegen-typescript-fabbrica Goto Github PK
View Code? Open in Web Editor NEWGraphQL Code Generator Plugin to define fake data factory.
License: MIT License
GraphQL Code Generator Plugin to define fake data factory.
License: MIT License
This problem occurs with factories where no traits are defined.
import {
defineBookFactory,
defineAuthorFactory,
dynamic,
} from '../__generated__/fabbrica';
import { test, expect, expectTypeOf } from 'vitest';
// Define factories
const BookFactory = defineBookFactory({
defaultFields: {
__typename: 'Book',
id: dynamic(({ seq }) => `Book-${seq}`),
title: 'Yuyushiki',
author: undefined,
},
});
const AuthorFactory = defineAuthorFactory({
defaultFields: {
__typename: 'Author',
id: dynamic(({ seq }) => `Author-${seq}`),
name: 'Komata Mikami',
books: undefined,
},
traits: {
withoutBooks: {
defaultFields: {
books: [],
},
},
},
});
test('throw a compile error when an unknown trait name is passed to `.use()`', async () => {
// @ts-expect-error -- Should throw a compile error, but does not throw.
await BookFactory.use('unknownTrantName').build();
// @ts-expect-error
await AuthorFactory.use('unknownTrantName').build();
});
$ npm start
> start
> npm run gen && npm run lint && npm run test
> gen
> graphql-codegen
✔ Parse Configuration
✔ Generate outputs
> lint
> tsc
src/index.test.ts:34:3 - error TS2578: Unused '@ts-expect-error' directive.
34 // @ts-expect-error -- Should throw a compile error, but does not throw.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Found 1 error in src/index.test.ts:34
npm ERR! code EIO
npm ERR! syscall write
npm ERR! errno -5
npm ERR! EIO: i/o error, write
npm ERR! A complete log of this run can be found in: /home/.npm/_logs/2024-03-09T05_46_36_334Z-debug-0.log
jsh: spawn npm EIO
@mizdra/graphql-codegen-typescript-fabbrica version: 0.3.2
typescript version (optional):
@graphql-codegen/cli version (optional):
@graphql-codegen/typescript version (optional):
I would expect a type error to be reported when an unknown field is passed to defaultFields
, but it is not.
type Book {
title: String!
}
import { defineBookFactory } from '../__generated__/fabbrica';
const BookFactory = defineBookFactory({
defaultFields: {
title: 'Yuyushiki',
// @ts-expect-error I expect a type error to be reported, but it is not.
unknownField: 'hello',
},
});
A type error is reported.
No type errors are reported.
It may be necessary to assign a dedicated type parameter for each field.
get
function does not return a usable type.
get('author')
is Promise<OptionalAuthor | undefined>
.OptionalAuthor
means { id?: string | undefined, name?: string | undefined, email?: string | undefined }
.author
may be undefined
. This is inconvenient for the user.get
function is not very useful.get(...)
: get('name')
await
keyword before get(...)
: await get('name')
(await get('name')) ?? 'defaultName'
`${(await get('name')) ?? 'defaultName'}@yuyushiki.net`
dynamic
function: dynamic(async ({ get }) => `${(await get('name')) ?? 'defaultName'}@yuyushiki.net`)
Change the interface as follows:
seq
in defaultFields
and .build()
functionconst BookFactory = defineBookFactory({
defaultFields: {
id: dynamic(({ seq }) => `Book-${seq}`),
name: 'Book',
},
});
const book = await BookFactory.build({
name: dynamic(({ seq }) => `Book-${seq}`),
});
const BookFactory = defineBookFactory({
defaultFields: (({ seq }) => ({
id: `Book-${seq}`,
name: 'Book',
})),
});
const book = await BookFactory.build(({ seq }) => ({
name: `Book-${seq}`,
}));
get
for Dependent Fieldsconst AuthorFactory = defineAuthorFactory({
defaultFields: {
name: 'mikamikomata',
email: dynamic(async ({ get }) => `${(await get('name')) ?? 'defaultName'}@yuyushiki.net`),
},
});
const AuthorFactory = defineAuthorFactory({
defaultFields: (({ seq }) => {
const name = 'mikamikomata';
return {
name,
email: `${name}@yuyushiki.net`,
};
}),
});
get
for Transient Fields (depend: #73)const AuthorFactory = defineAuthorFactory.withTransientFields({
bookCount: 0,
})({
defaultFields: {
books: dynamic(async ({ get }) => {
const bookCount = (await get('bookCount')) ?? 0;
return BookFactory.buildList(bookCount);
}),
},
});
const AuthorFactory = defineAuthorFactory.withTransientFields({
bookCount: 0,
})({
defaultFields: async ({ transientFields: { bookCount } }) => ({
books: await BookFactory.buildList(bookCount),
}),
});
get
functiondynamic
utilityAn interface that returns undefined complicates the code.
const UserFactory = defineUserFactory({
defaultFields: {
id: dynamic(({ seq }) => `User-${seq}`),
name: 'yukari',
email: dynamic(async ({ get }) => `${(await get('name')) ?? 'defaultName'}@yuyushiki.net`),
},
});
An interface that throws an exception instead of returning undefined simplifies the code.
const UserFactory = defineUserFactory({
defaultFields: {
id: dynamic(({ seq }) => `User-${seq}`),
name: 'yukari',
email: dynamic(async ({ get }) => `${(await get('name'))}@yuyushiki.net`),
},
});
Global Transient Fields can define special Transient Fields that also affects build
methods called on deep stacks.
import {
defineBookFactory,
defineBookShelfFactory,
} from '../__generated__/fabbrica';
declare module '@mizdra/graphql-codegen-typescript-fabbrica/helper' {
interface GlobalTransientFields {
$user: { __typename: 'User', name: string };
}
}
const BookFactory = defineBookFactory({
defaultFields: {
__typename: 'Book',
id: dynamic(({ seq }) => `Book:${seq}`),
title: 'old-book',
author: dynamic(async ({ get }) => await get('$user')!),
},
});
const BookShelfFactory = defineBookShelfFactory({
defaultFields: {
__typename: 'BookShelf',
id: ({ seq }): `BookShelf:${seq}`,
name: 'my-bookshelf',
owner: dynamic(async ({ get }) => await get('$user')!),
books: dynamic(async () => await BookFactory.buildList()),
},
});
const bookShelf = BookShelfFactory.build({
$user: { __typename: 'User', name: 'mizdra' },
});
expect(bookShelf).toStrictEqual({
__typename: 'BookShelf',
id: 'BookShelf:0',
name: 'my-bookshelf',
owner: { __typename: 'User', name: 'mizdra' },
books: [
{
id: 'Book:0',
title: 'old-book',
author: { __typename: 'User', name: 'mizdra' },
},
{
id: 'Book:1',
title: 'old-book',
author: { __typename: 'User', name: 'mizdra' },
},
],
});
I think that https://github.com/tc39/proposal-async-context is required to implement this feature.
blocked by: #73
Alias is a feature that allows you to change the result of a field to any name you want.
This can be used to generate a response with a field that does not exist in Author
type.
const query = graphql`
query ExampleQuery @raw_response_type {
author(id: "1") {
id
name
name2 # alias
}
}
`;
const data = useClientQuery(query);
console.log(data);
// output:
// {
// author: {
// id: "1",
// name: "Mikami Komata",
// name2: "Mikami Komata",
// },
// }
However, graphql-codegen-typescript-fabbrica cannot generate alias-derived fields. This makes it difficult to build responses for queries that use aliases.
const AuthorFactory = defineAuthorFactory({
defaultFields: {
id: dynamic(({ seq }) => `Author-${seq}`),
name: "Komata Mikami",
},
});
const author = await AuthorFactory.build();
// ^? { id: string, name: string }
const dummyResponse: ExampleQuery$rawResponse = { author };
// ^^^^^^^^^^^^^ error: author.name2 is missing
Allow defaultFields
to accept alias-derived fields. The interface is designed with reference to Quramy/prisma-fabbrica#252.
import { type OptionalAuthor } from '../__generated__/fabbrica';
const AuthorFactory = defineAuthorFactory.withAdditionalFields<{ name2: OptionalAuthor['name'] }>()({
defaultFields: {
id: dynamic(({ seq }) => `Author-${seq}`),
name: "Komata Mikami",
name2: "Komata Mikami", // alias-derived field
},
});
const author = await AuthorFactory.build();
// ^? { id: string, name: string, name2: string }
const dummyResponse: ExampleQuery$rawResponse = { author }; // ok
In the current implementation of graphql-codegen-typescript-fabbrica, fields not included in type are treated as transient fields. transient fields are not included in the built data.
Therefore, to implement this proposal, we have to change the interface of transient fields.
pkg.exports
field does not work with tsconfig.moduleResolution === 'node'
. Therefore, this field should not be used.
Add a utility that allows users to build connections without defining XxxEdgeFactory
or XxxConnectionFactory
.
import { defineBookFactory, dynamic } from '../__generated__/fabbrica';
const PageInfoFactory = definePageInfoFactory({
defaultFields: {
__typename: 'PageInfo',
},
});
const BookFactory = defineBookFactory({
defaultFields: {
__typename: 'Book',
id: dynamic(({ seq }) => `Book-${seq}`),
},
PageInfoFactory,
});
// Basic
const bookConnection1 = await BookFactory.connection(2);
expect(bookConnection1).toStrictEqual({
__typename: 'BookConnection',
edges: [
{ __typename: 'BookEdge', node: { __typename: 'Book', id: 'Book-1' }, cursor: 'BookEdge-1' },
{ __typename: 'BookEdge', node: { __typename: 'Book', id: 'Book-2' }, cursor: 'BookEdge-2' },
],
pageInfo: {
__typename: 'PageInfo',
startCursor: 'BookEdge-1',
endCursor: 'BookEdge-2',
},
});
BookFactory.resetSequence();
// Override connection or edge fields
const bookConnection2 = await BookFactory.connection(2, {
pageInfo: dynamic(
() => PageInfoFactory.build({ hasPreviousPage: false, hasNextPage: true }),
),
});
expect(bookConnection2).toStrictEqual({
__typename: 'BookConnection',
edges: [
{ __typename: 'BookEdge', node: { __typename: 'Book', id: 'Book-1' }, cursor: 'BookEdge-1' },
{ __typename: 'BookEdge', node: { __typename: 'Book', id: 'Book-2' }, cursor: 'BookEdge-2' },
],
pageInfo: {
__typename: 'PageInfo',
hasPreviousPage: false,
hasNextPage: true,
startCursor: 'BookEdge-1',
endCursor: 'BookEdge-2',
},
});
BookFactory.resetSequence();
// Custom Connection Fields (e.g. `totalCount`)
const bookConnection3 = await BookFactory.connection(2, {
totalCount: 3, // Add
pageInfo: dynamic(
() => PageInfoFactory.build({ hasPreviousPage: false, hasNextPage: true }),
),
});
expect(bookConnection3).toStrictEqual({
__typename: 'BookConnection',
edges: [
{ __typename: 'BookEdge', node: { __typename: 'Book', id: 'Book-1' }, cursor: 'BookEdge-1' },
{ __typename: 'BookEdge', node: { __typename: 'Book', id: 'Book-2' }, cursor: 'BookEdge-2' },
],
pageInfo: {
__typename: 'PageInfo',
hasPreviousPage: false,
hasNextPage: true,
startCursor: 'BookEdge-1',
endCursor: 'BookEdge-2',
},
totalCount: 3, // Added
});
BookFactory.resetSequence();
hasPreviousPage
and totalCount
cannot be set.
defineXxxConnection
or defineXxxEdge
.defineAuthorFactoryInternal
import {
defineAuthorFactoryInternal,
dynamic,
FieldsResolver,
Traits,
AuthorFactoryDefineOptions,
AuthorFactoryInterface,
} from '../__generated__/fabbrica';
import { Author } from '../__generated__/types';
// Prepare custom `defineAuthorFactory` with transient fields
type AuthorTransientFields = {
bookCount: number;
};
function defineAuthorFactoryWithTransientFields<
_DefaultFieldsResolver extends FieldsResolver<Author & AuthorTransientFields>,
_Traits extends Traits<Author, AuthorTransientFields>,
>(
options: AuthorFactoryDefineOptions<AuthorTransientFields, _DefaultFieldsResolver, _Traits>,
): AuthorFactoryInterface<AuthorTransientFields, _DefaultFieldsResolver, _Traits> {
return defineAuthorFactoryInternal(options);
}
// Use custom `defineAuthorFactory`
const AuthorFactory = defineAuthorFactoryWithTransientFields({
defaultFields: {
id: dynamic(({ seq }) => `Author-${seq}`),
name: 'Komata Mikami',
books: dynamic(async ({ get }) => {
const bookCount = (await get('bookCount')) ?? 0;
return BookFactory.buildList(bookCount);
}),
bookCount: 0,
},
});
import {
dynamic,
FieldsResolver,
Traits,
AuthorFactoryDefineOptions,
AuthorFactoryInterface,
} from '../__generated__/fabbrica';
import { Author } from '../__generated__/types';
const AuthorFactory = defineAuthorFactory.withTransientFields({
bookCount: 0,
})({
defaultFields: {
id: dynamic(({ seq }) => `Author-${seq}`),
name: 'Komata Mikami',
books: dynamic(async ({ get }) => {
const bookCount = (await get('bookCount')) ?? 0;
return BookFactory.buildList(bookCount);
}),
},
});
defineXxxFactoryInternal
is not exported now. Use defineXxxFactory.withTransientFields
to define Transient Fields.Currently, Transient Fields are only allowed in defineTypeFactoryWithTransientFields
.
declare function defineAuthorFactoryWithTransientFields<
_TransientFieldsResolver extends TransientFieldsResolver<Author, Record<string, unknown>>,
TOptions extends AuthorFactoryDefineOptions<ResolvedFields<_TransientFieldsResolver>>,
>(
transientFields: _TransientFieldsResolver,
options: TOptions,
): AuthorFactoryInterface<ResolvedFields<_TransientFieldsResolver>, TOptions>;
const BookFactory = defineBookFactory({
defaultFields: {
id: lazy(({ seq }) => `Book-${seq}`),
title: lazy(({ seq }) => `ゆゆ式 ${seq}巻`),
author: undefined,
},
});
const AuthorFactory = defineAuthorFactoryWithTransientFields(
{
bookCount: 0,
},
{
defaultFields: {
id: lazy(({ seq }) => `Author-${seq}`),
name: '三上小又',
books: lazy(async ({ get }) => {
const bookCount = await get('bookCount');
// eslint-disable-next-line max-nested-callbacks
return Promise.all(Array.from({ length: bookCount }, async () => BookFactory.build()));
}),
},
},
);
This confuses users as they have to use an unusual API. In addition, it is awkward to have to add one more argument.
I want defineTypeFactory
to allow Transient Fields. The user specifies the type of Transient Fields using the type argument. Also, the default value of Transient Fields is specified by the defaultFields
option.
declare function defineAuthorFactory<
TransientFields extends Record<string, unknown>,
TOptions extends AuthorFactoryDefineOptions<TransientFields>,
>(
options: TOptions,
): AuthorFactoryInterface<TransientFields, TOptions>;
const BookFactory = defineBookFactory({
defaultFields: {
id: lazy(({ seq }) => `Book-${seq}`),
title: lazy(({ seq }) => `ゆゆ式 ${seq}巻`),
author: undefined,
},
});
type BookTransientFields = {
bookCount: number;
};
const AuthorFactory = defineAuthorFactory<BookTransientFields>({
defaultFields: {
id: lazy(({ seq }) => `Author-${seq}`),
name: '三上小又',
books: lazy(async ({ get }) => {
const bookCount = await get('bookCount');
// eslint-disable-next-line max-nested-callbacks
return Promise.all(Array.from({ length: bookCount }, async () => BookFactory.build()));
}),
bookCount: 0,
},
});
As you can see from the sample code, defineTypeFactory
currently has a type argument TOptions
. This type argument is inferred from the type of the options
argument. This allows the user to strictly type the return value of defineTypeFactory
without having to specify the type argument.
However, it causes problems when TransientFields
is added to the type arguments of defineTypeFactory
. The user must explicitly pass the type TransientFields
from the function caller, but then the type of the options
argument is not inferred.
TypeScript does not support partial inference of type arguments. Therefore, an implementation of this feature is currently not possible. We will probably have to wait for the following issue to be resolved.
typescript version: 5.4.2
@graphql-codegen/cli version: 5.0.2
@graphql-codegen/typescript version: 4.0.6
@mizdra/graphql-codegen-typescript-fabbrica version: 0.3.2
If you pass an interface or union to defaultFields that is missing some fields, tsc will throw a compile error.
type Query {
node(id: ID!): Node
search(query: String!): SearchResult
}
interface Node {
id: ID!
}
union SearchResult = Article | User
type Article implements Node {
id: ID!
title: String!
}
type User implements Node {
id: ID!
name: String!
}
defineQueryFactory({
defaultFields: {
__typename: 'Query',
// Missing fields such as `id`.
// Expected no error, but got an error.
node: {},
},
});
defineQueryFactory({
defaultFields: {
__typename: 'Query',
// Missing fields such as `id`.
// Expected no error, but got an error.
search: {},
},
});
No typescript compile errors. The field should be allowed to be missing.
Typescript compile error occurs.
The OptionalQuery
type was as follows:
export type OptionalQuery = {
__typename?: 'Query';
node?: Query['node'] | undefined;
search?: Query['search'] | undefined;
};
Perhaps this is wrong. The type should be as follows:
export type OptionalNode = OptionalArticle | OptionalUser;
export type OptionalSearchResult = OptionalArticle | OptionalUser;
export type OptionalQuery = {
__typename?: 'Query';
node?: OptionalNode | undefined;
search?: OptionalSearchResult | undefined;
};
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.