Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
at main 7.0 kB view raw
1import { Kind, parse, print } from '@0no-co/graphql.web'; 2import type { DocumentNode, DefinitionNode } from './graphql'; 3import type { HashValue } from './hash'; 4import { phash } from './hash'; 5import { stringifyVariables } from './variables'; 6 7import type { 8 DocumentInput, 9 TypedDocumentNode, 10 AnyVariables, 11 GraphQLRequest, 12 RequestExtensions, 13} from '../types'; 14 15type PersistedDocumentNode = TypedDocumentNode & { 16 documentId?: string; 17}; 18 19/** A `DocumentNode` annotated with its hashed key. 20 * @internal 21 */ 22export type KeyedDocumentNode = TypedDocumentNode & { 23 __key: HashValue; 24}; 25 26const SOURCE_NAME = 'gql'; 27const GRAPHQL_STRING_RE = /("{3}[\s\S]*"{3}|"(?:\\.|[^"])*")/g; 28const REPLACE_CHAR_RE = /(?:#[^\n\r]+)?(?:[\r\n]+|$)/g; 29 30const replaceOutsideStrings = (str: string, idx: number): string => 31 idx % 2 === 0 ? str.replace(REPLACE_CHAR_RE, '\n') : str; 32 33/** Sanitizes a GraphQL document string by replacing comments and redundant newlines in it. */ 34const sanitizeDocument = (node: string): string => 35 node.split(GRAPHQL_STRING_RE).map(replaceOutsideStrings).join('').trim(); 36 37const prints: Map<DocumentNode | DefinitionNode, string> = new Map< 38 DocumentNode | DefinitionNode, 39 string 40>(); 41const docs: Map<HashValue, KeyedDocumentNode> = new Map< 42 HashValue, 43 KeyedDocumentNode 44>(); 45 46/** A cached printing function for GraphQL documents. 47 * 48 * @param node - A string of a document or a {@link DocumentNode} 49 * @returns A normalized printed string of the passed GraphQL document. 50 * 51 * @remarks 52 * This function accepts a GraphQL query string or {@link DocumentNode}, 53 * then prints and sanitizes it. The sanitizer takes care of removing 54 * comments, which otherwise alter the key of the document although the 55 * document is otherwise equivalent to another. 56 * 57 * When a {@link DocumentNode} is passed to this function, it caches its 58 * output by modifying the `loc.source.body` property on the GraphQL node. 59 */ 60export const stringifyDocument = ( 61 node: string | DefinitionNode | DocumentNode 62): string => { 63 let printed: string; 64 if (typeof node === 'string') { 65 printed = sanitizeDocument(node); 66 } else if (node.loc && docs.get((node as KeyedDocumentNode).__key) === node) { 67 printed = node.loc.source.body; 68 } else { 69 printed = prints.get(node) || sanitizeDocument(print(node)); 70 prints.set(node, printed); 71 } 72 73 if (typeof node !== 'string' && !node.loc) { 74 (node as any).loc = { 75 start: 0, 76 end: printed.length, 77 source: { 78 body: printed, 79 name: SOURCE_NAME, 80 locationOffset: { line: 1, column: 1 }, 81 }, 82 }; 83 } 84 85 return printed; 86}; 87 88/** Computes the hash for a document's string using {@link stringifyDocument}'s output. 89 * 90 * @param node - A string of a document or a {@link DocumentNode} 91 * @returns A {@link HashValue} 92 * 93 * @privateRemarks 94 * This function adds the operation name of the document to the hash, since sometimes 95 * a merged document with multiple operations may be used. Although `urql` requires a 96 * `DocumentNode` to only contain a single operation, when the cached `loc.source.body` 97 * of a `DocumentNode` is used, this string may still contain multiple operations and 98 * the resulting hash should account for only one at a time. 99 */ 100const hashDocument = ( 101 node: string | DefinitionNode | DocumentNode 102): HashValue => { 103 let key: HashValue; 104 if ((node as PersistedDocumentNode).documentId) { 105 key = phash((node as PersistedDocumentNode).documentId!); 106 } else { 107 key = phash(stringifyDocument(node)); 108 // Add the operation name to the produced hash 109 if ((node as DocumentNode).definitions) { 110 const operationName = getOperationName(node as DocumentNode); 111 if (operationName) key = phash(`\n# ${operationName}`, key); 112 } 113 } 114 return key; 115}; 116 117/** Returns a canonical version of the passed `DocumentNode` with an added hash key. 118 * 119 * @param node - A string of a document or a {@link DocumentNode} 120 * @returns A {@link KeyedDocumentNode} 121 * 122 * @remarks 123 * `urql` will always avoid unnecessary work, no matter whether a user passes `DocumentNode`s 124 * or strings of GraphQL documents to its APIs. 125 * 126 * This function will return a canonical version of a {@link KeyedDocumentNode} no matter 127 * which kind of input is passed, avoiding parsing or hashing of passed data as needed. 128 */ 129export const keyDocument = (node: string | DocumentNode): KeyedDocumentNode => { 130 let key: HashValue; 131 let query: DocumentNode; 132 if (typeof node === 'string') { 133 key = hashDocument(node); 134 query = docs.get(key) || parse(node, { noLocation: true }); 135 } else { 136 key = (node as KeyedDocumentNode).__key || hashDocument(node); 137 query = docs.get(key) || node; 138 } 139 140 // Add location information if it's missing 141 if (!query.loc) stringifyDocument(query); 142 143 (query as KeyedDocumentNode).__key = key; 144 docs.set(key, query as KeyedDocumentNode); 145 return query as KeyedDocumentNode; 146}; 147 148/** Creates a `GraphQLRequest` from the passed parameters. 149 * 150 * @param q - A string of a document or a {@link DocumentNode} 151 * @param variables - A variables object for the defined GraphQL operation. 152 * @returns A {@link GraphQLRequest} 153 * 154 * @remarks 155 * `createRequest` creates a {@link GraphQLRequest} from the passed parameters, 156 * while replacing the document as needed with a canonical version of itself, 157 * to avoid parsing, printing, or hashing the same input multiple times. 158 * 159 * If no variables are passed, canonically it'll default to an empty object, 160 * which is removed from the resulting hash key. 161 */ 162export const createRequest = < 163 Data = any, 164 Variables extends AnyVariables = AnyVariables, 165>( 166 _query: DocumentInput<Data, Variables>, 167 _variables: Variables, 168 extensions?: RequestExtensions | undefined 169): GraphQLRequest<Data, Variables> => { 170 const variables = _variables || ({} as Variables); 171 const query = keyDocument(_query); 172 const printedVars = stringifyVariables(variables, true); 173 let key = query.__key; 174 if (printedVars !== '{}') key = phash(printedVars, key); 175 return { key, query, variables, extensions }; 176}; 177 178/** Returns the name of the `DocumentNode`'s operation, if any. 179 * @param query - A {@link DocumentNode} 180 * @returns the operation's name contained within the document, or `undefined` 181 */ 182export const getOperationName = (query: DocumentNode): string | undefined => { 183 for (let i = 0, l = query.definitions.length; i < l; i++) { 184 const node = query.definitions[i]; 185 if (node.kind === Kind.OPERATION_DEFINITION) { 186 return node.name ? node.name.value : undefined; 187 } 188 } 189}; 190 191/** Returns the type of the `DocumentNode`'s operation, if any. 192 * @param query - A {@link DocumentNode} 193 * @returns the operation's type contained within the document, or `undefined` 194 */ 195export const getOperationType = (query: DocumentNode): string | undefined => { 196 for (let i = 0, l = query.definitions.length; i < l; i++) { 197 const node = query.definitions[i]; 198 if (node.kind === Kind.OPERATION_DEFINITION) { 199 return node.operation; 200 } 201 } 202};