graphql-buddy 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/api.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  normalizeGraphqlFiles,
6
6
  validate,
7
7
  validateDocuments
8
- } from "./chunk-ZIUWDMA7.js";
8
+ } from "./chunk-O3MJ27FP.js";
9
9
  export {
10
10
  bundle,
11
11
  loadDocuments,
@@ -79,6 +79,12 @@ var loadDocuments2 = (graphqlFiles) => __async(null, null, function* () {
79
79
  }
80
80
  return yield graphqlLoad.loadDocuments(files, {
81
81
  loaders: [new graphqlFileLoader.GraphQLFileLoader()],
82
+ // By default @graphql-tools/load strips type definitions out of executable
83
+ // documents, so SDL that lands in an operations file (or a schema file
84
+ // caught by an operations glob) would silently vanish. Keeping every
85
+ // definition lets `ExecutableDefinitionsRule` reject it during validation
86
+ // instead.
87
+ filterKinds: [],
82
88
  pathAliases: {
83
89
  mappings: Object.fromEntries(pathAliases.entries())
84
90
  }
@@ -114,21 +120,48 @@ var validateDocuments = (options) => {
114
120
  if (source.document == null) {
115
121
  continue;
116
122
  }
117
- const definesOperation = source.document.definitions.some(
118
- (definition) => definition.kind === graphql.Kind.OPERATION_DEFINITION
119
- );
120
- const errors = graphql.validate(
121
- schema,
122
- source.document,
123
- graphql.specifiedRules.filter(
124
- (rule) => definesOperation || rule !== graphql.NoUnusedFragmentsRule
125
- )
126
- );
123
+ const errors = validateDocument(schema, source.document);
127
124
  if (errors.length > 0) {
128
125
  throw errors[0];
129
126
  }
130
127
  }
131
128
  };
129
+ var validateDocument = (schema, document) => {
130
+ const errors = [];
131
+ const operations = document.definitions.filter(
132
+ (definition) => definition.kind === graphql.Kind.OPERATION_DEFINITION
133
+ );
134
+ for (const operation of operations) {
135
+ if (schema.getRootType(operation.operation) == null) {
136
+ errors.push(
137
+ new graphql.GraphQLError(
138
+ `Schema does not define a ${operation.operation} root type.`,
139
+ { nodes: operation }
140
+ )
141
+ );
142
+ }
143
+ }
144
+ const rules = graphql.specifiedRules.filter(
145
+ (rule) => operations.length > 0 || rule !== graphql.NoUnusedFragmentsRule
146
+ );
147
+ const typeInfo = new graphql.TypeInfo(schema);
148
+ const context = new graphql.ValidationContext(
149
+ schema,
150
+ document,
151
+ typeInfo,
152
+ (error) => {
153
+ errors.push(error);
154
+ }
155
+ );
156
+ graphql.visit(
157
+ document,
158
+ graphql.visitWithTypeInfo(
159
+ typeInfo,
160
+ graphql.visitInParallel(rules.map((rule) => rule(context)))
161
+ )
162
+ );
163
+ return errors;
164
+ };
132
165
 
133
166
  // src/api/commands/bundle.ts
134
167
  var bundle = (options) => __async(null, null, function* () {
@@ -143,7 +176,7 @@ var bundle = (options) => __async(null, null, function* () {
143
176
 
144
177
  // src/api/commands/validate.ts
145
178
  import * as stringifyObject from "stringify-object";
146
- var validate2 = (options) => __async(null, null, function* () {
179
+ var validate = (options) => __async(null, null, function* () {
147
180
  const normalizedSchemaOptions = yield normalizeGraphqlFiles(
148
181
  options.schema
149
182
  );
@@ -185,6 +218,6 @@ export {
185
218
  loadSchema2 as loadSchema,
186
219
  validateDocuments,
187
220
  bundle,
188
- validate2 as validate
221
+ validate
189
222
  };
190
- //# sourceMappingURL=chunk-ZIUWDMA7.js.map
223
+ //# sourceMappingURL=chunk-O3MJ27FP.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/api/commands/bundle.ts","../src/api/shared/GraphqlFiles.ts","../src/api/shared/loadDocuments.ts","../src/api/shared/loadSchema.ts","../src/api/shared/validateDocuments.ts","../src/api/commands/validate.ts"],"sourcesContent":["import * as graphqlToolsUtils from '@graphql-tools/utils';\nimport * as graphql from 'graphql';\n\nimport * as shared from '../shared';\n\nexport type BundleOptions = {\n schema: shared.GraphqlFiles;\n\n // Defaults to true.\n shake?: boolean | null;\n};\n\n/**\n * Bundles multiple schema together into a single file.\n *\n * @param options - Bundle options.\n *\n * @returns The merged schema as text.\n */\nexport const bundle = async (options: BundleOptions): Promise<string> => {\n const shake = options.shake ?? true;\n\n let schema = await shared.loadSchema(options.schema);\n if (shake) {\n schema = graphqlToolsUtils.pruneSchema(schema);\n }\n\n return graphql.printSchema(schema);\n};\n","import * as nodePath from 'node:path';\n\nimport * as glob from 'glob';\n\nexport type GraphqlFiles = UnsafeGraphqlFiles | NormalizedGraphqlFiles;\n\nexport type UnsafeGraphqlFiles = {\n normalized?: false | null;\n\n // Relative paths will be resolved using `base`.\n //\n // Also accepts glob patterns.\n files: Array<string>;\n\n // Directory from which `files` are resolved.\n //\n // Defaults to `process.cwd()`.\n base?: string | null;\n\n // See @graphql-tools/import#PathAliases\n //\n // Note that relative paths will be resolved relative to `base` to maintain\n // parity with `files`.\n pathAliases?: Map<string, string> | null;\n};\n\nexport type NormalizedGraphqlFiles = {\n normalized: true;\n\n // Absolute paths.\n files: Array<string>;\n\n // See @graphql-tools/import#PathAliases\n pathAliases: Map<string, string>;\n};\n\nexport const normalizeGraphqlFiles = async (\n graphqlFiles: GraphqlFiles,\n): Promise<NormalizedGraphqlFiles> => {\n if (graphqlFiles.normalized) {\n return graphqlFiles;\n }\n\n const base = graphqlFiles.base ?? process.cwd();\n const files = await glob.glob(\n graphqlFiles.files.map((file) => nodePath.resolve(base, file)),\n );\n\n const pathAliases = new Map<string, string>();\n if (graphqlFiles.pathAliases != null) {\n for (const [alias, source] of graphqlFiles.pathAliases.entries()) {\n pathAliases.set(alias, nodePath.resolve(base, source));\n }\n }\n\n return {\n normalized: true,\n files,\n pathAliases,\n };\n};\n","import * as nodeFs from 'node:fs/promises';\n\nimport * as graphqlFileLoader from '@graphql-tools/graphql-file-loader';\nimport * as graphqlLoad from '@graphql-tools/load';\n\nimport type * as graphqlUtils from '@graphql-tools/utils';\n\nimport * as graphqlFilesModule from './GraphqlFiles';\n\n/**\n * Loads, validates, and parses graphql documents.\n *\n * @param graphqlFiles - Operation or fragment files to process.\n *\n * @returns The parsed GraphQL documents (or throws an error if parsing fails).\n */\nexport const loadDocuments = async (\n graphqlFiles: graphqlFilesModule.GraphqlFiles,\n): Promise<Array<graphqlUtils.Source>> => {\n const { files, pathAliases } =\n await graphqlFilesModule.normalizeGraphqlFiles(graphqlFiles);\n\n // @graphql-tools/graphql-file-loader silently skips input files which are not\n // found. I suspect this is so that it can delegate missing inputs to other\n // loaders. For our purposes, this is undesirable because we want to inform\n // clients if expected inputs are missing. For that reason, we manually\n // validate that the files exist first.\n for (const file of files) {\n try {\n await nodeFs.access(file, nodeFs.constants.F_OK);\n } catch {\n throw new Error(`File does not exist: ${file}`);\n }\n }\n\n return await graphqlLoad.loadDocuments(files, {\n loaders: [new graphqlFileLoader.GraphQLFileLoader()],\n // By default @graphql-tools/load strips type definitions out of executable\n // documents, so SDL that lands in an operations file (or a schema file\n // caught by an operations glob) would silently vanish. Keeping every\n // definition lets `ExecutableDefinitionsRule` reject it during validation\n // instead.\n filterKinds: [],\n pathAliases: {\n mappings: Object.fromEntries(pathAliases.entries()),\n },\n });\n};\n","import * as nodeFs from 'node:fs/promises';\n\nimport * as graphqlFileLoader from '@graphql-tools/graphql-file-loader';\nimport * as graphqlLoad from '@graphql-tools/load';\n\nimport type * as graphql from 'graphql';\n\nimport * as graphqlFilesModule from './GraphqlFiles';\n\n/**\n * Loads, validates, and parses graphql schema.\n *\n * @param graphqlFiles - Schema files to process.\n *\n * @returns The parsed GraphQL schema (or throws an error if parsing fails).\n */\nexport const loadSchema = async (\n graphqlFiles: graphqlFilesModule.GraphqlFiles,\n): Promise<graphql.GraphQLSchema> => {\n const { files, pathAliases } =\n await graphqlFilesModule.normalizeGraphqlFiles(graphqlFiles);\n\n // @graphql-tools/graphql-file-loader silently skips input files which are not\n // found. I suspect this is so that it can delegate missing inputs to other\n // loaders. For our purposes, this is undesirable because we want to inform\n // clients if expected inputs are missing. For that reason, we manually\n // validate that the files exist first.\n for (const file of files) {\n try {\n await nodeFs.access(file, nodeFs.constants.F_OK);\n } catch {\n throw new Error(`File does not exist: ${file}`);\n }\n }\n\n return graphqlLoad.loadSchema(files, {\n loaders: [new graphqlFileLoader.GraphQLFileLoader()],\n pathAliases: {\n mappings: Object.fromEntries(pathAliases.entries()),\n },\n });\n};\n","import * as graphql from 'graphql';\n\nimport type * as graphqlUtils from '@graphql-tools/utils';\n\nexport type ValidateDocumentsOptions = {\n schema: graphql.GraphQLSchema;\n documents: Array<graphqlUtils.Source>;\n};\n\n/**\n * Validates GraphQL documents against their target schema.\n *\n * @param options - Validation options (schema and documents).\n *\n * @throws GraphQLError instances when validation fails.\n */\nexport const validateDocuments = (options: ValidateDocumentsOptions): void => {\n const { schema, documents } = options;\n\n for (const source of documents) {\n if (source.document == null) {\n continue;\n }\n\n const errors = validateDocument(schema, source.document);\n if (errors.length > 0) {\n throw errors[0];\n }\n }\n};\n\n/**\n * Validates a single document against the schema.\n *\n * This deliberately does not use `graphql.validate`. That function begins by\n * asserting the *schema* is valid, which among other things requires a query\n * root type. Fragment libraries are frequently validated against only the\n * type definitions they select from.\n *\n * @param schema - The parsed graphql schema.\n * @param document - The document to validate.\n *\n * @returns Any errors found in the document.\n */\nconst validateDocument = (\n schema: graphql.GraphQLSchema,\n document: graphql.DocumentNode,\n): Array<graphql.GraphQLError> => {\n const errors: Array<graphql.GraphQLError> = [];\n\n const operations = document.definitions.filter(\n (definition): definition is graphql.OperationDefinitionNode =>\n definition.kind === graphql.Kind.OPERATION_DEFINITION,\n );\n\n // Ensure each operation matches a root GraphQL type. This is not natively\n // checked by the `ValidationContext` because the validation context only\n // inspects operations whose type information is available.\n for (const operation of operations) {\n if (schema.getRootType(operation.operation) == null) {\n errors.push(\n new graphql.GraphQLError(\n `Schema does not define a ${operation.operation} root type.`,\n { nodes: operation },\n ),\n );\n }\n }\n\n // Fragment-only documents are fragment \"libraries\" that operations pull in\n // via `#import`. Validated in isolation they would trip\n // `NoUnusedFragmentsRule`, so we drop that single rule for them. Every\n // other rule, most importantly that each referenced field, argument, and\n // type actually exists, are still enforced.\n const rules = graphql.specifiedRules.filter(\n (rule) => operations.length > 0 || rule !== graphql.NoUnusedFragmentsRule,\n );\n\n const typeInfo = new graphql.TypeInfo(schema);\n const context = new graphql.ValidationContext(\n schema,\n document,\n typeInfo,\n (error) => {\n errors.push(error);\n },\n );\n graphql.visit(\n document,\n graphql.visitWithTypeInfo(\n typeInfo,\n graphql.visitInParallel(rules.map((rule) => rule(context))),\n ),\n );\n\n return errors;\n};\n","import * as stringifyObject from 'stringify-object';\n\nimport * as shared from '../shared';\n\nexport type ValidateOptions = {\n schema: shared.GraphqlFiles;\n\n // Optional GraphQL operation documents (queries, mutations, subscriptions,\n // and fragments) to validate against the loaded schema. When omitted, only\n // the schema itself is validated.\n operations?: shared.GraphqlFiles | null;\n};\n\n/**\n * Validates that the provided GraphQL schema contain valid syntax and have no\n * unknown symbols. When operations are provided, additionally validates that\n * every operation is valid against that schema.\n *\n * @param options - Validate options.\n *\n * @returns If validation is successful, returns a validation stamp: a text blob\n * describing the successful outcome and options used during validation. This\n * is often useful in build systems where a build step *must* emit a file. If\n * validation fails, a runtime error will be thrown.\n */\nexport const validate = async (options: ValidateOptions): Promise<string> => {\n // We manually normalize the `LoadSchemaOptions` so that we can write the most\n // accurate data in our validation stamp. For example, normalization expands\n // globs and resolves absolute paths. These are critical steps to ensure the\n // validation stamp describes without ambiguity which schema were loaded.\n const normalizedSchemaOptions = await shared.normalizeGraphqlFiles(\n options.schema,\n );\n\n const schema = await shared.loadSchema(normalizedSchemaOptions);\n\n if (options.operations == null) {\n return createValidationStamp({ schema: normalizedSchemaOptions });\n }\n\n const normalizedOperationsOptions = await shared.normalizeGraphqlFiles(\n options.operations,\n );\n shared.validateDocuments({\n schema,\n documents: await shared.loadDocuments(normalizedOperationsOptions),\n });\n\n return createValidationStamp({\n schema: normalizedSchemaOptions,\n operations: normalizedOperationsOptions,\n });\n};\n\nconst createValidationStamp = (options: {\n schema: shared.NormalizedGraphqlFiles;\n operations?: shared.NormalizedGraphqlFiles;\n}): string =>\n stringifyObject.default(\n {\n options: {\n schema: options.schema,\n operations: options.operations,\n },\n outcome: { valid: true },\n },\n {\n indent: ` `,\n filter: (container, property) =>\n !(container === options.schema && property === `normalized`) &&\n !(container === options.operations && property === `normalized`),\n },\n );\n"],"mappings":"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","names":["graphql","loadDocuments","nodeFs","graphqlFileLoader","graphqlLoad","loadSchema","loadSchema","loadSchema","loadDocuments"]}
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  __spreadValues,
5
5
  bundle,
6
6
  validate
7
- } from "./chunk-ZIUWDMA7.js";
7
+ } from "./chunk-O3MJ27FP.js";
8
8
 
9
9
  // src/cli/index.ts
10
10
  import * as commander3 from "@commander-js/extra-typings";
@@ -122,12 +122,12 @@ var validate2 = (options) => __async(null, null, function* () {
122
122
  // src/cli/index.ts
123
123
  var main = () => {
124
124
  process.setUncaughtExceptionCaptureCallback((error) => {
125
+ process.exitCode = 1;
125
126
  if (error instanceof graphql.GraphQLError) {
126
127
  console.error(error.toString());
127
128
  return;
128
129
  }
129
130
  console.error(error);
130
- process.exit(1);
131
131
  });
132
132
  commander3.program.name(`graphql-buddy`).description(
133
133
  `CLI tools for manipulating graphql // designed for build system tooling.`
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cli/index.ts","../src/cli/commands/bundle.ts","../src/cli/shared/parsePathAliases.ts","../src/cli/commands/validate.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport * as commander from '@commander-js/extra-typings';\nimport * as graphql from 'graphql';\n\nimport * as commands from './commands';\n\nconst main = () => {\n process.setUncaughtExceptionCaptureCallback((error) => {\n // When thrown natively, GraphQLError prints like any other error with\n // minimal context, however, when logged with `.toString()` it prints\n // helpful debugging information critical for debugging syntax errors (such\n // as source file, location, symbol, etc...).\n //\n // For example:\n //\n // ```\n // Syntax Error: Expected \":\", found Name \"ID\".\n //\n // src/services/api/schema/CurrentUser.graphql:2:6\n // 1 | type CurrentUser {\n // 2 | id ID!\n // | ^\n // 3 | dateCreated: DateTime!\n // ```\n //\n // Versus the naive output:\n //\n // ```\n // return new _GraphQLError.GraphQLError(`Syntax Error: ${description}`, {\n // GraphQLError: Syntax Error: Expected \":\", found Name \"ID\".\n // at syntaxError (node_modules/graphql/error/syntaxError.js:15:10)\n // ```\n if (error instanceof graphql.GraphQLError) {\n console.error(error.toString());\n return;\n }\n\n console.error(error);\n\n process.exit(1);\n });\n\n commander.program\n .name(`graphql-buddy`)\n .description(\n `CLI tools for manipulating graphql // designed for build system tooling.`,\n )\n .addCommand(commands.createBundleCommand())\n .addCommand(commands.createValidateCommand())\n .parse();\n};\n\nmain();\n","import * as nodeFs from 'node:fs/promises';\nimport * as nodePath from 'node:path';\n\nimport * as commander from '@commander-js/extra-typings';\n\nimport * as api from '../../api';\nimport * as shared from '../shared';\n\nexport const createBundleCommand = () =>\n new commander.Command()\n .name(`bundle`)\n .description(`Bundles multiple GraphQL inputs into a single file.`)\n .argument(`<schema...>`, `Input GraphQL schema files to bundle.`)\n .option(\n `-b, --base <directory>`,\n `Base directory from which paths are resolved.`,\n )\n .option(\n `-a, --alias <pattern...>`,\n `Path alias options for @graphql-tools/load.`,\n )\n .option(\n `--out <file>`,\n `Where to write the bundled schema.`,\n `bundle.graphql`,\n )\n .option(`--no-shake`, `Prevents pruning of unused types.`)\n .option(`-s, --silent`, `Only log critical information.`, false)\n .action((schema, options) =>\n bundle({\n schema,\n ...options,\n }),\n );\n\nconst bundle = async (options: {\n schema: Array<string>;\n base?: string;\n alias?: Array<string>;\n out: string;\n shake: boolean;\n silent: boolean;\n}): Promise<void> => {\n const { schema, base, alias, shake, silent } = options;\n const out = nodePath.resolve(base ?? process.cwd(), options.out);\n\n await nodeFs.writeFile(\n out,\n await api.bundle({\n schema: {\n files: schema,\n base,\n pathAliases: shared.parsePathAliases(alias ?? []),\n },\n shake,\n }),\n );\n\n if (!silent) {\n console.log(`✅ bundler encountered no errors.`);\n }\n};\n","export type PathAliases = Map<string, string>;\n\nexport const parsePathAliases = (\n encodedAliases: Array<string>,\n): PathAliases => {\n const decodedAliases = new Map<string, string>();\n\n for (const encodedAlias of encodedAliases) {\n const parts = encodedAlias.split(`=`);\n if (parts.length !== 2) {\n throw new Error(\n `Expected an alias in the form \"alias=source\" but found \"${encodedAlias}\"`,\n );\n }\n\n const [alias, source] = parts;\n if (decodedAliases.has(alias)) {\n throw new Error(`Alias \"${alias}\" is set multiple times.`);\n }\n\n decodedAliases.set(alias, source);\n }\n\n return decodedAliases;\n};\n","import * as nodeFs from 'node:fs/promises';\nimport * as nodePath from 'node:path';\n\nimport * as commander from '@commander-js/extra-typings';\n\nimport * as api from '../../api';\nimport * as shared from '../shared';\n\nexport const createValidateCommand = () =>\n new commander.Command()\n .name(`validate`)\n .description(\n `Checks that all provided GraphQL files form a valid schema, and optionally that operations are valid against it.`,\n )\n .argument(`<schema...>`, `Input GraphQL schema files to validate.`)\n .option(\n `-b, --base <directory>`,\n `Base directory from which paths are resolved.`,\n )\n .option(\n `-a, --alias <pattern...>`,\n `Path alias options for @graphql-tools/load.`,\n )\n .option(\n `-o, --operations <operations...>`,\n `GraphQL operation documents to validate against the schema.`,\n )\n .option(\n `--stamp <file>`,\n `Creates a validation stamp to confirm valid GraphQL schema.`,\n )\n .option(`-s, --silent`, `Only log critical information.`, false)\n .action((schema, options) =>\n validate({\n schema,\n ...options,\n }),\n );\n\nconst validate = async (options: {\n schema: Array<string>;\n base?: string;\n alias?: Array<string>;\n operations?: Array<string>;\n stamp?: string;\n silent: boolean;\n}): Promise<void> => {\n const { schema, base, alias, operations, stamp, silent } = options;\n\n const pathAliases = shared.parsePathAliases(alias ?? []);\n\n const validateOptions: api.ValidateOptions = {\n schema: {\n files: schema,\n base,\n pathAliases,\n },\n };\n if (operations != null) {\n validateOptions.operations = {\n files: operations,\n base,\n pathAliases,\n };\n }\n const validationStamp = await api.validate(validateOptions);\n\n if (stamp != null) {\n const destination = nodePath.resolve(base ?? process.cwd(), stamp);\n await nodeFs.writeFile(destination, validationStamp);\n }\n\n if (!silent) {\n console.log(`✅ validation encountered no errors.`);\n }\n};\n"],"mappings":"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","names":["commander","bundle","nodeFs","nodePath","commander","validate"]}
1
+ {"version":3,"sources":["../src/cli/index.ts","../src/cli/commands/bundle.ts","../src/cli/shared/parsePathAliases.ts","../src/cli/commands/validate.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport * as commander from '@commander-js/extra-typings';\nimport * as graphql from 'graphql';\n\nimport * as commands from './commands';\n\nconst main = () => {\n process.setUncaughtExceptionCaptureCallback((error) => {\n process.exitCode = 1;\n\n // When thrown natively, GraphQLError prints like any other error with\n // minimal context, however, when logged with `.toString()` it prints\n // helpful debugging information critical for debugging syntax errors (such\n // as source file, location, symbol, etc...).\n //\n // For example:\n //\n // ```\n // Syntax Error: Expected \":\", found Name \"ID\".\n //\n // src/services/api/schema/CurrentUser.graphql:2:6\n // 1 | type CurrentUser {\n // 2 | id ID!\n // | ^\n // 3 | dateCreated: DateTime!\n // ```\n //\n // Versus the naive output:\n //\n // ```\n // return new _GraphQLError.GraphQLError(`Syntax Error: ${description}`, {\n // GraphQLError: Syntax Error: Expected \":\", found Name \"ID\".\n // at syntaxError (node_modules/graphql/error/syntaxError.js:15:10)\n // ```\n if (error instanceof graphql.GraphQLError) {\n console.error(error.toString());\n return;\n }\n\n console.error(error);\n });\n\n commander.program\n .name(`graphql-buddy`)\n .description(\n `CLI tools for manipulating graphql // designed for build system tooling.`,\n )\n .addCommand(commands.createBundleCommand())\n .addCommand(commands.createValidateCommand())\n .parse();\n};\n\nmain();\n","import * as nodeFs from 'node:fs/promises';\nimport * as nodePath from 'node:path';\n\nimport * as commander from '@commander-js/extra-typings';\n\nimport * as api from '../../api';\nimport * as shared from '../shared';\n\nexport const createBundleCommand = () =>\n new commander.Command()\n .name(`bundle`)\n .description(`Bundles multiple GraphQL inputs into a single file.`)\n .argument(`<schema...>`, `Input GraphQL schema files to bundle.`)\n .option(\n `-b, --base <directory>`,\n `Base directory from which paths are resolved.`,\n )\n .option(\n `-a, --alias <pattern...>`,\n `Path alias options for @graphql-tools/load.`,\n )\n .option(\n `--out <file>`,\n `Where to write the bundled schema.`,\n `bundle.graphql`,\n )\n .option(`--no-shake`, `Prevents pruning of unused types.`)\n .option(`-s, --silent`, `Only log critical information.`, false)\n .action((schema, options) =>\n bundle({\n schema,\n ...options,\n }),\n );\n\nconst bundle = async (options: {\n schema: Array<string>;\n base?: string;\n alias?: Array<string>;\n out: string;\n shake: boolean;\n silent: boolean;\n}): Promise<void> => {\n const { schema, base, alias, shake, silent } = options;\n const out = nodePath.resolve(base ?? process.cwd(), options.out);\n\n await nodeFs.writeFile(\n out,\n await api.bundle({\n schema: {\n files: schema,\n base,\n pathAliases: shared.parsePathAliases(alias ?? []),\n },\n shake,\n }),\n );\n\n if (!silent) {\n console.log(`✅ bundler encountered no errors.`);\n }\n};\n","export type PathAliases = Map<string, string>;\n\nexport const parsePathAliases = (\n encodedAliases: Array<string>,\n): PathAliases => {\n const decodedAliases = new Map<string, string>();\n\n for (const encodedAlias of encodedAliases) {\n const parts = encodedAlias.split(`=`);\n if (parts.length !== 2) {\n throw new Error(\n `Expected an alias in the form \"alias=source\" but found \"${encodedAlias}\"`,\n );\n }\n\n const [alias, source] = parts;\n if (decodedAliases.has(alias)) {\n throw new Error(`Alias \"${alias}\" is set multiple times.`);\n }\n\n decodedAliases.set(alias, source);\n }\n\n return decodedAliases;\n};\n","import * as nodeFs from 'node:fs/promises';\nimport * as nodePath from 'node:path';\n\nimport * as commander from '@commander-js/extra-typings';\n\nimport * as api from '../../api';\nimport * as shared from '../shared';\n\nexport const createValidateCommand = () =>\n new commander.Command()\n .name(`validate`)\n .description(\n `Checks that all provided GraphQL files form a valid schema, and optionally that operations are valid against it.`,\n )\n .argument(`<schema...>`, `Input GraphQL schema files to validate.`)\n .option(\n `-b, --base <directory>`,\n `Base directory from which paths are resolved.`,\n )\n .option(\n `-a, --alias <pattern...>`,\n `Path alias options for @graphql-tools/load.`,\n )\n .option(\n `-o, --operations <operations...>`,\n `GraphQL operation documents to validate against the schema.`,\n )\n .option(\n `--stamp <file>`,\n `Creates a validation stamp to confirm valid GraphQL schema.`,\n )\n .option(`-s, --silent`, `Only log critical information.`, false)\n .action((schema, options) =>\n validate({\n schema,\n ...options,\n }),\n );\n\nconst validate = async (options: {\n schema: Array<string>;\n base?: string;\n alias?: Array<string>;\n operations?: Array<string>;\n stamp?: string;\n silent: boolean;\n}): Promise<void> => {\n const { schema, base, alias, operations, stamp, silent } = options;\n\n const pathAliases = shared.parsePathAliases(alias ?? []);\n\n const validateOptions: api.ValidateOptions = {\n schema: {\n files: schema,\n base,\n pathAliases,\n },\n };\n if (operations != null) {\n validateOptions.operations = {\n files: operations,\n base,\n pathAliases,\n };\n }\n const validationStamp = await api.validate(validateOptions);\n\n if (stamp != null) {\n const destination = nodePath.resolve(base ?? process.cwd(), stamp);\n await nodeFs.writeFile(destination, validationStamp);\n }\n\n if (!silent) {\n console.log(`✅ validation encountered no errors.`);\n }\n};\n"],"mappings":"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","names":["commander","bundle","nodeFs","nodePath","commander","validate"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "graphql-buddy",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "author": "Hunter John Larco",
@@ -79,6 +79,12 @@ describe(`validate`, () => {
79
79
  ).resolves.toStrictEqual(expect.any(String));
80
80
  });
81
81
 
82
+ it(`throws for operations when the schema has no matching root type.`, async () => {
83
+ await expect(
84
+ validate.validate(documentsFixture(`missing_root_type`)),
85
+ ).rejects.toThrowError(`Schema does not define a query root type.`);
86
+ });
87
+
82
88
  it(`still validates field selections inside fragment-only files.`, async () => {
83
89
  await expect(
84
90
  validate.validate(documentsFixture(`invalid_fragment`)),
@@ -22,24 +22,76 @@ export const validateDocuments = (options: ValidateDocumentsOptions): void => {
22
22
  continue;
23
23
  }
24
24
 
25
- // Fragment-only documents are fragment "libraries" that operations pull in
26
- // via `#import`. Validated in isolation they would trip
27
- // `NoUnusedFragmentsRule`, so we drop that single rule for them. Every
28
- // other rule, most importantly that each referenced field, argument, and
29
- // type actually exists, are still enforced.
30
- const definesOperation = source.document.definitions.some(
31
- (definition) => definition.kind === graphql.Kind.OPERATION_DEFINITION,
32
- );
33
-
34
- const errors = graphql.validate(
35
- schema,
36
- source.document,
37
- graphql.specifiedRules.filter(
38
- (rule) => definesOperation || rule !== graphql.NoUnusedFragmentsRule,
39
- ),
40
- );
25
+ const errors = validateDocument(schema, source.document);
41
26
  if (errors.length > 0) {
42
27
  throw errors[0];
43
28
  }
44
29
  }
45
30
  };
31
+
32
+ /**
33
+ * Validates a single document against the schema.
34
+ *
35
+ * This deliberately does not use `graphql.validate`. That function begins by
36
+ * asserting the *schema* is valid, which among other things requires a query
37
+ * root type. Fragment libraries are frequently validated against only the
38
+ * type definitions they select from.
39
+ *
40
+ * @param schema - The parsed graphql schema.
41
+ * @param document - The document to validate.
42
+ *
43
+ * @returns Any errors found in the document.
44
+ */
45
+ const validateDocument = (
46
+ schema: graphql.GraphQLSchema,
47
+ document: graphql.DocumentNode,
48
+ ): Array<graphql.GraphQLError> => {
49
+ const errors: Array<graphql.GraphQLError> = [];
50
+
51
+ const operations = document.definitions.filter(
52
+ (definition): definition is graphql.OperationDefinitionNode =>
53
+ definition.kind === graphql.Kind.OPERATION_DEFINITION,
54
+ );
55
+
56
+ // Ensure each operation matches a root GraphQL type. This is not natively
57
+ // checked by the `ValidationContext` because the validation context only
58
+ // inspects operations whose type information is available.
59
+ for (const operation of operations) {
60
+ if (schema.getRootType(operation.operation) == null) {
61
+ errors.push(
62
+ new graphql.GraphQLError(
63
+ `Schema does not define a ${operation.operation} root type.`,
64
+ { nodes: operation },
65
+ ),
66
+ );
67
+ }
68
+ }
69
+
70
+ // Fragment-only documents are fragment "libraries" that operations pull in
71
+ // via `#import`. Validated in isolation they would trip
72
+ // `NoUnusedFragmentsRule`, so we drop that single rule for them. Every
73
+ // other rule, most importantly that each referenced field, argument, and
74
+ // type actually exists, are still enforced.
75
+ const rules = graphql.specifiedRules.filter(
76
+ (rule) => operations.length > 0 || rule !== graphql.NoUnusedFragmentsRule,
77
+ );
78
+
79
+ const typeInfo = new graphql.TypeInfo(schema);
80
+ const context = new graphql.ValidationContext(
81
+ schema,
82
+ document,
83
+ typeInfo,
84
+ (error) => {
85
+ errors.push(error);
86
+ },
87
+ );
88
+ graphql.visit(
89
+ document,
90
+ graphql.visitWithTypeInfo(
91
+ typeInfo,
92
+ graphql.visitInParallel(rules.map((rule) => rule(context))),
93
+ ),
94
+ );
95
+
96
+ return errors;
97
+ };
@@ -0,0 +1,7 @@
1
+ type Foo {
2
+ id: ID!
3
+ name: String
4
+ dateCreated: DateTime!
5
+ }
6
+
7
+ scalar DateTime
@@ -0,0 +1,5 @@
1
+ query AllFoo {
2
+ allFoo {
3
+ id
4
+ }
5
+ }
@@ -0,0 +1,7 @@
1
+ type Foo {
2
+ id: ID!
3
+ name: String
4
+ dateCreated: DateTime!
5
+ }
6
+
7
+ scalar DateTime
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/api/commands/bundle.ts","../src/api/shared/GraphqlFiles.ts","../src/api/shared/loadDocuments.ts","../src/api/shared/loadSchema.ts","../src/api/shared/validateDocuments.ts","../src/api/commands/validate.ts"],"sourcesContent":["import * as graphqlToolsUtils from '@graphql-tools/utils';\nimport * as graphql from 'graphql';\n\nimport * as shared from '../shared';\n\nexport type BundleOptions = {\n schema: shared.GraphqlFiles;\n\n // Defaults to true.\n shake?: boolean | null;\n};\n\n/**\n * Bundles multiple schema together into a single file.\n *\n * @param options - Bundle options.\n *\n * @returns The merged schema as text.\n */\nexport const bundle = async (options: BundleOptions): Promise<string> => {\n const shake = options.shake ?? true;\n\n let schema = await shared.loadSchema(options.schema);\n if (shake) {\n schema = graphqlToolsUtils.pruneSchema(schema);\n }\n\n return graphql.printSchema(schema);\n};\n","import * as nodePath from 'node:path';\n\nimport * as glob from 'glob';\n\nexport type GraphqlFiles = UnsafeGraphqlFiles | NormalizedGraphqlFiles;\n\nexport type UnsafeGraphqlFiles = {\n normalized?: false | null;\n\n // Relative paths will be resolved using `base`.\n //\n // Also accepts glob patterns.\n files: Array<string>;\n\n // Directory from which `files` are resolved.\n //\n // Defaults to `process.cwd()`.\n base?: string | null;\n\n // See @graphql-tools/import#PathAliases\n //\n // Note that relative paths will be resolved relative to `base` to maintain\n // parity with `files`.\n pathAliases?: Map<string, string> | null;\n};\n\nexport type NormalizedGraphqlFiles = {\n normalized: true;\n\n // Absolute paths.\n files: Array<string>;\n\n // See @graphql-tools/import#PathAliases\n pathAliases: Map<string, string>;\n};\n\nexport const normalizeGraphqlFiles = async (\n graphqlFiles: GraphqlFiles,\n): Promise<NormalizedGraphqlFiles> => {\n if (graphqlFiles.normalized) {\n return graphqlFiles;\n }\n\n const base = graphqlFiles.base ?? process.cwd();\n const files = await glob.glob(\n graphqlFiles.files.map((file) => nodePath.resolve(base, file)),\n );\n\n const pathAliases = new Map<string, string>();\n if (graphqlFiles.pathAliases != null) {\n for (const [alias, source] of graphqlFiles.pathAliases.entries()) {\n pathAliases.set(alias, nodePath.resolve(base, source));\n }\n }\n\n return {\n normalized: true,\n files,\n pathAliases,\n };\n};\n","import * as nodeFs from 'node:fs/promises';\n\nimport * as graphqlFileLoader from '@graphql-tools/graphql-file-loader';\nimport * as graphqlLoad from '@graphql-tools/load';\n\nimport type * as graphqlUtils from '@graphql-tools/utils';\n\nimport * as graphqlFilesModule from './GraphqlFiles';\n\n/**\n * Loads, validates, and parses graphql documents.\n *\n * @param graphqlFiles - Operation or fragment files to process.\n *\n * @returns The parsed GraphQL documents (or throws an error if parsing fails).\n */\nexport const loadDocuments = async (\n graphqlFiles: graphqlFilesModule.GraphqlFiles,\n): Promise<Array<graphqlUtils.Source>> => {\n const { files, pathAliases } =\n await graphqlFilesModule.normalizeGraphqlFiles(graphqlFiles);\n\n // @graphql-tools/graphql-file-loader silently skips input files which are not\n // found. I suspect this is so that it can delegate missing inputs to other\n // loaders. For our purposes, this is undesirable because we want to inform\n // clients if expected inputs are missing. For that reason, we manually\n // validate that the files exist first.\n for (const file of files) {\n try {\n await nodeFs.access(file, nodeFs.constants.F_OK);\n } catch {\n throw new Error(`File does not exist: ${file}`);\n }\n }\n\n return await graphqlLoad.loadDocuments(files, {\n loaders: [new graphqlFileLoader.GraphQLFileLoader()],\n pathAliases: {\n mappings: Object.fromEntries(pathAliases.entries()),\n },\n });\n};\n","import * as nodeFs from 'node:fs/promises';\n\nimport * as graphqlFileLoader from '@graphql-tools/graphql-file-loader';\nimport * as graphqlLoad from '@graphql-tools/load';\n\nimport type * as graphql from 'graphql';\n\nimport * as graphqlFilesModule from './GraphqlFiles';\n\n/**\n * Loads, validates, and parses graphql schema.\n *\n * @param graphqlFiles - Schema files to process.\n *\n * @returns The parsed GraphQL schema (or throws an error if parsing fails).\n */\nexport const loadSchema = async (\n graphqlFiles: graphqlFilesModule.GraphqlFiles,\n): Promise<graphql.GraphQLSchema> => {\n const { files, pathAliases } =\n await graphqlFilesModule.normalizeGraphqlFiles(graphqlFiles);\n\n // @graphql-tools/graphql-file-loader silently skips input files which are not\n // found. I suspect this is so that it can delegate missing inputs to other\n // loaders. For our purposes, this is undesirable because we want to inform\n // clients if expected inputs are missing. For that reason, we manually\n // validate that the files exist first.\n for (const file of files) {\n try {\n await nodeFs.access(file, nodeFs.constants.F_OK);\n } catch {\n throw new Error(`File does not exist: ${file}`);\n }\n }\n\n return graphqlLoad.loadSchema(files, {\n loaders: [new graphqlFileLoader.GraphQLFileLoader()],\n pathAliases: {\n mappings: Object.fromEntries(pathAliases.entries()),\n },\n });\n};\n","import * as graphql from 'graphql';\n\nimport type * as graphqlUtils from '@graphql-tools/utils';\n\nexport type ValidateDocumentsOptions = {\n schema: graphql.GraphQLSchema;\n documents: Array<graphqlUtils.Source>;\n};\n\n/**\n * Validates GraphQL documents against their target schema.\n *\n * @param options - Validation options (schema and documents).\n *\n * @throws GraphQLError instances when validation fails.\n */\nexport const validateDocuments = (options: ValidateDocumentsOptions): void => {\n const { schema, documents } = options;\n\n for (const source of documents) {\n if (source.document == null) {\n continue;\n }\n\n // Fragment-only documents are fragment \"libraries\" that operations pull in\n // via `#import`. Validated in isolation they would trip\n // `NoUnusedFragmentsRule`, so we drop that single rule for them. Every\n // other rule, most importantly that each referenced field, argument, and\n // type actually exists, are still enforced.\n const definesOperation = source.document.definitions.some(\n (definition) => definition.kind === graphql.Kind.OPERATION_DEFINITION,\n );\n\n const errors = graphql.validate(\n schema,\n source.document,\n graphql.specifiedRules.filter(\n (rule) => definesOperation || rule !== graphql.NoUnusedFragmentsRule,\n ),\n );\n if (errors.length > 0) {\n throw errors[0];\n }\n }\n};\n","import * as stringifyObject from 'stringify-object';\n\nimport * as shared from '../shared';\n\nexport type ValidateOptions = {\n schema: shared.GraphqlFiles;\n\n // Optional GraphQL operation documents (queries, mutations, subscriptions,\n // and fragments) to validate against the loaded schema. When omitted, only\n // the schema itself is validated.\n operations?: shared.GraphqlFiles | null;\n};\n\n/**\n * Validates that the provided GraphQL schema contain valid syntax and have no\n * unknown symbols. When operations are provided, additionally validates that\n * every operation is valid against that schema.\n *\n * @param options - Validate options.\n *\n * @returns If validation is successful, returns a validation stamp: a text blob\n * describing the successful outcome and options used during validation. This\n * is often useful in build systems where a build step *must* emit a file. If\n * validation fails, a runtime error will be thrown.\n */\nexport const validate = async (options: ValidateOptions): Promise<string> => {\n // We manually normalize the `LoadSchemaOptions` so that we can write the most\n // accurate data in our validation stamp. For example, normalization expands\n // globs and resolves absolute paths. These are critical steps to ensure the\n // validation stamp describes without ambiguity which schema were loaded.\n const normalizedSchemaOptions = await shared.normalizeGraphqlFiles(\n options.schema,\n );\n\n const schema = await shared.loadSchema(normalizedSchemaOptions);\n\n if (options.operations == null) {\n return createValidationStamp({ schema: normalizedSchemaOptions });\n }\n\n const normalizedOperationsOptions = await shared.normalizeGraphqlFiles(\n options.operations,\n );\n shared.validateDocuments({\n schema,\n documents: await shared.loadDocuments(normalizedOperationsOptions),\n });\n\n return createValidationStamp({\n schema: normalizedSchemaOptions,\n operations: normalizedOperationsOptions,\n });\n};\n\nconst createValidationStamp = (options: {\n schema: shared.NormalizedGraphqlFiles;\n operations?: shared.NormalizedGraphqlFiles;\n}): string =>\n stringifyObject.default(\n {\n options: {\n schema: options.schema,\n operations: options.operations,\n },\n outcome: { valid: true },\n },\n {\n indent: ` `,\n filter: (container, property) =>\n !(container === options.schema && property === `normalized`) &&\n !(container === options.operations && property === `normalized`),\n },\n );\n"],"mappings":"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","names":["graphql","loadDocuments","nodeFs","graphqlFileLoader","graphqlLoad","loadSchema","loadSchema","validate","loadSchema","loadDocuments"]}
@@ -1,20 +0,0 @@
1
- type Query {
2
- allFoo: [Foo!]!
3
- foo(id: ID!): Foo
4
- }
5
-
6
- type Foo {
7
- id: ID!
8
- name: String
9
- bar: Bar
10
- }
11
-
12
- type Bar {
13
- baz: Baz
14
- }
15
-
16
- type Baz {
17
- dateCreated: DateTime!
18
- }
19
-
20
- scalar DateTime