diff --git a/README.md b/README.md index 43550ff..077a92c 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ npm install --save persistgraphql The build tool binary is called `persistgraphql`. Running it with no other arguments should give: ``` -Usage: persistgraphql input_file [output file] [--add_typename] +Usage: persistgraphql input_file [output file] [--add_typename] [--hash_ids] ``` It can be called on a file containing GraphQL query definitions with extension `.graphql`: @@ -49,6 +49,14 @@ persistgraphql index.ts output.json It can also take the `--add_typename` flag which will apply a query transformation to the query documents, adding the `__typename` field at every level of the query. You must pass this option if your client code uses this query transformation. +``` +persistgraphql src/ --hash_ids +``` + +## Using a hash of the query as an ID to allow scaling for multiple clients + +Use the optional `hash_ids` flag to substitute a `sha512` hash of the query as the map value rather than the default which is an incremental integer. This will avoid ID collisions for multiple clients using the same server. + ``` persistgraphql src/ --add_typename ``` diff --git a/package.json b/package.json index ddb6c6d..c0d029c 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "apollo-client": "^1.1", "graphql": ">=0.9.4 <0.11", "graphql-tag": "^2.0.0", + "hasha": "^3.0.0", "lodash": "^4.17.4", "whatwg-fetch": "^2.0.3", "yargs": "^7.1.0" diff --git a/src/ExtractGQL.ts b/src/ExtractGQL.ts index 6eabaf9..5024fde 100644 --- a/src/ExtractGQL.ts +++ b/src/ExtractGQL.ts @@ -2,6 +2,7 @@ import fs = require('fs'); import path = require('path'); +import hasha = require('hasha'); import { parse, @@ -42,6 +43,9 @@ import { import _ = require('lodash'); export type ExtractGQLOptions = { + extension?: string, + hashIds?: boolean, + inJsCode?: boolean, inputFilePath: string, outputFilePath?: string, queryTransformers?: QueryTransformer[], @@ -63,6 +67,9 @@ export class ExtractGQL { // The file extension to load queries from public extension: string; + // Whether to use a hash of the query as an ID in the query map + public hashIds: boolean = false; + // Whether to look for standalone .graphql files or template literals in JavaScript code public inJsCode: boolean = false; @@ -106,17 +113,19 @@ export class ExtractGQL { } constructor({ + extension = 'graphql', + hashIds = false, + inJsCode = false, inputFilePath, outputFilePath = 'extracted_queries.json', queryTransformers = [], - extension = 'graphql', - inJsCode = false, }: ExtractGQLOptions) { + this.extension = extension; + this.hashIds = hashIds; + this.inJsCode = inJsCode; this.inputFilePath = inputFilePath; this.outputFilePath = outputFilePath; this.queryTransformers = queryTransformers; - this.extension = extension; - this.inJsCode = inJsCode; } // Add a query transformer to the end of the list of query transformers. @@ -153,7 +162,7 @@ export class ExtractGQL { const transformedQueryWithFragments = this.getQueryFragments(transformedDocument, transformedDefinition); transformedQueryWithFragments.definitions.unshift(transformedDefinition); const docQueryKey = this.getQueryDocumentKey(transformedQueryWithFragments); - result[docQueryKey] = this.getQueryId(); + result[docQueryKey] = this.hashIds ? hasha(docQueryKey) : this.getQueryId(); }); return result; } @@ -332,7 +341,8 @@ export interface YArgsv { export const main = (argv: YArgsv) => { // These are the unhypenated arguments that yargs does not process // further. - const args: string[] = argv._; + const args: string[] = argv._ + let hashIds: boolean = false; let inputFilePath: string; let outputFilePath: string; const queryTransformers: QueryTransformer[] = []; @@ -353,7 +363,18 @@ export const main = (argv: YArgsv) => { queryTransformers.push(addTypenameTransformer); } + // Check if we are passed "--hash_ids", if we are, we have to + // use a hash of the query as an ID instead of integers. + // The hash_ids flag will use a sha512 hash of the query as the map value + // rather than the default which is an incremental integer + // This will avoid ID collisions for multiple clients using the same server. + if (argv['hash_ids']) { + console.log('Using hash of query as ID.'); + hashIds = true; + } + const options: ExtractGQLOptions = { + hashIds, inputFilePath, outputFilePath, queryTransformers, diff --git a/test/network_interface/ApolloNetworkInterface.ts b/test/network_interface/ApolloNetworkInterface.ts index d35dff5..42cdb87 100644 --- a/test/network_interface/ApolloNetworkInterface.ts +++ b/test/network_interface/ApolloNetworkInterface.ts @@ -1,3 +1,4 @@ +import hasha = require('hasha'); import * as chai from 'chai'; const { assert } = chai; @@ -90,6 +91,34 @@ describe('PersistedQueryNetworkInterface', () => { }); }); + describe('Using --hash_ids', () => { + const egql = new ExtractGQL({ hashIds: true, inputFilePath: 'nothing' }); + const queriesDocument = gql` + query getAuthor { + author { + firstName + lastName + } + } + query getHouse { + house { + address + } + } + `; + const queryMap = egql.createMapFromDocument(queriesDocument); + const keys = Object.keys(queryMap); + + it('should use a hash of the query as the value if --hash_ids is true', () => { + assert.equal(queryMap[keys[0]], hasha(keys[0])); + assert.equal(queryMap[keys[1]], hasha(keys[1])); + }); + + it('should not use an integer as the value if --hash_ids is true', () => { + assert.notEqual(queryMap[keys[0]], 1); + }); + }); + describe('sending query ids', () => { const egql = new ExtractGQL({ inputFilePath: 'nothing' }); const queriesDocument = gql` diff --git a/typings.d.ts b/typings.d.ts index 2a4d25b..960b2ca 100644 --- a/typings.d.ts +++ b/typings.d.ts @@ -18,3 +18,8 @@ declare module 'deep-assign' { function deepAssign(...objects: any[]): any; export = deepAssign; } + +declare module 'hasha' { + function hasha(...objects: any[]): any; + export = hasha; +}