@replit/graphql-codegen-persisted-queries
Version:
GraphQL Codegen plugin to generate persisted query manifests for server and client
1 lines • 15.4 kB
Source Map (JSON)
{"version":3,"sources":["../src/utils/hashing.ts","../src/utils/transforms.ts","../src/utils/fragments.ts","../src/core/generator.ts","../src/core/plugin.ts"],"names":["createHash","content","config","algorithm","digest","TYPENAME_FIELD","Kind","printDefinitions","definitions","print","addTypenameToDocument","doc","visit","node","_key","parent","selections","hasTypename","selection","isIntrospection","findFragments","docs","fragments","findUsedFragments","operation","knownFragments","_usedFragments","usedFragments","fragmentName","fragment","processOperations","processedDocs","operations","def","operationName","query","hash","generateClientManifest","manifest","generateServerManifest","operationDetails","plugin","_schema","documents","documentNodes"],"mappings":"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","file":"index.mjs","sourcesContent":["import * as crypto from 'node:crypto';\nimport { PluginConfig } from '../types';\n\n/**\n * Creates a hash for a given string using the specified algorithm\n *\n * @param content - The content string to hash\n * @param config - Plugin configuration\n * @returns A hex string hash of the content by default, or a prefixed hash (e.g. \"sha256:abc123...\")\n * when includeAlgorithmPrefix is true for GraphQL over HTTP specification compliance\n */\nexport function createHash(content: string, config: PluginConfig): string {\n const algorithm = config.algorithm || 'sha256';\n\n const digest = crypto\n .createHash(algorithm)\n .update(content, 'utf8')\n .digest()\n .toString('hex');\n\n // Only prefix with algorithm if includeAlgorithmPrefix is true\n return config.includeAlgorithmPrefix === true\n ? `${algorithm}:${digest}`\n : digest;\n}\n","import {\n DocumentNode,\n FieldNode,\n Kind,\n OperationDefinitionNode,\n print,\n visit,\n} from 'graphql';\nimport { Definition } from '../types';\n\n/**\n * Typename field definition to add to selection sets\n */\nconst TYPENAME_FIELD: FieldNode = {\n kind: Kind.FIELD,\n name: {\n kind: Kind.NAME,\n value: '__typename',\n },\n};\n\n/**\n * Prints an array of definitions as a single string\n *\n * @param definitions - Array of GraphQL definitions to print\n * @returns Printed string representation\n */\nexport function printDefinitions(\n definitions: (Definition | DocumentNode)[],\n): string {\n return definitions.map(print).join('\\n\\n');\n}\n\n/**\n * Adds __typename to all selection sets in a document\n * Adapted from Apollo Client\n *\n * @param doc - GraphQL document node\n * @returns Document with __typename fields added\n */\nexport function addTypenameToDocument(doc: DocumentNode): DocumentNode {\n return visit(doc, {\n SelectionSet: {\n enter(node, _key, parent) {\n // Don't add __typename to OperationDefinitions\n if (\n parent &&\n (parent as OperationDefinitionNode).kind === 'OperationDefinition'\n ) {\n return;\n }\n\n // No changes if no selections\n const { selections } = node;\n if (!selections) {\n return;\n }\n\n // Skip if selection already has __typename\n const hasTypename = selections.some((selection) => {\n return (\n selection.kind === 'Field' &&\n (selection as FieldNode).name.value === '__typename'\n );\n });\n\n // Skip if this is an introspection query\n const isIntrospection = selections.some((selection) => {\n return (\n selection.kind === 'Field' &&\n (selection as FieldNode).name.value.startsWith('__')\n );\n });\n\n if (hasTypename || isIntrospection) {\n return;\n }\n\n // Create and return a new SelectionSet with a __typename Field\n return {\n ...node,\n selections: [...selections, TYPENAME_FIELD],\n };\n },\n },\n });\n}\n","import {\n DocumentNode,\n FragmentDefinitionNode,\n OperationDefinitionNode,\n visit,\n} from 'graphql';\n\n/**\n * Extracts all fragment definitions from an array of documents\n *\n * @param docs - Array of GraphQL document nodes to scan\n * @returns A map of fragment names to their definitions\n */\nexport function findFragments(\n docs: (DocumentNode | FragmentDefinitionNode)[],\n): Map<string, FragmentDefinitionNode> {\n const fragments = new Map<string, FragmentDefinitionNode>();\n\n for (const doc of docs) {\n visit(doc, {\n FragmentDefinition: {\n enter(node) {\n fragments.set(node.name.value, node);\n },\n },\n });\n }\n\n return fragments;\n}\n\n/**\n * Finds all fragments used in an operation or fragment, including nested fragments\n *\n * @param operation - The operation or fragment to analyze\n * @param knownFragments - Map of all available fragments\n * @param _usedFragments - Optional accumulator for recursive calls\n * @returns Map of all fragments used by the operation\n * @throws Error if a referenced fragment cannot be found\n */\nexport function findUsedFragments(\n operation: OperationDefinitionNode | FragmentDefinitionNode,\n knownFragments: ReadonlyMap<string, FragmentDefinitionNode>,\n _usedFragments?: Map<string, FragmentDefinitionNode>,\n): Map<string, FragmentDefinitionNode> {\n const usedFragments = _usedFragments\n ? _usedFragments\n : new Map<string, FragmentDefinitionNode>();\n\n visit(operation, {\n FragmentSpread: {\n enter(node) {\n const fragmentName = node.name.value;\n const fragment = knownFragments.get(fragmentName);\n\n if (fragment) {\n // Skip if we've already processed this fragment to avoid circular references\n if (usedFragments.has(fragmentName)) {\n return;\n }\n\n usedFragments.set(fragmentName, fragment);\n // Recursively find nested fragments\n findUsedFragments(fragment, knownFragments, usedFragments);\n } else {\n throw new Error(`Unknown fragment: ${fragmentName}`);\n }\n },\n },\n });\n\n return usedFragments;\n}\n","import { DocumentNode, visit } from 'graphql';\nimport {\n PluginConfig,\n ClientOperationListManifest,\n ServerOperationListManifest,\n PersistedQueryManifestOperation,\n ProcessedOperation,\n} from '../types';\nimport { createHash } from '../utils/hashing';\nimport { addTypenameToDocument } from '../utils/transforms';\nimport { findFragments, findUsedFragments } from '../utils/fragments';\nimport { printDefinitions } from '../utils/transforms';\n\n/**\n * Process documents and generate operation hashes with their details\n *\n * @param docs - Array of GraphQL document nodes\n * @param config - Plugin configuration\n * @returns Array of processed operations with their details\n * @throws Error if an operation is missing a name\n */\nfunction processOperations(\n docs: DocumentNode[],\n config: PluginConfig,\n): ProcessedOperation[] {\n // Add __typename to all selection sets\n const processedDocs = docs.map(addTypenameToDocument);\n const operations: ProcessedOperation[] = [];\n\n const knownFragments = findFragments(processedDocs);\n\n for (const doc of processedDocs) {\n visit(doc, {\n OperationDefinition: {\n enter(def) {\n if (!def.name) {\n throw new Error('OperationDefinition missing name');\n }\n\n const operationName = def.name.value;\n const usedFragments = findUsedFragments(def, knownFragments);\n\n // The order is important here. We want to have the operation definition first,\n // then the fragments as they are included.\n // Otherwise, the generated hashes won't match with the one generated on the server.\n const query = printDefinitions([def, ...usedFragments.values()]);\n\n const hash = createHash(query, config);\n\n operations.push({\n name: operationName,\n hash,\n type: def.operation,\n query,\n definition: def,\n fragments: Array.from(usedFragments.values()),\n });\n },\n },\n });\n }\n\n return operations;\n}\n\n/**\n * Generates a client-side persisted query manifest\n * Client manifests map operation names to their hash\n *\n * @param docs - Array of GraphQL document nodes\n * @param config - Plugin configuration\n * @returns Client-side manifest object\n * @throws Error if an operation is missing a name\n */\nexport function generateClientManifest(\n docs: DocumentNode[],\n config: PluginConfig,\n): ClientOperationListManifest {\n const operations = processOperations(docs, config);\n const manifest: ClientOperationListManifest = {};\n\n for (const operation of operations) {\n manifest[operation.name] = operation.hash;\n }\n\n return manifest;\n}\n\n/**\n * Generates a server-side persisted query manifest\n * Server manifests map query hashes to operation details\n *\n * @param docs - Array of GraphQL document nodes\n * @param config - Plugin configuration\n * @returns Server-side manifest object\n * @throws Error if an operation is missing a name\n */\nexport function generateServerManifest(\n docs: DocumentNode[],\n config: PluginConfig,\n): ServerOperationListManifest {\n const operations = processOperations(docs, config);\n\n const manifest: ServerOperationListManifest = {\n format: 'apollo-persisted-query-manifest',\n version: 1,\n operations: {},\n };\n\n for (const operation of operations) {\n const operationDetails: PersistedQueryManifestOperation = {\n type: operation.type,\n name: operation.name,\n body: operation.query,\n };\n\n manifest.operations[operation.hash] = operationDetails;\n }\n\n return manifest;\n}\n","import { DocumentNode } from 'graphql';\nimport { PersistedQueryPlugin } from '../types';\nimport { generateClientManifest, generateServerManifest } from './generator';\n\n/**\n * GraphQL CodeGen plugin for generating persisted operation manifests\n *\n * @param _schema - GraphQL schema (unused)\n * @param documents - GraphQL documents to process\n * @param config - Plugin configuration\n * @returns JSON string representation of the generated manifest\n * @throws Error if no documents are provided or output format is not specified\n *\n * For GraphQL over HTTP specification compliance, set `includeAlgorithmPrefix: true`\n * to enable the \"Prefixed Document Identifier\" format (e.g., `sha256:abc123...`).\n */\nexport const plugin: PersistedQueryPlugin = (_schema, documents, config) => {\n // Validate input documents\n if (\n !documents ||\n documents.length === 0 ||\n documents.some((doc) => !doc?.document)\n ) {\n throw new Error('Found no documents to generate persisted operation ids.');\n }\n\n // Extract document nodes\n const documentNodes: DocumentNode[] = documents\n .map((doc) => doc.document)\n .filter((doc): doc is DocumentNode => doc !== undefined);\n\n // Generate appropriate manifest format based on configuration\n if (config.output === 'client') {\n return JSON.stringify(\n generateClientManifest(documentNodes, config),\n null,\n ' ',\n );\n } else if (config.output === 'server') {\n return JSON.stringify(\n generateServerManifest(documentNodes, config),\n null,\n ' ',\n );\n } else {\n throw new Error(\"Must configure output to 'server' or 'client'\");\n }\n};\n"]}