Skip to content

Commit

Permalink
feat: implement persistedDocuments.hashAlgorithm (#9353)
Browse files Browse the repository at this point in the history
* feat: implement the ability to specify the hash algorithm used for persisted documents

* Improve `hashAlgorithm` based on @ponbac's suggestion
  • Loading branch information
charpeni authored May 9, 2023
1 parent 25b249e commit d7e335b
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 3 deletions.
5 changes: 5 additions & 0 deletions .changeset/orange-tables-cry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphql-codegen/client-preset': minor
---

Implement the ability the specify the hash algorithm used for persisted documents via `persistedDocuments.hashAlgorithm`
15 changes: 14 additions & 1 deletion packages/presets/client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,15 @@ export type ClientPresetConfig = {
* @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 & {});
};
};

Expand Down Expand Up @@ -143,6 +152,10 @@ export const preset: Types.OutputPreset<ClientPresetConfig> = {
omitDefinitions:
(typeof options.presetConfig.persistedDocuments === 'object' &&
options.presetConfig.persistedDocuments.mode) === 'replaceDocumentWithHash' || false,
hashAlgorithm:
(typeof options.presetConfig.persistedDocuments === 'object' &&
options.presetConfig.persistedDocuments.hashAlgorithm) ||
'sha1',
}
: null;

Expand Down Expand Up @@ -180,7 +193,7 @@ export const preset: Types.OutputPreset<ClientPresetConfig> = {

if (persistedDocuments) {
const documentString = normalizeAndPrintDocumentNode(documentNode);
const hash = generateDocumentHash(documentString);
const hash = generateDocumentHash(documentString, persistedDocuments.hashAlgorithm);
persistedDocumentsMap.set(hash, documentString);
return { ...meta, [persistedDocuments.hashPropertyName]: hash };
}
Expand Down
4 changes: 2 additions & 2 deletions packages/presets/client/src/persisted-documents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { type DocumentNode, Kind, visit } from 'graphql';
/**
* This function generates a hash from a document node.
*/
export function generateDocumentHash(operation: string): string {
const shasum = crypto.createHash('sha1');
export function generateDocumentHash(operation: string, algorithm: 'sha1' | 'sha256' | (string & {})): string {
const shasum = crypto.createHash(algorithm);
shasum.update(operation);
return shasum.digest('hex');
}
Expand Down
79 changes: 79 additions & 0 deletions packages/presets/client/tests/client-preset.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1608,6 +1608,85 @@ export * from "./gql.js";`);
export const BbbDocument = {"__meta__":{"cacheKeys":["bbb"],"hash":"2a8e0849914b13ebc13b112ba5a502678d757511"},"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"bbb"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"b"}}]}}]} as unknown as DocumentNode<BbbQuery, BbbQueryVariables>;"
`);
});

it('hashAlgorithm="sha256"', async () => {
const result = await executeCodegen({
schema: [
/* GraphQL */ `
type Query {
a: String
b: String
c: String
}
`,
],
documents: path.join(__dirname, 'fixtures/simple-uppercase-operation-name.ts'),
generates: {
'out1/': {
preset,
presetConfig: {
persistedDocuments: {
hashAlgorithm: 'sha256',
},
},
},
},
emitLegacyCommonJSImports: false,
});

expect(result).toHaveLength(5);

const persistedDocuments = result.find(file => file.filename === 'out1/persisted-documents.json');

expect(persistedDocuments.content).toMatchInlineSnapshot(`
"{
"7d0eedabb966107835cf307a0ebaf93b5d2cb8c30228611ffe3d27a53c211a0c": "query A { a }",
"a62a11aa72041e38d8c12ef77e1e7c208d9605db60bb5abb1717e8af98e4b410": "query B { b }"
}"
`);

const graphqlFile = result.find(file => file.filename === 'out1/graphql.ts');
expect(graphqlFile.content).toMatchInlineSnapshot(`
"/* eslint-disable */
import { TypedDocumentNode as DocumentNode } 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 Query = {
__typename?: 'Query';
a?: Maybe<Scalars['String']>;
b?: Maybe<Scalars['String']>;
c?: Maybe<Scalars['String']>;
};
export type AQueryVariables = Exact<{ [key: string]: never; }>;
export type AQuery = { __typename?: 'Query', a?: string | null };
export type BQueryVariables = Exact<{ [key: string]: never; }>;
export type BQuery = { __typename?: 'Query', b?: string | null };
export type CFragment = { __typename?: 'Query', c?: string | null } & { ' $fragmentName'?: 'CFragment' };
export const CFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"C"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Query"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"c"}}]}}]} as unknown as DocumentNode<CFragment, unknown>;
export const ADocument = {"__meta__":{"hash":"7d0eedabb966107835cf307a0ebaf93b5d2cb8c30228611ffe3d27a53c211a0c"},"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"A"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"a"}}]}}]} as unknown as DocumentNode<AQuery, AQueryVariables>;
export const BDocument = {"__meta__":{"hash":"a62a11aa72041e38d8c12ef77e1e7c208d9605db60bb5abb1717e8af98e4b410"},"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"B"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"b"}}]}}]} as unknown as DocumentNode<BQuery, BQueryVariables>;"
`);
});
});

it('correctly handle fragment references', async () => {
Expand Down

0 comments on commit d7e335b

Please sign in to comment.