Skip to content

Implement TypedDocumentNode support (with type-safe variables) #275

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
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
76 changes: 76 additions & 0 deletions doc/src/markdown/plugins/typedDocumentNode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
---
link: plugins/typedDocumentNode
title: TypedDocumentNode
order: 4
category: Plugins
---

## Usage with Typed Document Node

Zeus can generate builders for [`TypedDocumentNode`][typed-document-node], a type-safe query
representation understood by most GraphQL clients (including Apollo, urql etc) by adding the
`--typedDocumentNode` flag to the CLI.

### Generate Type-Safe Zeus Schema And TypedDocumentNode query builders

```sh
$ zeus schema.graphql ./ --typedDocumentNode
# typedDocumentNode.ts file with typed document node builders is now in the output destination
```

### TypedDocumentNode + Apollo Client useQuery examples

The following example demonstrates usage with Apollo. Other clients should work similarly.

```tsx
import { query, $ } from './zeus/typedDocumentNode';
import { useQuery } from '@apollo/client';

const myQuery = query({
// Get autocomplete here:
cardById: [
{ cardId: $('cardId') },
{
Attack: true,
Defense: true,
},
],
});

const Main = () => {
const { data } = useQuery(myQuery, {
variables: {
// Get autocomplete and typechecking here:
cardId: someId
}
});
// data response is typed
return <div>{data.drawCard.name}</div>;
};
```

### Variable support

Variables should be supported at any level of the query. Examples:

```typescript
const userMemberships = query({
user: [
{ id: $('id') },
{ memberships: [
{ limit: $('limit') },
{ role: true },
],
},
],
});

const mutate = mutation({
insertBooking: [
{ object: $('booking') },
{ id: true, bookerName: true },
],
});
```

[typed-document-node]: https://www.graphql-code-generator.com/plugins/typed-document-node
48 changes: 48 additions & 0 deletions examples/typescript-node-big-schema/src/zeus/typedDocumentNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
import gql from 'graphql-tag';
import { GraphQLTypes, InputType, ValueTypes, Zeus } from './index';

type Variable<T, Name extends string> = {
' __zeus_name': Name;
' __zeus_type': T;
};

type QueryInputWithVariables<T> = T extends string | number | Array<any>
? Variable<T, any> | T
: Variable<T, any> | { [K in keyof T]: QueryInputWithVariables<T[K]> } | T;

type QueryWithVariables<T> = T extends [infer Input, infer Output]
? [QueryInputWithVariables<Input>, QueryWithVariables<Output>]
: { [K in keyof T]: QueryWithVariables<T[K]> };

type ExtractVariables<Query> = Query extends Variable<infer VType, infer VName>
? { [key in VName]: VType }
: Query extends [infer Inputs, infer Outputs]
? ExtractVariables<Inputs> & ExtractVariables<Outputs>
: Query extends string | number | boolean
? {}
: UnionToIntersection<{ [K in keyof Query]: ExtractVariables<Query[K]> }[keyof Query]>;

type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;

export const $ = <Type, Name extends string>(name: Name) => {
return ('ZEUS_VAR$' + name) as any as Variable<Type, Name>;
};

export function query<Z extends QueryWithVariables<ValueTypes['query_root']>>(
query: Z,
): TypedDocumentNode<InputType<GraphQLTypes['query_root'], Z>, ExtractVariables<Z>> {
return gql(Zeus('query', query as any));
}

export function mutation<Z extends QueryWithVariables<ValueTypes['mutation_root']>>(
mutation: Z,
): TypedDocumentNode<InputType<GraphQLTypes['mutation_root'], Z>, ExtractVariables<Z>> {
return gql(Zeus('mutation', mutation as any));
}

export function subscription<Z extends QueryWithVariables<ValueTypes['subscription_root']>>(
subscription: Z,
): TypedDocumentNode<InputType<GraphQLTypes['subscription_root'], Z>, ExtractVariables<Z>> {
return gql(Zeus('subscription', subscription as any));
}
2 changes: 2 additions & 0 deletions examples/typescript-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
"@apollo/client": "^3.4.16",
"node-fetch": "^2.6.0",
"react-query": "^3.27.0",
"@graphql-typed-document-node/core":"^3.1.1",
"graphql-tag":"^2.12.6",
"ws": "^8.5.0"
}
}
50 changes: 50 additions & 0 deletions examples/typescript-node/src/zeus/typedDocumentNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/* eslint-disable */

import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
import gql from 'graphql-tag';
import { GraphQLTypes, InputType, ValueTypes, Zeus } from './index';

type Variable<T, Name extends string> = {
' __zeus_name': Name;
' __zeus_type': T;
};

type QueryInputWithVariables<T> = T extends string | number | Array<any>
? Variable<T, any> | T
: Variable<T, any> | { [K in keyof T]: QueryInputWithVariables<T[K]> } | T;

type QueryWithVariables<T> = T extends [infer Input, infer Output]
? [QueryInputWithVariables<Input>, QueryWithVariables<Output>]
: { [K in keyof T]: QueryWithVariables<T[K]> };

type ExtractVariables<Query> = Query extends Variable<infer VType, infer VName>
? { [key in VName]: VType }
: Query extends [infer Inputs, infer Outputs]
? ExtractVariables<Inputs> & ExtractVariables<Outputs>
: Query extends string | number | boolean
? {}
: UnionToIntersection<{ [K in keyof Query]: ExtractVariables<Query[K]> }[keyof Query]>;

type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;

export const $ = <Type, Name extends string>(name: Name) => {
return ('ZEUS_VAR$' + name) as any as Variable<Type, Name>;
};

export function query<Z extends QueryWithVariables<ValueTypes['Query']>>(
query: Z,
): TypedDocumentNode<InputType<GraphQLTypes['Query'], Z>, ExtractVariables<Z>> {
return gql(Zeus('query', query as any));
}

export function mutation<Z extends QueryWithVariables<ValueTypes['Mutation']>>(
mutation: Z,
): TypedDocumentNode<InputType<GraphQLTypes['Mutation'], Z>, ExtractVariables<Z>> {
return gql(Zeus('mutation', mutation as any));
}

export function subscription<Z extends QueryWithVariables<ValueTypes['Subscription']>>(
subscription: Z,
): TypedDocumentNode<InputType<GraphQLTypes['Subscription'], Z>, ExtractVariables<Z>> {
return gql(Zeus('subscription', subscription as any));
}
4 changes: 4 additions & 0 deletions src/CLI/CLIClass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Parser } from 'graphql-js-tree';
import { pluginApollo } from '@/plugins/apollo';
import { pluginReactQuery } from '@/plugins/react-query';
import { pluginStucco } from '@/plugins/stuccoSubscriptions';
import { pluginTypedDocumentNode } from '@/plugins/typed-document-node';

/**
* basic yargs interface
Expand Down Expand Up @@ -96,6 +97,9 @@ export class CLI {
if (args.stuccoSubscriptions) {
writeFileRecursive(path.join(pathToFile, 'zeus'), `stuccoSubscriptions.ts`, pluginStucco({ tree }).ts);
}
if (args.typedDocumentNode) {
writeFileRecursive(path.join(pathToFile, 'zeus'), `typedDocumentNode.ts`, pluginTypedDocumentNode({ tree }).ts);
}
};
}

Expand Down
5 changes: 5 additions & 0 deletions src/CLI/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ zeus [path] [output_path] [options]
describe: 'Generate React Query useTypedQuery module',
boolean: true,
})
.option('typedDocumentNode', {
alias: 'td',
describe: 'Generate TypedDocumentNode createQuery module',
boolean: true,
})
.option('header', {
alias: 'h',
describe:
Expand Down
53 changes: 53 additions & 0 deletions src/plugins/typed-document-node/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Parser } from 'graphql-js-tree';
import { pluginTypedDocumentNode } from '.';

describe('plugin typed document node test', () => {
it('generates correct typed document node plugin from the schema', () => {
const schema = `
type Query{
people: [String!]!
}
type Mutation{
register(name: String!): String!
}
type Subscription{
registrations: [String!]!
}
schema{
query: Query
mutation: Mutation
subscription: Subscription
}
`;
const tree = Parser.parse(schema);
const tdnResult = pluginTypedDocumentNode({ tree });

expect(tdnResult.ts).toContain(`
import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
import gql from 'graphql-tag';
import { GraphQLTypes, InputType, ValueTypes, Zeus } from './index';
`);

expect(tdnResult.ts).toContain(`
export function query<Z extends QueryWithVariables<ValueTypes["Query"]>>(
query: Z,
): TypedDocumentNode<InputType<GraphQLTypes["Query"], Z>, ExtractVariables<Z>> {
return gql(Zeus("query", query as any));
}`);

expect(tdnResult.ts).toContain(`
export function mutation<Z extends QueryWithVariables<ValueTypes["Mutation"]>>(
mutation: Z,
): TypedDocumentNode<InputType<GraphQLTypes["Mutation"], Z>, ExtractVariables<Z>> {
return gql(Zeus("mutation", mutation as any));
}
`);
expect(tdnResult.ts).toContain(`
export function subscription<Z extends QueryWithVariables<ValueTypes["Subscription"]>>(
subscription: Z,
): TypedDocumentNode<InputType<GraphQLTypes["Subscription"], Z>, ExtractVariables<Z>> {
return gql(Zeus("subscription", subscription as any));
}
`);
});
});
69 changes: 69 additions & 0 deletions src/plugins/typed-document-node/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { OperationType, ParserTree } from 'graphql-js-tree';

const createOperationFunction = ({ queryName, operation }: { queryName: string; operation: OperationType }) => {
// const firstLetter = operation[0];

return {
queryName,
operation,
ts: `export function ${operation}<Z extends QueryWithVariables<ValueTypes["${queryName}"]>>(
${operation}: Z,
): TypedDocumentNode<InputType<GraphQLTypes["${queryName}"], Z>, ExtractVariables<Z>> {
return gql(Zeus("${operation}", ${operation} as any));
}
`,
};
};

export const pluginTypedDocumentNode = ({ tree, esModule }: { tree: ParserTree; esModule?: boolean }) => {
const operationNodes = tree.nodes.filter((n) => n.type.operations);
const opsFunctions = operationNodes.flatMap((n) =>
n.type.operations!.map((o) => createOperationFunction({ queryName: n.name, operation: o })),
);

const o = opsFunctions.reduce(
(a, b) => {
a.ts = [a.ts, b.ts].join('\n');
return a;
},
{ ts: '' },
);

return {
ts: `/* eslint-disable */

import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
import gql from 'graphql-tag';
import { GraphQLTypes, InputType, ValueTypes, Zeus } from './index';

type Variable<T, Name extends string> = {
' __zeus_name': Name;
' __zeus_type': T;
};

type QueryInputWithVariables<T> = T extends string | number | Array<any>
? Variable<T, any> | T
: Variable<T, any> | { [K in keyof T]: QueryInputWithVariables<T[K]> } | T;

type QueryWithVariables<T> = T extends [infer Input, infer Output]
? [QueryInputWithVariables<Input>, QueryWithVariables<Output>]
: { [K in keyof T]: QueryWithVariables<T[K]> };

type ExtractVariables<Query> = Query extends Variable<infer VType, infer VName>
? { [key in VName]: VType }
: Query extends [infer Inputs, infer Outputs]
? ExtractVariables<Inputs> & ExtractVariables<Outputs>
: Query extends string | number | boolean
? {}
: UnionToIntersection<{ [K in keyof Query]: ExtractVariables<Query[K]> }[keyof Query]>;

type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;

export const $ = <Type, Name extends string>(name: Name) => {
return ('ZEUS_VAR$' + name) as any as Variable<Type, Name>;
};

${o.ts}
`,
};
};