diff --git a/doc/src/markdown/plugins/typedDocumentNode.md b/doc/src/markdown/plugins/typedDocumentNode.md new file mode 100644 index 00000000..97517df4 --- /dev/null +++ b/doc/src/markdown/plugins/typedDocumentNode.md @@ -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
{data.drawCard.name}
; +}; +``` + +### 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 \ No newline at end of file diff --git a/examples/typescript-node-big-schema/src/zeus/typedDocumentNode.ts b/examples/typescript-node-big-schema/src/zeus/typedDocumentNode.ts new file mode 100644 index 00000000..0ab9ab34 --- /dev/null +++ b/examples/typescript-node-big-schema/src/zeus/typedDocumentNode.ts @@ -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 = { + ' __zeus_name': Name; + ' __zeus_type': T; +}; + +type QueryInputWithVariables = T extends string | number | Array + ? Variable | T + : Variable | { [K in keyof T]: QueryInputWithVariables } | T; + +type QueryWithVariables = T extends [infer Input, infer Output] + ? [QueryInputWithVariables, QueryWithVariables] + : { [K in keyof T]: QueryWithVariables }; + +type ExtractVariables = Query extends Variable + ? { [key in VName]: VType } + : Query extends [infer Inputs, infer Outputs] + ? ExtractVariables & ExtractVariables + : Query extends string | number | boolean + ? {} + : UnionToIntersection<{ [K in keyof Query]: ExtractVariables }[keyof Query]>; + +type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; + +export const $ = (name: Name) => { + return ('ZEUS_VAR$' + name) as any as Variable; +}; + +export function query>( + query: Z, +): TypedDocumentNode, ExtractVariables> { + return gql(Zeus('query', query as any)); +} + +export function mutation>( + mutation: Z, +): TypedDocumentNode, ExtractVariables> { + return gql(Zeus('mutation', mutation as any)); +} + +export function subscription>( + subscription: Z, +): TypedDocumentNode, ExtractVariables> { + return gql(Zeus('subscription', subscription as any)); +} diff --git a/examples/typescript-node/package.json b/examples/typescript-node/package.json index df7b11ce..06265eff 100644 --- a/examples/typescript-node/package.json +++ b/examples/typescript-node/package.json @@ -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" } } diff --git a/examples/typescript-node/src/zeus/typedDocumentNode.ts b/examples/typescript-node/src/zeus/typedDocumentNode.ts new file mode 100644 index 00000000..f520ca23 --- /dev/null +++ b/examples/typescript-node/src/zeus/typedDocumentNode.ts @@ -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 = { + ' __zeus_name': Name; + ' __zeus_type': T; +}; + +type QueryInputWithVariables = T extends string | number | Array + ? Variable | T + : Variable | { [K in keyof T]: QueryInputWithVariables } | T; + +type QueryWithVariables = T extends [infer Input, infer Output] + ? [QueryInputWithVariables, QueryWithVariables] + : { [K in keyof T]: QueryWithVariables }; + +type ExtractVariables = Query extends Variable + ? { [key in VName]: VType } + : Query extends [infer Inputs, infer Outputs] + ? ExtractVariables & ExtractVariables + : Query extends string | number | boolean + ? {} + : UnionToIntersection<{ [K in keyof Query]: ExtractVariables }[keyof Query]>; + +type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; + +export const $ = (name: Name) => { + return ('ZEUS_VAR$' + name) as any as Variable; +}; + +export function query>( + query: Z, +): TypedDocumentNode, ExtractVariables> { + return gql(Zeus('query', query as any)); +} + +export function mutation>( + mutation: Z, +): TypedDocumentNode, ExtractVariables> { + return gql(Zeus('mutation', mutation as any)); +} + +export function subscription>( + subscription: Z, +): TypedDocumentNode, ExtractVariables> { + return gql(Zeus('subscription', subscription as any)); +} diff --git a/src/CLI/CLIClass.ts b/src/CLI/CLIClass.ts index 65848a74..a89eb565 100644 --- a/src/CLI/CLIClass.ts +++ b/src/CLI/CLIClass.ts @@ -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 @@ -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); + } }; } diff --git a/src/CLI/index.ts b/src/CLI/index.ts index 541205e0..f77a9065 100644 --- a/src/CLI/index.ts +++ b/src/CLI/index.ts @@ -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: diff --git a/src/plugins/typed-document-node/index.spec.ts b/src/plugins/typed-document-node/index.spec.ts new file mode 100644 index 00000000..be8cd087 --- /dev/null +++ b/src/plugins/typed-document-node/index.spec.ts @@ -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>( + query: Z, +): TypedDocumentNode, ExtractVariables> { + return gql(Zeus("query", query as any)); +}`); + + expect(tdnResult.ts).toContain(` +export function mutation>( + mutation: Z, +): TypedDocumentNode, ExtractVariables> { + return gql(Zeus("mutation", mutation as any)); +} +`); + expect(tdnResult.ts).toContain(` +export function subscription>( + subscription: Z, +): TypedDocumentNode, ExtractVariables> { + return gql(Zeus("subscription", subscription as any)); +} +`); + }); +}); diff --git a/src/plugins/typed-document-node/index.ts b/src/plugins/typed-document-node/index.ts new file mode 100644 index 00000000..4ed604ca --- /dev/null +++ b/src/plugins/typed-document-node/index.ts @@ -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}>( + ${operation}: Z, +): TypedDocumentNode, ExtractVariables> { + 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 = { + ' __zeus_name': Name; + ' __zeus_type': T; +}; + +type QueryInputWithVariables = T extends string | number | Array + ? Variable | T + : Variable | { [K in keyof T]: QueryInputWithVariables } | T; + +type QueryWithVariables = T extends [infer Input, infer Output] + ? [QueryInputWithVariables, QueryWithVariables] + : { [K in keyof T]: QueryWithVariables }; + +type ExtractVariables = Query extends Variable + ? { [key in VName]: VType } + : Query extends [infer Inputs, infer Outputs] + ? ExtractVariables & ExtractVariables + : Query extends string | number | boolean + ? {} + : UnionToIntersection<{ [K in keyof Query]: ExtractVariables }[keyof Query]>; + +type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; + +export const $ = (name: Name) => { + return ('ZEUS_VAR$' + name) as any as Variable; +}; + +${o.ts} +`, + }; +};