Giter Club home page Giter Club logo

graphql-codegen-typescript-fabbrica's People

Contributors

mizdra avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

Forkers

azu

graphql-codegen-typescript-fabbrica's Issues

No type error is reported when an unknown trait name is passed to use

This problem occurs with factories where no traits are defined.

https://stackblitz.com/edit/playground-graphql-codegen-typescript-fabbrica-1ryp1f?file=src%2Findex.test.ts&view=editor

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

Passing an unknown field does not report type errors

Environment

@mizdra/graphql-codegen-typescript-fabbrica version: 0.3.2
typescript version (optional):
@graphql-codegen/cli version (optional):
@graphql-codegen/typescript version (optional):

Summary

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',
  },
});

Step to reproduce

https://stackblitz.com/edit/playground-graphql-codegen-typescript-fabbrica-4yjhkc?file=src%2Findex.test.ts&view=editor

What did you expect to happen?

A type error is reported.

What actually happened?

No type errors are reported.

Participation

  • I am willing to submit a pull request for this issue.

Additional comments

It may be necessary to assign a dedicated type parameter for each field.

https://www.typescriptlang.org/play?#code/C4TwDgpgBA8mwEsD2A7AhgGwEJKQaygF4oBvAKCikWAwgH4AuKAZ2ACcEUBzKAHygCuKACYQAZpwjCA3BSgJhjFu049+Q0RJRTZAX1llRAYwxo20MUKOJUUAEYCEGYTnwBGADwAVKBAAewBAizLDwyOjYuHgAfAAUZlzMTF4AlMmyRqisUObMAhjAbkT2js6ueG6x5AD01ZSUAHp0ctS0TADkaO0ANHK1UAACwMwAtP6Q1mNsbEhsckJ4KEgA7igAYggQzkxuvbopsrG5+YUsaIjMEhAhJFQINBBMrBzcUPsGxqbmUJYo1uElJwuKIAJm8vgCQWEITgNgi5QA2u1WhB2gBdYoacSSYTdKAAVQhgWCoThmER7QU6MxImx2mEcQSSVIdweSi8eIUSkJ+yYtxRyU5wiYPIyWWAOWuJxBxQcQPKIKqZH69SaLXubSgnR6fTqQ1G4wgkwg01m8xQixW6022yguzI7zIRylBRlzHOCEumxurM1z1UbwOZEMRq+FiscMBZSiAGZwf5idDSeFyVFGWxEnzfY8oF4kSj0ULkkiqRjebmxShsscCjHZaVgfgY0qVY1mpQBVqur1KP19WM-BNgFMZnNKAslqsNlthXa9kHnXla2cLlcfZ3-a9HUA

Change dynamic field interfaces

Motivation

  • The get function does not return a usable type.
    • The type of get('author') is Promise<OptionalAuthor | undefined>.
    • OptionalAuthor means { id?: string | undefined, name?: string | undefined, email?: string | undefined }.
    • All fields of author may be undefined. This is inconvenient for the user.
    • Therefore, get function is not very useful.
  • Dependent Field is not simple to define.
    • What it means...
      • Call get(...): get('name')
      • Add await keyword before get(...): await get('name')
      • Prepare default value: (await get('name')) ?? 'defaultName'
      • Compute the field: `${(await get('name')) ?? 'defaultName'}@yuyushiki.net`
      • Wrap with dynamic function: dynamic(async ({ get }) => `${(await get('name')) ?? 'defaultName'}@yuyushiki.net`)
    • Many steps are required. It is not simple.
    • I would like to define it more simply.

Proposal

Change the interface as follows:

Case. 1: Use seq in defaultFields and .build() function

Before

const BookFactory = defineBookFactory({
  defaultFields: {
    id: dynamic(({ seq }) => `Book-${seq}`),
    name: 'Book',
  },
});
const book = await BookFactory.build({
  name: dynamic(({ seq }) => `Book-${seq}`),
});

After

const BookFactory = defineBookFactory({
  defaultFields: (({ seq }) => ({
    id: `Book-${seq}`,
    name: 'Book',
  })),
});
const book = await BookFactory.build(({ seq }) => ({
  name: `Book-${seq}`,
}));

Case. 2: Use get for Dependent Fields

Before

const AuthorFactory = defineAuthorFactory({
  defaultFields: {
    name: 'mikamikomata',
    email: dynamic(async ({ get }) => `${(await get('name')) ?? 'defaultName'}@yuyushiki.net`),
  },
});

After

const AuthorFactory = defineAuthorFactory({
  defaultFields: (({ seq }) => {
    const name = 'mikamikomata';
    return {
      name,
      email: `${name}@yuyushiki.net`,
    };
  }),
});

Case. 3: Use get for Transient Fields (depend: #73)

Before

const AuthorFactory = defineAuthorFactory.withTransientFields({
  bookCount: 0,
})({
  defaultFields: {
    books: dynamic(async ({ get }) => {
      const bookCount = (await get('bookCount')) ?? 0;
      return BookFactory.buildList(bookCount);
    }),
  },
});

After

const AuthorFactory = defineAuthorFactory.withTransientFields({
  bookCount: 0,
})({
  defaultFields: async ({ transientFields: { bookCount } }) => ({
    books: await BookFactory.buildList(bookCount),
  }),
});

Breaking Changes

  • Remove get function
  • Remove dynamic utility

Implement `get(name: string): Field` and `getOrDefault(name: string): Field` instead of `get(name: string): Field | undefined`

An 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

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.

Support additional fields

blocked by: #73

Problem

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

Solution

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

Drawbacks

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.

Support `.buildConnection(...)`

Add a utility that allows users to build connections without defining XxxEdgeFactory or XxxConnectionFactory.

Example

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();

Limitation

  • Default values for hasPreviousPage and totalCount cannot be set.
    • If you want to set default values, use defineXxxConnection or defineXxxEdge.

Change the interface of Transient Fields

  • Unblock #59
  • No need to export internal APIs such as defineAuthorFactoryInternal
  • Easy to define transient fields
  • The interface is designed with reference to Quramy/prisma-fabbrica#252

Before

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,
  },
});

After

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);
    }),
  },
});

Breaking Changes

  • defineXxxFactoryInternal is not exported now. Use defineXxxFactory.withTransientFields to define Transient Fields.

Allow Transient Fields in `defineTypeFactory`

Problem

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.

Solution

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,
  },
});

Additional context

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.

Interface and union fields are not optional

Environment

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

Summary

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: {},
  },
});

Step to reproduce

https://stackblitz.com/edit/playground-graphql-codegen-typescript-fabbrica-fdjpge?file=src%2Findex.test.ts

What did you expect to happen?

No typescript compile errors. The field should be allowed to be missing.

What actually happened?

Typescript compile error occurs.

Link to Minimal Reproducible Example

https://stackblitz.com/edit/playground-graphql-codegen-typescript-fabbrica-fdjpge?file=src%2Findex.test.ts

Participation

  • I am willing to submit a pull request for this issue.

Additional comments

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;
};

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.