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 useGraphqlPrint & removeClientSpecificFields options to persisted… #9926

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
137 changes: 81 additions & 56 deletions packages/presets/client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,29 +70,46 @@ export type ClientPresetConfig = {
onExecutableDocumentNode?: (documentNode: DocumentNode) => void | Record<string, unknown>;
/** Persisted operations configuration. */
persistedDocuments?:
| boolean
| {
/**
* @description Behavior for the output file.
* @default 'embedHashInDocument'
* "embedHashInDocument" will add a property within the `DocumentNode` with the hash of the operation.
* "replaceDocumentWithHash" will fully drop the document definition.
*/
mode?: 'embedHashInDocument' | 'replaceDocumentWithHash';
/**
* @description Name of the property that will be added to the `DocumentNode` with the hash of the operation.
*/
hashPropertyName?: string;
/**
* @description Algorithm used to generate the hash, could be useful if your server expects something specific (e.g., Apollo Server expects `sha256`).
*
* The algorithm parameter is typed with known algorithms and as a string rather than a union because it solely depends on Crypto's algorithms supported
* by the version of OpenSSL on the platform.
*
* @default `sha1`
*/
hashAlgorithm?: 'sha1' | 'sha256' | (string & {});
};
| boolean
| {
/**
* @description Behavior for the output file.
* @default 'embedHashInDocument'
* "embedHashInDocument" will add a property within the `DocumentNode` with the hash of the operation.
* "replaceDocumentWithHash" will fully drop the document definition.
*/
mode?: 'embedHashInDocument' | 'replaceDocumentWithHash';
/**
* @description Name of the property that will be added to the `DocumentNode` with the hash of the operation.
*/
hashPropertyName?: string;
/**
* @description Algorithm used to generate the hash, could be useful if your server expects something specific (e.g., Apollo Server expects `sha256`).
*
* The algorithm parameter is typed with known algorithms and as a string rather than a union because it solely depends on Crypto's algorithms supported
* by the version of OpenSSL on the platform.
*
* @default `sha1`
*/
hashAlgorithm?: 'sha1' | 'sha256' | (string & {});
/**
* @description Wether to use print from graphql to convert the `DocumentNode` to string or to use the default: @graphql-tools/documents printExecutableGraphQLDocument
*
* @graphql-tools/documents prints the documents in the same way but does a few things you might not want: sort/order alphabetically and remove whitespaces.
*
* @default `false`
*/
useGraphqlPrint?: boolean;
/**
* @description Remove client specific fields from the `DocumentNode`
*
* This removes all client specific directives/fields from the document that the server does not know about.
* Set the false if you want to keep these in the generated persisted-documents.
*
* @default `true`
*/
removeClientSpecificFields?: boolean;
};
};

const isOutputFolderLike = (baseOutputDir: string) => baseOutputDir.endsWith('/');
Expand Down Expand Up @@ -148,18 +165,26 @@ export const preset: Types.OutputPreset<ClientPresetConfig> = {

const persistedDocuments = options.presetConfig.persistedDocuments
? {
hashPropertyName:
(typeof options.presetConfig.persistedDocuments === 'object' &&
options.presetConfig.persistedDocuments.hashPropertyName) ||
'hash',
omitDefinitions:
(typeof options.presetConfig.persistedDocuments === 'object' &&
options.presetConfig.persistedDocuments.mode) === 'replaceDocumentWithHash' || false,
hashAlgorithm:
(typeof options.presetConfig.persistedDocuments === 'object' &&
options.presetConfig.persistedDocuments.hashAlgorithm) ||
'sha1',
}
hashPropertyName:
(typeof options.presetConfig.persistedDocuments === 'object' &&
options.presetConfig.persistedDocuments.hashPropertyName) ||
'hash',
omitDefinitions:
(typeof options.presetConfig.persistedDocuments === 'object' &&
options.presetConfig.persistedDocuments.mode) === 'replaceDocumentWithHash' || false,
hashAlgorithm:
(typeof options.presetConfig.persistedDocuments === 'object' &&
options.presetConfig.persistedDocuments.hashAlgorithm) ||
'sha1',
useGraphqlPrint:
(typeof options.presetConfig.persistedDocuments === 'object' &&
typeof options.presetConfig.persistedDocuments.useGraphqlPrint === 'boolean')
? options.presetConfig.persistedDocuments.useGraphqlPrint : false,
removeClientSpecificFields:
(typeof options.presetConfig.persistedDocuments === 'object' &&
typeof options.presetConfig.persistedDocuments.removeClientSpecificFields === 'boolean')
? options.presetConfig.persistedDocuments.removeClientSpecificFields : true,
}
: null;

const sourcesWithOperations = processSources(options.documents, node => {
Expand Down Expand Up @@ -195,7 +220,7 @@ export const preset: Types.OutputPreset<ClientPresetConfig> = {
const meta = onExecutableDocumentNodeHook?.(documentNode);

if (persistedDocuments) {
const documentString = normalizeAndPrintDocumentNode(documentNode);
const documentString = normalizeAndPrintDocumentNode(documentNode, persistedDocuments.useGraphqlPrint, persistedDocuments.removeClientSpecificFields);
const hash = generateDocumentHash(documentString, persistedDocuments.hashAlgorithm);
persistedDocumentsMap.set(hash, documentString);
return { ...meta, [persistedDocuments.hashPropertyName]: hash };
Expand Down Expand Up @@ -319,29 +344,29 @@ export const preset: Types.OutputPreset<ClientPresetConfig> = {
},
...(isPersistedOperations
? [
{
filename: `${options.baseOutputDir}persisted-documents.json`,
plugins: [
{
[`persisted-operations`]: {},
},
],
pluginMap: {
[`persisted-operations`]: {
plugin: async () => {
await tdnFinished.promise;
return {
content: JSON.stringify(Object.fromEntries(persistedDocumentsMap.entries()), null, 2),
};
},
{
filename: `${options.baseOutputDir}persisted-documents.json`,
plugins: [
{
[`persisted-operations`]: {},
},
],
pluginMap: {
[`persisted-operations`]: {
plugin: async () => {
await tdnFinished.promise;
return {
content: JSON.stringify(Object.fromEntries(persistedDocumentsMap.entries()), null, 2),
};
},
},
schema: options.schema,
config: {},
documents: sources,
documentTransforms: options.documentTransforms,
},
]
schema: options.schema,
config: {},
documents: sources,
documentTransforms: options.documentTransforms,
},
]
: []),
...(fragmentMaskingFileGenerateConfig ? [fragmentMaskingFileGenerateConfig] : []),
...(indexFileGenerateConfig ? [indexFileGenerateConfig] : []),
Expand Down
25 changes: 12 additions & 13 deletions packages/presets/client/src/persisted-documents.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as crypto from 'crypto';
import { printExecutableGraphQLDocument } from '@graphql-tools/documents';
import { type DocumentNode, Kind, visit } from 'graphql';
import { printExecutableGraphQLDocument, } from '@graphql-tools/documents';
import { type DocumentNode, Kind, visit, print } from 'graphql';

/**
* This function generates a hash from a document node.
Expand All @@ -14,25 +14,24 @@ export function generateDocumentHash(operation: string, algorithm: 'sha1' | 'sha
/**
* Normalizes and prints a document node.
*/
export function normalizeAndPrintDocumentNode(documentNode: DocumentNode): string {
/**
* This removes all client specific directives/fields from the document
* that the server does not know about.
* In a future version this should be more configurable.
* If you look at this and want to customize it.
* Send a PR :)
*/
export function normalizeAndPrintDocumentNode(documentNode: DocumentNode, useGraphqlPrint: boolean, removeClientSpecificFields: boolean): string {
const sanitizedDocument = visit(documentNode, {
[Kind.FIELD](field) {
if (field.directives?.some(directive => directive.name.value === 'client')) {
if (removeClientSpecificFields && field.directives?.some(directive => directive.name.value === 'client')) {
return null;
}
},
[Kind.DIRECTIVE](directive) {
if (directive.name.value === 'connection') {
if (removeClientSpecificFields && directive.name.value === 'connection') {
return null;
}
},
});
return printExecutableGraphQLDocument(sanitizedDocument);

if (useGraphqlPrint) {
return print(sanitizedDocument);
} else {
return printExecutableGraphQLDocument(sanitizedDocument);
}

}