Skip to content

Commit

Permalink
Add TypedDocumentNode string alternative (#9137)
Browse files Browse the repository at this point in the history
  • Loading branch information
beerose committed Mar 21, 2023
1 parent e567901 commit 2256c8b
Show file tree
Hide file tree
Showing 63 changed files with 1,362 additions and 821 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@graphql-codegen/client-preset": patch
---
dependencies updates:
- Updated dependency [`@graphql-typed-document-node/core@3.2.0` ↗︎](https://www.npmjs.com/package/@graphql-typed-document-node/core/v/3.2.0) (from `3.1.2`, in `dependencies`)
8 changes: 8 additions & 0 deletions .changeset/gold-dragons-poke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@graphql-codegen/typed-document-node': major
'@graphql-codegen/gql-tag-operations': major
'@graphql-codegen/client-preset': major
'@graphql-codegen/gql-tag-operations-preset': major
---

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.
42 changes: 20 additions & 22 deletions dev-test/gql-tag-operations/graphql/fragment-masking.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,46 @@
import { ResultOf, TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
import { ResultOf, DocumentTypeDecoration } from '@graphql-typed-document-node/core';

export type FragmentType<TDocumentType extends DocumentNode<any, any>> = TDocumentType extends DocumentNode<
infer TType,
any
>
? TType extends { ' $fragmentName'?: infer TKey }
? TKey extends string
? { ' $fragmentRefs'?: { [key in TKey]: TType } }
export type FragmentType<TDocumentType extends DocumentTypeDecoration<any, any>> =
TDocumentType extends DocumentTypeDecoration<infer TType, any>
? TType extends { ' $fragmentName'?: infer TKey }
? TKey extends string
? { ' $fragmentRefs'?: { [key in TKey]: TType } }
: never
: never
: never
: never;
: never;

// return non-nullable if `fragmentType` is non-nullable
export function useFragment<TType>(
_documentNode: DocumentNode<TType, any>,
fragmentType: FragmentType<DocumentNode<TType, any>>
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>>
): TType;
// return nullable if `fragmentType` is nullable
export function useFragment<TType>(
_documentNode: DocumentNode<TType, any>,
fragmentType: FragmentType<DocumentNode<TType, any>> | null | undefined
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | null | undefined
): TType | null | undefined;
// return array of non-nullable if `fragmentType` is array of non-nullable
export function useFragment<TType>(
_documentNode: DocumentNode<TType, any>,
fragmentType: ReadonlyArray<FragmentType<DocumentNode<TType, any>>>
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>>
): ReadonlyArray<TType>;
// return array of nullable if `fragmentType` is array of nullable
export function useFragment<TType>(
_documentNode: DocumentNode<TType, any>,
fragmentType: ReadonlyArray<FragmentType<DocumentNode<TType, any>>> | null | undefined
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined
): ReadonlyArray<TType> | null | undefined;
export function useFragment<TType>(
_documentNode: DocumentNode<TType, any>,
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType:
| FragmentType<DocumentNode<TType, any>>
| ReadonlyArray<FragmentType<DocumentNode<TType, any>>>
| FragmentType<DocumentTypeDecoration<TType, any>>
| ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>>
| null
| undefined
): TType | ReadonlyArray<TType> | null | undefined {
return fragmentType as any;
}

export function makeFragmentData<F extends DocumentNode, FT extends ResultOf<F>>(
export function makeFragmentData<F extends DocumentTypeDecoration<any, any>, FT extends ResultOf<F>>(
data: FT,
_fragment: F
): FragmentType<F> {
Expand Down
11 changes: 11 additions & 0 deletions examples/persisted-documents-string-mode/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Yoga Persisted Documents Example

Example for showing how to use GraphQL Code Generator for only allowing the execution of persisted operations.

[Learn more about Yoga Persisted Operations](https://the-guild.dev/graphql/yoga-server/docs/features/persisted-operations)

## Usage

Run `yarn codegen --watch` for starting GraphQL Code Generator in watch mode.

Run `yarn test` for running a tests located within `yoga.spec.ts`.
6 changes: 6 additions & 0 deletions examples/persisted-documents-string-mode/babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
presets: [
['@babel/preset-env', { targets: { node: process.versions.node.split('.')[0] } }],
'@babel/preset-typescript',
],
};
21 changes: 21 additions & 0 deletions examples/persisted-documents-string-mode/codegen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { type CodegenConfig } from '@graphql-codegen/cli';

const config: CodegenConfig = {
schema: './src/yoga.ts',
documents: ['src/**/*.ts'],
generates: {
'./src/gql/': {
preset: 'client-preset',
presetConfig: {
persistedDocuments: true,
},
config: {
documentMode: 'string',
},
},
},
hooks: { afterAllFileWrite: ['prettier --write'] },
};

export default config;
3 changes: 3 additions & 0 deletions examples/persisted-documents-string-mode/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
transform: { '^.+\\.ts': 'babel-jest' },
};
26 changes: 26 additions & 0 deletions examples/persisted-documents-string-mode/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "example-persisted-documents-string-mode",
"version": "0.0.0",
"private": true,
"dependencies": {
"graphql-yoga": "3.7.2",
"@graphql-yoga/plugin-persisted-operations": "1.7.2"
},
"devDependencies": {
"@graphql-typed-document-node/core": "3.2.0",
"jest": "28.1.3",
"babel-jest": "28.1.3",
"@graphql-codegen/cli": "3.2.2",
"@graphql-codegen/client-preset": "2.1.1",
"@babel/core": "7.21.0",
"@babel/preset-env": "7.20.2",
"@babel/preset-typescript": "7.21.0"
},
"scripts": {
"test": "jest",
"codegen": "graphql-codegen --config codegen.ts",
"build": "tsc",
"test:end2end": "yarn test"
},
"bob": false
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { ResultOf, DocumentTypeDecoration } from '@graphql-typed-document-node/core';

export type FragmentType<TDocumentType extends DocumentTypeDecoration<any, any>> =
TDocumentType extends DocumentTypeDecoration<infer TType, any>
? TType extends { ' $fragmentName'?: infer TKey }
? TKey extends string
? { ' $fragmentRefs'?: { [key in TKey]: TType } }
: never
: never
: never;

// return non-nullable if `fragmentType` is non-nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>>
): TType;
// return nullable if `fragmentType` is nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | null | undefined
): TType | null | undefined;
// return array of non-nullable if `fragmentType` is array of non-nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>>
): ReadonlyArray<TType>;
// return array of nullable if `fragmentType` is array of nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined
): ReadonlyArray<TType> | null | undefined;
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType:
| FragmentType<DocumentTypeDecoration<TType, any>>
| ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>>
| null
| undefined
): TType | ReadonlyArray<TType> | null | undefined {
return fragmentType as any;
}

export function makeFragmentData<F extends DocumentTypeDecoration<any, any>, FT extends ResultOf<F>>(
data: FT,
_fragment: F
): FragmentType<F> {
return data as FragmentType<F>;
}
27 changes: 27 additions & 0 deletions examples/persisted-documents-string-mode/src/gql/gql.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/* eslint-disable */
import * as types from './graphql';

/**
* Map of all GraphQL operations in the project.
*
* This map has several performance disadvantages:
* 1. It is not tree-shakeable, so it will include all operations in the project.
* 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle.
* 3. It does not support dead code elimination, so it will add unused operations.
*
* Therefore it is highly recommended to use the babel or swc plugin for production.
*/
const documents = {
'\n query HelloQuery {\n hello\n }\n': types.HelloQueryDocument,
};

/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(
source: '\n query HelloQuery {\n hello\n }\n'
): typeof import('./graphql').HelloQueryDocument;

export function graphql(source: string) {
return (documents as any)[source] ?? {};
}
57 changes: 57 additions & 0 deletions examples/persisted-documents-string-mode/src/gql/graphql.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/* eslint-disable */
import { DocumentTypeDecoration } from '@graphql-typed-document-node/core';
export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
ID: string;
String: string;
Boolean: boolean;
Int: number;
Float: number;
};

export type Mutation = {
__typename?: 'Mutation';
echo: Scalars['String'];
};

export type MutationEchoArgs = {
message: Scalars['String'];
};

export type Query = {
__typename?: 'Query';
hello: Scalars['String'];
};

export type HelloQueryQueryVariables = Exact<{ [key: string]: never }>;

export type HelloQueryQuery = { __typename?: 'Query'; hello: string };

export class TypedDocumentString<TResult, TVariables>
extends String
implements DocumentTypeDecoration<TResult, TVariables>
{
__apiType?: DocumentTypeDecoration<TResult, TVariables>['__apiType'];

constructor(private value: string, public __meta__?: { hash: string }) {
super(value);
}

toString(): string & DocumentTypeDecoration<TResult, TVariables> {
return this.value;
}
}

export const HelloQueryDocument = new TypedDocumentString(
`
query HelloQuery {
hello
}
`,
{ hash: '86f01e23de1c770cabbc35b2d87f2e5fd7557b6f' }
) as unknown as TypedDocumentString<HelloQueryQuery, HelloQueryQueryVariables>;
2 changes: 2 additions & 0 deletions examples/persisted-documents-string-mode/src/gql/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './fragment-masking';
export * from './gql';
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"86f01e23de1c770cabbc35b2d87f2e5fd7557b6f": "query HelloQuery { hello }"
}
15 changes: 15 additions & 0 deletions examples/persisted-documents-string-mode/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { createServer } from 'http';
import { makeYoga } from './yoga.js';

import persistedDocumentsDictionary from './gql/persisted-documents.json';

const persistedDocuments = new Map<string, string>(Object.entries(persistedDocumentsDictionary));

const yoga = makeYoga({ persistedDocuments });
const server = createServer(yoga);

// Start the server and you're done!
server.listen(4000, () => {
// eslint-disable-next-line no-console
console.info('Server is running on http://localhost:4000/graphql');
});
85 changes: 85 additions & 0 deletions examples/persisted-documents-string-mode/src/yoga.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { graphql } from './gql/index';
import { makeYoga } from './yoga';
import persistedDocumentsDictionary from './gql/persisted-documents.json';

const persistedDocuments = new Map<string, string>(Object.entries(persistedDocumentsDictionary));

const HelloQuery = graphql(/* GraphQL */ `
query HelloQuery {
hello
}
`);

describe('Persisted Documents', () => {
it('execute document without persisted operation enabled', async () => {
const yoga = makeYoga({ persistedDocuments: null });
const result = await yoga.fetch('http://yoga/graphql', {
method: 'POST',
headers: {
'content-type': 'application/json',
accept: 'application/json',
},
body: JSON.stringify({
query: HelloQuery,
}),
});
expect(await result.json()).toMatchInlineSnapshot(`
Object {
"data": Object {
"hello": "Hello world!",
},
}
`);
});

it('can not execute arbitrary operation with persisted operations enabled', async () => {
const yoga = makeYoga({ persistedDocuments });

const result = await yoga.fetch('http://yoga/graphql', {
method: 'POST',
headers: {
'content-type': 'application/json',
accept: 'application/json',
},
body: JSON.stringify({
query: HelloQuery,
}),
});
expect(await result.json()).toMatchInlineSnapshot(`
Object {
"errors": Array [
Object {
"message": "PersistedQueryOnly",
},
],
}
`);
});

it('can execute persisted operation with persisted operations enabled', async () => {
const yoga = makeYoga({ persistedDocuments });
const result = await yoga.fetch('http://yoga/graphql', {
method: 'POST',
headers: {
'content-type': 'application/json',
accept: 'application/json',
},
body: JSON.stringify({
extensions: {
persistedQuery: {
version: 1,
sha256Hash: (HelloQuery as any)['__meta__']['hash'],
},
},
}),
});

expect(await result.json()).toMatchInlineSnapshot(`
Object {
"data": Object {
"hello": "Hello world!",
},
}
`);
});
});

0 comments on commit 2256c8b

Please sign in to comment.