Skip to content

Commit 2256c8b

Browse files
authored
Add TypedDocumentNode string alternative (#9137)
1 parent e567901 commit 2256c8b

File tree

63 files changed

+1362
-821
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

63 files changed

+1362
-821
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@graphql-codegen/client-preset": patch
3+
---
4+
dependencies updates:
5+
- Updated dependency [`@graphql-typed-document-node/[email protected]` ↗︎](https://www.npmjs.com/package/@graphql-typed-document-node/core/v/3.2.0) (from `3.1.2`, in `dependencies`)

.changeset/gold-dragons-poke.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@graphql-codegen/typed-document-node': major
3+
'@graphql-codegen/gql-tag-operations': major
4+
'@graphql-codegen/client-preset': major
5+
'@graphql-codegen/gql-tag-operations-preset': major
6+
---
7+
8+
Add `TypedDocumentNode` string alternative that doesn't require GraphQL AST on the client. This change requires `@graphql-typed-document-node/core` in version `3.2.0` or higher.

dev-test/gql-tag-operations/graphql/fragment-masking.ts

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,46 @@
1-
import { ResultOf, TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
1+
import { ResultOf, DocumentTypeDecoration } from '@graphql-typed-document-node/core';
22

3-
export type FragmentType<TDocumentType extends DocumentNode<any, any>> = TDocumentType extends DocumentNode<
4-
infer TType,
5-
any
6-
>
7-
? TType extends { ' $fragmentName'?: infer TKey }
8-
? TKey extends string
9-
? { ' $fragmentRefs'?: { [key in TKey]: TType } }
3+
export type FragmentType<TDocumentType extends DocumentTypeDecoration<any, any>> =
4+
TDocumentType extends DocumentTypeDecoration<infer TType, any>
5+
? TType extends { ' $fragmentName'?: infer TKey }
6+
? TKey extends string
7+
? { ' $fragmentRefs'?: { [key in TKey]: TType } }
8+
: never
109
: never
11-
: never
12-
: never;
10+
: never;
1311

1412
// return non-nullable if `fragmentType` is non-nullable
1513
export function useFragment<TType>(
16-
_documentNode: DocumentNode<TType, any>,
17-
fragmentType: FragmentType<DocumentNode<TType, any>>
14+
_documentNode: DocumentTypeDecoration<TType, any>,
15+
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>>
1816
): TType;
1917
// return nullable if `fragmentType` is nullable
2018
export function useFragment<TType>(
21-
_documentNode: DocumentNode<TType, any>,
22-
fragmentType: FragmentType<DocumentNode<TType, any>> | null | undefined
19+
_documentNode: DocumentTypeDecoration<TType, any>,
20+
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | null | undefined
2321
): TType | null | undefined;
2422
// return array of non-nullable if `fragmentType` is array of non-nullable
2523
export function useFragment<TType>(
26-
_documentNode: DocumentNode<TType, any>,
27-
fragmentType: ReadonlyArray<FragmentType<DocumentNode<TType, any>>>
24+
_documentNode: DocumentTypeDecoration<TType, any>,
25+
fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>>
2826
): ReadonlyArray<TType>;
2927
// return array of nullable if `fragmentType` is array of nullable
3028
export function useFragment<TType>(
31-
_documentNode: DocumentNode<TType, any>,
32-
fragmentType: ReadonlyArray<FragmentType<DocumentNode<TType, any>>> | null | undefined
29+
_documentNode: DocumentTypeDecoration<TType, any>,
30+
fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined
3331
): ReadonlyArray<TType> | null | undefined;
3432
export function useFragment<TType>(
35-
_documentNode: DocumentNode<TType, any>,
33+
_documentNode: DocumentTypeDecoration<TType, any>,
3634
fragmentType:
37-
| FragmentType<DocumentNode<TType, any>>
38-
| ReadonlyArray<FragmentType<DocumentNode<TType, any>>>
35+
| FragmentType<DocumentTypeDecoration<TType, any>>
36+
| ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>>
3937
| null
4038
| undefined
4139
): TType | ReadonlyArray<TType> | null | undefined {
4240
return fragmentType as any;
4341
}
4442

45-
export function makeFragmentData<F extends DocumentNode, FT extends ResultOf<F>>(
43+
export function makeFragmentData<F extends DocumentTypeDecoration<any, any>, FT extends ResultOf<F>>(
4644
data: FT,
4745
_fragment: F
4846
): FragmentType<F> {
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Yoga Persisted Documents Example
2+
3+
Example for showing how to use GraphQL Code Generator for only allowing the execution of persisted operations.
4+
5+
[Learn more about Yoga Persisted Operations](https://the-guild.dev/graphql/yoga-server/docs/features/persisted-operations)
6+
7+
## Usage
8+
9+
Run `yarn codegen --watch` for starting GraphQL Code Generator in watch mode.
10+
11+
Run `yarn test` for running a tests located within `yoga.spec.ts`.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module.exports = {
2+
presets: [
3+
['@babel/preset-env', { targets: { node: process.versions.node.split('.')[0] } }],
4+
'@babel/preset-typescript',
5+
],
6+
};
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// eslint-disable-next-line import/no-extraneous-dependencies
2+
import { type CodegenConfig } from '@graphql-codegen/cli';
3+
4+
const config: CodegenConfig = {
5+
schema: './src/yoga.ts',
6+
documents: ['src/**/*.ts'],
7+
generates: {
8+
'./src/gql/': {
9+
preset: 'client-preset',
10+
presetConfig: {
11+
persistedDocuments: true,
12+
},
13+
config: {
14+
documentMode: 'string',
15+
},
16+
},
17+
},
18+
hooks: { afterAllFileWrite: ['prettier --write'] },
19+
};
20+
21+
export default config;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports = {
2+
transform: { '^.+\\.ts': 'babel-jest' },
3+
};
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "example-persisted-documents-string-mode",
3+
"version": "0.0.0",
4+
"private": true,
5+
"dependencies": {
6+
"graphql-yoga": "3.7.2",
7+
"@graphql-yoga/plugin-persisted-operations": "1.7.2"
8+
},
9+
"devDependencies": {
10+
"@graphql-typed-document-node/core": "3.2.0",
11+
"jest": "28.1.3",
12+
"babel-jest": "28.1.3",
13+
"@graphql-codegen/cli": "3.2.2",
14+
"@graphql-codegen/client-preset": "2.1.1",
15+
"@babel/core": "7.21.0",
16+
"@babel/preset-env": "7.20.2",
17+
"@babel/preset-typescript": "7.21.0"
18+
},
19+
"scripts": {
20+
"test": "jest",
21+
"codegen": "graphql-codegen --config codegen.ts",
22+
"build": "tsc",
23+
"test:end2end": "yarn test"
24+
},
25+
"bob": false
26+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { ResultOf, DocumentTypeDecoration } from '@graphql-typed-document-node/core';
2+
3+
export type FragmentType<TDocumentType extends DocumentTypeDecoration<any, any>> =
4+
TDocumentType extends DocumentTypeDecoration<infer TType, any>
5+
? TType extends { ' $fragmentName'?: infer TKey }
6+
? TKey extends string
7+
? { ' $fragmentRefs'?: { [key in TKey]: TType } }
8+
: never
9+
: never
10+
: never;
11+
12+
// return non-nullable if `fragmentType` is non-nullable
13+
export function useFragment<TType>(
14+
_documentNode: DocumentTypeDecoration<TType, any>,
15+
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>>
16+
): TType;
17+
// return nullable if `fragmentType` is nullable
18+
export function useFragment<TType>(
19+
_documentNode: DocumentTypeDecoration<TType, any>,
20+
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | null | undefined
21+
): TType | null | undefined;
22+
// return array of non-nullable if `fragmentType` is array of non-nullable
23+
export function useFragment<TType>(
24+
_documentNode: DocumentTypeDecoration<TType, any>,
25+
fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>>
26+
): ReadonlyArray<TType>;
27+
// return array of nullable if `fragmentType` is array of nullable
28+
export function useFragment<TType>(
29+
_documentNode: DocumentTypeDecoration<TType, any>,
30+
fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined
31+
): ReadonlyArray<TType> | null | undefined;
32+
export function useFragment<TType>(
33+
_documentNode: DocumentTypeDecoration<TType, any>,
34+
fragmentType:
35+
| FragmentType<DocumentTypeDecoration<TType, any>>
36+
| ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>>
37+
| null
38+
| undefined
39+
): TType | ReadonlyArray<TType> | null | undefined {
40+
return fragmentType as any;
41+
}
42+
43+
export function makeFragmentData<F extends DocumentTypeDecoration<any, any>, FT extends ResultOf<F>>(
44+
data: FT,
45+
_fragment: F
46+
): FragmentType<F> {
47+
return data as FragmentType<F>;
48+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/* eslint-disable */
2+
import * as types from './graphql';
3+
4+
/**
5+
* Map of all GraphQL operations in the project.
6+
*
7+
* This map has several performance disadvantages:
8+
* 1. It is not tree-shakeable, so it will include all operations in the project.
9+
* 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle.
10+
* 3. It does not support dead code elimination, so it will add unused operations.
11+
*
12+
* Therefore it is highly recommended to use the babel or swc plugin for production.
13+
*/
14+
const documents = {
15+
'\n query HelloQuery {\n hello\n }\n': types.HelloQueryDocument,
16+
};
17+
18+
/**
19+
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
20+
*/
21+
export function graphql(
22+
source: '\n query HelloQuery {\n hello\n }\n'
23+
): typeof import('./graphql').HelloQueryDocument;
24+
25+
export function graphql(source: string) {
26+
return (documents as any)[source] ?? {};
27+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/* eslint-disable */
2+
import { DocumentTypeDecoration } from '@graphql-typed-document-node/core';
3+
export type Maybe<T> = T | null;
4+
export type InputMaybe<T> = Maybe<T>;
5+
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
6+
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
7+
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
8+
/** All built-in and custom scalars, mapped to their actual values */
9+
export type Scalars = {
10+
ID: string;
11+
String: string;
12+
Boolean: boolean;
13+
Int: number;
14+
Float: number;
15+
};
16+
17+
export type Mutation = {
18+
__typename?: 'Mutation';
19+
echo: Scalars['String'];
20+
};
21+
22+
export type MutationEchoArgs = {
23+
message: Scalars['String'];
24+
};
25+
26+
export type Query = {
27+
__typename?: 'Query';
28+
hello: Scalars['String'];
29+
};
30+
31+
export type HelloQueryQueryVariables = Exact<{ [key: string]: never }>;
32+
33+
export type HelloQueryQuery = { __typename?: 'Query'; hello: string };
34+
35+
export class TypedDocumentString<TResult, TVariables>
36+
extends String
37+
implements DocumentTypeDecoration<TResult, TVariables>
38+
{
39+
__apiType?: DocumentTypeDecoration<TResult, TVariables>['__apiType'];
40+
41+
constructor(private value: string, public __meta__?: { hash: string }) {
42+
super(value);
43+
}
44+
45+
toString(): string & DocumentTypeDecoration<TResult, TVariables> {
46+
return this.value;
47+
}
48+
}
49+
50+
export const HelloQueryDocument = new TypedDocumentString(
51+
`
52+
query HelloQuery {
53+
hello
54+
}
55+
`,
56+
{ hash: '86f01e23de1c770cabbc35b2d87f2e5fd7557b6f' }
57+
) as unknown as TypedDocumentString<HelloQueryQuery, HelloQueryQueryVariables>;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './fragment-masking';
2+
export * from './gql';
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"86f01e23de1c770cabbc35b2d87f2e5fd7557b6f": "query HelloQuery { hello }"
3+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { createServer } from 'http';
2+
import { makeYoga } from './yoga.js';
3+
4+
import persistedDocumentsDictionary from './gql/persisted-documents.json';
5+
6+
const persistedDocuments = new Map<string, string>(Object.entries(persistedDocumentsDictionary));
7+
8+
const yoga = makeYoga({ persistedDocuments });
9+
const server = createServer(yoga);
10+
11+
// Start the server and you're done!
12+
server.listen(4000, () => {
13+
// eslint-disable-next-line no-console
14+
console.info('Server is running on http://localhost:4000/graphql');
15+
});
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { graphql } from './gql/index';
2+
import { makeYoga } from './yoga';
3+
import persistedDocumentsDictionary from './gql/persisted-documents.json';
4+
5+
const persistedDocuments = new Map<string, string>(Object.entries(persistedDocumentsDictionary));
6+
7+
const HelloQuery = graphql(/* GraphQL */ `
8+
query HelloQuery {
9+
hello
10+
}
11+
`);
12+
13+
describe('Persisted Documents', () => {
14+
it('execute document without persisted operation enabled', async () => {
15+
const yoga = makeYoga({ persistedDocuments: null });
16+
const result = await yoga.fetch('http://yoga/graphql', {
17+
method: 'POST',
18+
headers: {
19+
'content-type': 'application/json',
20+
accept: 'application/json',
21+
},
22+
body: JSON.stringify({
23+
query: HelloQuery,
24+
}),
25+
});
26+
expect(await result.json()).toMatchInlineSnapshot(`
27+
Object {
28+
"data": Object {
29+
"hello": "Hello world!",
30+
},
31+
}
32+
`);
33+
});
34+
35+
it('can not execute arbitrary operation with persisted operations enabled', async () => {
36+
const yoga = makeYoga({ persistedDocuments });
37+
38+
const result = await yoga.fetch('http://yoga/graphql', {
39+
method: 'POST',
40+
headers: {
41+
'content-type': 'application/json',
42+
accept: 'application/json',
43+
},
44+
body: JSON.stringify({
45+
query: HelloQuery,
46+
}),
47+
});
48+
expect(await result.json()).toMatchInlineSnapshot(`
49+
Object {
50+
"errors": Array [
51+
Object {
52+
"message": "PersistedQueryOnly",
53+
},
54+
],
55+
}
56+
`);
57+
});
58+
59+
it('can execute persisted operation with persisted operations enabled', async () => {
60+
const yoga = makeYoga({ persistedDocuments });
61+
const result = await yoga.fetch('http://yoga/graphql', {
62+
method: 'POST',
63+
headers: {
64+
'content-type': 'application/json',
65+
accept: 'application/json',
66+
},
67+
body: JSON.stringify({
68+
extensions: {
69+
persistedQuery: {
70+
version: 1,
71+
sha256Hash: (HelloQuery as any)['__meta__']['hash'],
72+
},
73+
},
74+
}),
75+
});
76+
77+
expect(await result.json()).toMatchInlineSnapshot(`
78+
Object {
79+
"data": Object {
80+
"hello": "Hello world!",
81+
},
82+
}
83+
`);
84+
});
85+
});

0 commit comments

Comments
 (0)