Skip to content

Commit

Permalink
feat: add support for metadata and persisted operations in client pre…
Browse files Browse the repository at this point in the history
…set (#8757)

* feat: add support for persisted operations

* always do cleanup

* crypto ftw

* sanitize document

* stable print and config options

* feat: only include executable documents in persisted operations map

* feat: stable print

* fix shit

* feat: new __meta__ approach

* test: embed metadata in document node

* refactor: use Map and remove hacks

* docs: update react-query instructions

* add changesets

* feat: update to stable package version

* chore(dependencies): updated changesets for modified dependencies

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
  • Loading branch information
n1ru4l and github-actions[bot] committed Jan 18, 2023
1 parent 5bc753d commit 4f290aa
Show file tree
Hide file tree
Showing 10 changed files with 955 additions and 134 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@graphql-codegen/client-preset": patch
---
dependencies updates:
- Added dependency [`@graphql-tools/documents@^0.1.0` ↗︎](https://www.npmjs.com/package/@graphql-tools/documents/v/0.1.0) (to `dependencies`)
52 changes: 52 additions & 0 deletions .changeset/lemon-zebras-hunt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
'@graphql-codegen/client-preset': minor
---

Add support for persisted documents.

You can now generate and embed a persisted documents hash for the executable documents.

```ts
/** codegen.ts */
import { CodegenConfig } from '@graphql-codegen/cli'

const config: CodegenConfig = {
schema: 'https://swapi-graphql.netlify.app/.netlify/functions/index',
documents: ['src/**/*.tsx'],
ignoreNoDocuments: true, // for better experience with the watcher
generates: {
'./src/gql/': {
preset: 'client',
plugins: [],
presetConfig: {
persistedOperations: true,
}
}
}
}

export default config
```

This will generate `./src/gql/persisted-documents.json` (dictionary of hashes with their operation string).

In addition to that each generated document node will have a `__meta__.hash` property.

```ts
import { gql } from './gql.js'

const allFilmsWithVariablesQueryDocument = graphql(/* GraphQL */ `
query allFilmsWithVariablesQuery($first: Int!) {
allFilms(first: $first) {
edges {
node {
...FilmItem
}
}
}
}
`)

console.log((allFilmsWithVariablesQueryDocument as any)["__meta__"]["hash"])
```

54 changes: 54 additions & 0 deletions .changeset/tasty-adults-doubt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
'@graphql-codegen/client-preset': minor
---

Add support for embedding metadata in the document AST.

It is now possible to embed metadata (e.g. for your GraphQL client within the emitted code).

```ts
/** codegen.ts */
import { CodegenConfig } from '@graphql-codegen/cli'

const config: CodegenConfig = {
schema: 'https://swapi-graphql.netlify.app/.netlify/functions/index',
documents: ['src/**/*.tsx'],
ignoreNoDocuments: true, // for better experience with the watcher
generates: {
'./src/gql/': {
preset: 'client',
plugins: [],
presetConfig: {
onExecutableDocumentNode(documentNode) {
return {
operation: documentNode.definitions[0].operation,
name: documentNode.definitions[0].name.value
}
}
}
}
}
}

export default config
```

You can then access the metadata via the `__meta__` property on the document node.

```ts
import { gql } from './gql.js'

const allFilmsWithVariablesQueryDocument = graphql(/* GraphQL */ `
query allFilmsWithVariablesQuery($first: Int!) {
allFilms(first: $first) {
edges {
node {
...FilmItem
}
}
}
}
`)

console.log((allFilmsWithVariablesQueryDocument as any)["__meta__"])
```
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import autoBind from 'auto-bind';
import { pascalCase } from 'change-case-all';
import { DepGraph } from 'dependency-graph';
import {
DefinitionNode,
DocumentNode,
FragmentDefinitionNode,
FragmentSpreadNode,
GraphQLSchema,
Expand Down Expand Up @@ -228,8 +230,14 @@ export interface ClientSideBasePluginConfig extends ParsedConfig {
pureMagicComment?: boolean;
optimizeDocumentNode: boolean;
experimentalFragmentVariables?: boolean;
unstable_onExecutableDocumentNode?: Unstable_OnExecutableDocumentNode;
unstable_omitDefinitions?: boolean;
}

type ExecutableDocumentNodeMeta = Record<string, unknown>;

type Unstable_OnExecutableDocumentNode = (documentNode: DocumentNode) => void | ExecutableDocumentNodeMeta;

export class ClientSideBaseVisitor<
TRawConfig extends RawClientSideBasePluginConfig = RawClientSideBasePluginConfig,
TPluginConfig extends ClientSideBasePluginConfig = ClientSideBasePluginConfig
Expand All @@ -239,9 +247,13 @@ export class ClientSideBaseVisitor<
protected _additionalImports: string[] = [];
protected _imports = new Set<string>();

private _onExecutableDocumentNode?: Unstable_OnExecutableDocumentNode;
private _omitDefinitions?: boolean;
private _fragments: Map<string, LoadedFragment>;

constructor(
protected _schema: GraphQLSchema,
protected _fragments: LoadedFragment[],
fragments: LoadedFragment[],
rawConfig: TRawConfig,
additionalConfig: Partial<TPluginConfig>,
documents?: Types.DocumentFile[]
Expand Down Expand Up @@ -271,9 +283,10 @@ export class ClientSideBaseVisitor<
experimentalFragmentVariables: getConfigValue(rawConfig.experimentalFragmentVariables, false),
...additionalConfig,
} as any);

this._documents = documents;

this._onExecutableDocumentNode = (rawConfig as any).unstable_onExecutableDocumentNode;
this._omitDefinitions = (rawConfig as any).unstable_omitDefinitions;
this._fragments = new Map(fragments.map(fragment => [fragment.name, fragment]));
autoBind(this);
}

Expand All @@ -293,7 +306,7 @@ export class ClientSideBaseVisitor<
names.add(node.name.value);

if (withNested) {
const foundFragment = this._fragments.find(f => f.name === node.name.value);
const foundFragment = this._fragments.get(node.name.value);

if (foundFragment) {
const childItems = this._extractFragments(foundFragment.node, true);
Expand All @@ -312,20 +325,14 @@ export class ClientSideBaseVisitor<
return Array.from(names);
}

protected _transformFragments(document: FragmentDefinitionNode | OperationDefinitionNode): string[] {
const includeNestedFragments =
this.config.documentMode === DocumentMode.documentNode ||
(this.config.dedupeFragments && document.kind === 'OperationDefinition');

return this._extractFragments(document, includeNestedFragments).map(document =>
this.getFragmentVariableName(document)
);
protected _transformFragments(fragmentNames: Array<string>): string[] {
return fragmentNames.map(document => this.getFragmentVariableName(document));
}

protected _includeFragments(fragments: string[], nodeKind: 'FragmentDefinition' | 'OperationDefinition'): string {
if (fragments && fragments.length > 0) {
if (this.config.documentMode === DocumentMode.documentNode) {
return this._fragments
return Array.from(this._fragments.values())
.filter(f => fragments.includes(this.getFragmentVariableName(f.name)))
.map(fragment => print(fragment.node))
.join('\n');
Expand All @@ -346,8 +353,35 @@ export class ClientSideBaseVisitor<
return documentStr;
}

private _generateDocumentNodeMeta(
definitions: ReadonlyArray<DefinitionNode>,
fragmentNames: Array<string>
): ExecutableDocumentNodeMeta | void {
// If the document does not contain any executable operation, we don't need to hash it
if (definitions.every(def => def.kind !== Kind.OPERATION_DEFINITION)) {
return undefined;
}

const allDefinitions = [...definitions];

for (const fragment of fragmentNames) {
const fragmentRecord = this._fragments.get(fragment);
if (fragmentRecord) {
allDefinitions.push(fragmentRecord.node);
}
}

const documentNode: DocumentNode = { kind: Kind.DOCUMENT, definitions: allDefinitions };

return this._onExecutableDocumentNode(documentNode);
}

protected _gql(node: FragmentDefinitionNode | OperationDefinitionNode): string {
const fragments = this._transformFragments(node);
const includeNestedFragments =
this.config.documentMode === DocumentMode.documentNode ||
(this.config.dedupeFragments && node.kind === 'OperationDefinition');
const fragmentNames = this._extractFragments(node, includeNestedFragments);
const fragments = this._transformFragments(fragmentNames);

const doc = this._prepareDocument(`
${print(node).split('\\').join('\\\\') /* Re-escape escaped values in GraphQL syntax */}
Expand Down Expand Up @@ -375,11 +409,39 @@ export class ClientSideBaseVisitor<
...fragments.map(name => `...${name}.definitions`),
].join();

return `{"kind":"${Kind.DOCUMENT}","definitions":[${definitions}]}`;
let hashPropertyStr = '';

if (this._onExecutableDocumentNode) {
const meta = this._generateDocumentNodeMeta(gqlObj.definitions, fragmentNames);
if (meta) {
hashPropertyStr = `"__meta__": ${JSON.stringify(meta)}, `;
if (this._omitDefinitions === true) {
return `{${hashPropertyStr}}`;
}
}
}

return `{${hashPropertyStr}"kind":"${Kind.DOCUMENT}", "definitions":[${definitions}]}`;
}

let meta: ExecutableDocumentNodeMeta | void;

if (this._onExecutableDocumentNode) {
meta = this._generateDocumentNodeMeta(gqlObj.definitions, fragmentNames);
const metaNodePartial = { ['__meta__']: meta };

if (this._omitDefinitions === true) {
return JSON.stringify(metaNodePartial);
}

if (meta) {
return JSON.stringify({ ...metaNodePartial, ...gqlObj });
}
}

return JSON.stringify(gqlObj);
}

if (this.config.documentMode === DocumentMode.string) {
return '`' + doc + '`';
}
Expand Down Expand Up @@ -411,7 +473,7 @@ export class ClientSideBaseVisitor<
private get fragmentsGraph(): DepGraph<LoadedFragment> {
const graph = new DepGraph<LoadedFragment>({ circular: true });

for (const fragment of this._fragments) {
for (const fragment of this._fragments.values()) {
if (graph.hasNode(fragment.name)) {
const cachedAsString = print(graph.getNodeData(fragment.name).node);
const asString = print(fragment.node);
Expand All @@ -438,7 +500,7 @@ export class ClientSideBaseVisitor<
}

public get fragments(): string {
if (this._fragments.length === 0 || this.config.documentMode === DocumentMode.external) {
if (this._fragments.size === 0 || this.config.documentMode === DocumentMode.external) {
return '';
}

Expand Down
1 change: 1 addition & 0 deletions packages/presets/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"@graphql-codegen/plugin-helpers": "^3.1.2",
"@graphql-codegen/visitor-plugin-common": "^2.13.7",
"@graphql-typed-document-node/core": "3.1.1",
"@graphql-tools/documents": "^0.1.0",
"@graphql-tools/utils": "^9.0.0",
"tslib": "~2.4.0"
},
Expand Down

0 comments on commit 4f290aa

Please sign in to comment.