Skip to content
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

Add TypedDocumentNode string alternative #9137

Merged
merged 35 commits into from
Mar 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
4a7b339
Add unit tests
beerose Mar 8, 2023
504367a
Add TypedDocumentNode string alternative
beerose Mar 8, 2023
c7105db
Temporarily use snapshot version of @graphql-typed-document-node/core
beerose Mar 9, 2023
13e48a9
chore(dependencies): updated changesets for modified dependencies
github-actions[bot] Mar 9, 2023
02ed2b3
Update fragment-masking.ts files
beerose Mar 9, 2023
2d5a4e7
Update fragment-masking in dev-test dir
beerose Mar 9, 2023
590b2e2
Update @graphql-typed-document-node/core version in examples
beerose Mar 9, 2023
8e4e93f
Merge branch 'master' into typed-document-string
beerose Mar 9, 2023
7a5d42d
Update test after latest implememntation changes
beerose Mar 9, 2023
3892ba2
Changeset
beerose Mar 10, 2023
ef28588
Modify existing example
beerose Mar 10, 2023
961251f
Update typescript-graphql-request example
beerose Mar 13, 2023
dbd2053
Merge branch 'master' into typed-document-string
beerose Mar 13, 2023
8005ea3
Add failing test for duplicated fragments
beerose Mar 13, 2023
2c6ee68
Inline fragments in string document mode
beerose Mar 13, 2023
7b073f3
Update example with fragment inlining
beerose Mar 13, 2023
db98f71
Support __meta__ with typed document string
beerose Mar 13, 2023
346e1e5
Add TypedDocumentString class to handle metadata
beerose Mar 14, 2023
4e732da
Fix import
beerose Mar 14, 2023
82b75f0
Remove unused import
beerose Mar 14, 2023
56170f6
Merge branch 'master' into typed-document-string
beerose Mar 14, 2023
e3120b3
Update @graphql-typed-document-node/core version
beerose Mar 14, 2023
8b5ad73
Merge branch 'master' into typed-document-string
beerose Mar 15, 2023
f960ccf
chore(dependencies): updated changesets for modified dependencies
github-actions[bot] Mar 15, 2023
ed34330
Update react-query example — use fetch insetad of http executor
beerose Mar 15, 2023
0283daf
Merge branch 'master' into typed-document-string
beerose Mar 15, 2023
cfd9380
Add docs
beerose Mar 15, 2023
a97276b
Update @graphql-typed-document-node/code version
beerose Mar 20, 2023
2e99784
chore(dependencies): updated changesets for modified dependencies
github-actions[bot] Mar 20, 2023
bdc1e7d
Add more info to the dosc
beerose Mar 20, 2023
e954bea
Merge branch 'master' into typed-document-string
beerose Mar 20, 2023
0f155b4
Update docs
beerose Mar 21, 2023
5771e6d
Update changeset
beerose Mar 21, 2023
5ce6b4f
Update versions in changeset
beerose Mar 21, 2023
a86f4bc
Merge branch 'master' into typed-document-string
beerose Mar 21, 2023
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
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
Comment on lines -3 to +8
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this considered a breaking change for the codegen or not? @n1ru4l @saihaj

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so, people shouldn't use this type with anything else than our generated stuff.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed should be fine

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, as I think about it a bit more, this change kinda requires people to upgrade their typed document node version. 🤔

In graphql-yoga we treat peerDependency upgrades depending on the type of change.

e.g.
X.1.1 bump of a peer dependency would be a breaking change
1.X.1 bump of a peer dependency would be a minor change
1.1.X bump of a peer dependency would be a patch change

So I think we should treat this the same here (minor change).

Here the special case is that it is not listed as a peer dependency, but theoretically, it could be one, as people will need to install it, otherwise, their code won't compile with strict package manages like pnpm and yarn that do no hoisting. 🤔

Then, however, whether it should be a peer dependency or not also depends on the location where the code is generated.

So the safest option would be to ship this as a breaking change. I am sure people will open issues if we don't...

: 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!",
},
}
`);
});
});
Loading