Skip to content

Commit

Permalink
feat(core): ability to skip some specific validation rules with skipD…
Browse files Browse the repository at this point in the history
…ocumentValidation option (#6825)

* feat(core): ability to skip some specific validation rules with skipDocumentValidation option

* What if it is not boolean

* ...

* Update changeset

* Use memoize1 from utils for memoization

* Add note

* More refactor

* ...

* ...

* Fix dev-tests

* Add docs

* ...

* ...
  • Loading branch information
ardatan committed Oct 13, 2021
1 parent b9e85ad commit 7c60e5a
Show file tree
Hide file tree
Showing 7 changed files with 177 additions and 93 deletions.
6 changes: 6 additions & 0 deletions .changeset/mighty-numbers-ring.md
@@ -0,0 +1,6 @@
---
'@graphql-codegen/core': minor
'@graphql-codegen/plugin-helpers': minor
---

feat(core): ability to skip some specific validation rules with skipDocumentsValidation option
136 changes: 60 additions & 76 deletions packages/graphql-codegen-core/src/codegen.ts
Expand Up @@ -4,86 +4,88 @@ import {
isComplexPluginOutput,
federationSpec,
getCachedDocumentNodeFromSchema,
AddToSchemaResult,
} from '@graphql-codegen/plugin-helpers';
import { visit, DefinitionNode, Kind, print, NameNode } from 'graphql';
import { visit, DefinitionNode, Kind, print, NameNode, specifiedRules } from 'graphql';
import { executePlugin } from './execute-plugin';
import { checkValidationErrors, validateGraphQlDocuments, Source } from '@graphql-tools/utils';
import { checkValidationErrors, validateGraphQlDocuments, Source, asArray } from '@graphql-tools/utils';

import { mergeSchemas } from '@graphql-tools/schema';
import {
getSkipDocumentsValidationOption,
hasFederationSpec,
pickFlag,
prioritize,
shouldValidateDocumentsAgainstSchema,
shouldValidateDuplicateDocuments,
} from './utils';

export async function codegen(options: Types.GenerateOptions): Promise<string> {
const documents = options.documents || [];

if (documents.length > 0 && !options.skipDocumentsValidation) {
const skipDocumentsValidation = getSkipDocumentsValidationOption(options);

if (documents.length > 0 && shouldValidateDuplicateDocuments(skipDocumentsValidation)) {
validateDuplicateDocuments(documents);
}

const pluginPackages = Object.keys(options.pluginMap).map(key => options.pluginMap[key]);

if (!options.schemaAst) {
options.schemaAst = mergeSchemas({
schemas: [],
typeDefs: [options.schema],
convertExtensions: true,
assumeValid: true,
assumeValidSDL: true,
...options.config,
} as any);
}

// merged schema with parts added by plugins
let schemaChanged = false;
let schemaAst = pluginPackages.reduce((schemaAst, plugin) => {
const additionalTypeDefs: AddToSchemaResult[] = [];
for (const plugin of pluginPackages) {
const addToSchema =
typeof plugin.addToSchema === 'function' ? plugin.addToSchema(options.config) : plugin.addToSchema;

if (!addToSchema) {
return schemaAst;
if (addToSchema) {
additionalTypeDefs.push(addToSchema);
}
}

return mergeSchemas({
schemas: [schemaAst],
typeDefs: [addToSchema],
});
}, options.schemaAst);

const federationInConfig = pickFlag('federation', options.config);
const federationInConfig: boolean = pickFlag('federation', options.config);
const isFederation = prioritize(federationInConfig, false);

if (
isFederation &&
!schemaAst.getDirective('external') &&
!schemaAst.getDirective('requires') &&
!schemaAst.getDirective('provides') &&
!schemaAst.getDirective('key')
) {
schemaChanged = true;
schemaAst = mergeSchemas({
schemas: [schemaAst],
typeDefs: [federationSpec],
convertExtensions: true,
assumeValid: true,
assumeValidSDL: true,
} as any);
if (isFederation && !hasFederationSpec(options.schemaAst || options.schema)) {
additionalTypeDefs.push(federationSpec);
}

if (schemaChanged) {
options.schema = getCachedDocumentNodeFromSchema(schemaAst);
}

const skipDocumentValidation =
typeof options.config === 'object' && !Array.isArray(options.config) && options.config.skipDocumentsValidation;

if (options.schemaAst && documents.length > 0 && !skipDocumentValidation) {
// Use mergeSchemas, only if there is no GraphQLSchema provided or the schema should be extended
const mergeNeeded = !options.schemaAst || additionalTypeDefs.length > 0;

const schemaInstance = mergeNeeded
? mergeSchemas({
// If GraphQLSchema provided, use it
schemas: options.schemaAst ? [options.schemaAst] : [],
// If GraphQLSchema isn't provided but DocumentNode is, use it to get the final GraphQLSchema
typeDefs: options.schemaAst ? additionalTypeDefs : [options.schema, ...additionalTypeDefs],
convertExtensions: true,
assumeValid: true,
assumeValidSDL: true,
...options.config,
} as any)
: options.schemaAst;

const schemaDocumentNode =
mergeNeeded || !options.schema ? getCachedDocumentNodeFromSchema(schemaInstance) : options.schema;

if (schemaInstance && documents.length > 0 && shouldValidateDocumentsAgainstSchema(skipDocumentsValidation)) {
const ignored = ['NoUnusedFragments', 'NoUnusedVariables', 'KnownDirectives'];
if (typeof skipDocumentsValidation === 'object' && skipDocumentsValidation.ignoreRules) {
ignored.push(...asArray(skipDocumentsValidation.ignoreRules));
}
const extraFragments: { importFrom: string; node: DefinitionNode }[] =
options.config && (options.config as any).externalFragments ? (options.config as any).externalFragments : [];
const errors = await validateGraphQlDocuments(options.schemaAst, [
...documents,
...extraFragments.map(f => ({
location: f.importFrom,
document: { kind: Kind.DOCUMENT, definitions: [f.node] },
})),
]);
pickFlag('externalFragments', options.config) || [];
const errors = await validateGraphQlDocuments(
schemaInstance,
[
...documents,
...extraFragments.map(f => ({
location: f.importFrom,
document: { kind: Kind.DOCUMENT, definitions: [f.node] },
})),
],
specifiedRules.filter(rule => !ignored.some(ignoredRule => rule.name.startsWith(ignoredRule)))
);
checkValidationErrors(errors);
}

Expand All @@ -109,8 +111,8 @@ export async function codegen(options: Types.GenerateOptions): Promise<string> {
name,
config: execConfig,
parentConfig: options.config,
schema: options.schema,
schemaAst,
schema: schemaDocumentNode,
schemaAst: schemaInstance,
documents: options.documents,
outputFilename: options.filename,
allPlugins: options.plugins,
Expand Down Expand Up @@ -268,21 +270,3 @@ function validateDuplicateDocuments(files: Types.DocumentFile[]) {
}
});
}

function isObjectMap(obj: any): obj is Types.PluginConfig<any> {
return obj && typeof obj === 'object' && !Array.isArray(obj);
}

function prioritize<T>(...values: T[]): T {
const picked = values.find(val => typeof val === 'boolean');

if (typeof picked !== 'boolean') {
return values[values.length - 1];
}

return picked;
}

function pickFlag(flag: string, config: Types.PluginConfig): boolean | undefined {
return isObjectMap(config) ? (config as any)[flag] : undefined;
}
6 changes: 3 additions & 3 deletions packages/graphql-codegen-core/src/execute-plugin.ts
Expand Up @@ -10,7 +10,7 @@ export interface ExecutePluginOptions {
documents: Types.DocumentFile[];
outputFilename: string;
allPlugins: Types.ConfiguredPlugin[];
skipDocumentsValidation?: boolean;
skipDocumentsValidation?: Types.SkipDocumentsValidationOptions;
pluginContext?: { [key: string]: any };
}

Expand All @@ -20,9 +20,9 @@ export async function executePlugin(options: ExecutePluginOptions, plugin: Codeg
`Invalid Custom Plugin "${options.name}"`,
`
Plugin ${options.name} does not export a valid JS object with "plugin" function.
Make sure your custom plugin is written in the following form:
module.exports = {
plugin: (schema, documents, config) => {
return 'my-custom-plugin-content';
Expand Down
77 changes: 77 additions & 0 deletions packages/graphql-codegen-core/src/utils.ts
@@ -0,0 +1,77 @@
import { Types } from '@graphql-codegen/plugin-helpers';
import { isDocumentNode } from '@graphql-tools/utils';
import { DocumentNode, GraphQLSchema, isSchema, Kind } from 'graphql';

export function isObjectMap(obj: any): obj is Types.PluginConfig<any> {
return obj && typeof obj === 'object' && !Array.isArray(obj);
}

export function prioritize<T>(...values: T[]): T {
const picked = values.find(val => typeof val === 'boolean');

if (typeof picked !== 'boolean') {
return values[values.length - 1];
}

return picked;
}

export function pickFlag<TConfig, TKey extends keyof TConfig>(flag: TKey, config: TConfig): TConfig[TKey] | undefined {
return isObjectMap(config) ? config[flag] : undefined;
}

export function shouldValidateDuplicateDocuments(
skipDocumentsValidationOption: Types.GenerateOptions['skipDocumentsValidation']
) {
// If the value is true, skip all
if (skipDocumentsValidationOption === true) {
return false;
}
// If the value is object with the specific flag, only skip this one
if (typeof skipDocumentsValidationOption === 'object' && skipDocumentsValidationOption.skipDuplicateValidation) {
return false;
}
// If the value is falsy or the specific flag is not set, validate
return true;
}

export function shouldValidateDocumentsAgainstSchema(
skipDocumentsValidationOption: Types.GenerateOptions['skipDocumentsValidation']
) {
// If the value is true, skip all
if (skipDocumentsValidationOption === true) {
return false;
}
// If the value is object with the specific flag, only skip this one
if (typeof skipDocumentsValidationOption === 'object' && skipDocumentsValidationOption.skipValidationAgainstSchema) {
return false;
}
// If the value is falsy or the specific flag is not set, validate
return true;
}

export function getSkipDocumentsValidationOption(options: Types.GenerateOptions): Types.SkipDocumentsValidationOptions {
// If the value is set on the root level
if (options.skipDocumentsValidation) {
return options.skipDocumentsValidation;
}
// If the value is set under `config` property
const flagFromConfig: Types.SkipDocumentsValidationOptions = pickFlag('skipDocumentsValidation', options.config);
if (flagFromConfig) {
return flagFromConfig;
}
return false;
}

const federationDirectives = ['key', 'requires', 'provides', 'external'];

export function hasFederationSpec(schemaOrAST: GraphQLSchema | DocumentNode) {
if (isSchema(schemaOrAST)) {
return federationDirectives.some(directive => schemaOrAST.getDirective(directive));
} else if (isDocumentNode(schemaOrAST)) {
return schemaOrAST.definitions.some(
def => def.kind === Kind.DIRECTIVE_DEFINITION && federationDirectives.includes(def.name.value)
);
}
return false;
}
@@ -1,13 +1,3 @@
import { getDocumentNodeFromSchema } from '@graphql-tools/utils';
import { GraphQLSchema, DocumentNode } from 'graphql';
import { getDocumentNodeFromSchema, memoize1 } from '@graphql-tools/utils';

const schemaDocumentNodeCache = new WeakMap<GraphQLSchema, DocumentNode>();

export function getCachedDocumentNodeFromSchema(schema: GraphQLSchema) {
let documentNode = schemaDocumentNodeCache.get(schema);
if (!documentNode) {
documentNode = getDocumentNodeFromSchema(schema);
schemaDocumentNodeCache.set(schema, documentNode);
}
return documentNode;
}
export const getCachedDocumentNodeFromSchema = memoize1(getDocumentNodeFromSchema);
21 changes: 20 additions & 1 deletion packages/utils/plugins-helpers/src/types.ts
Expand Up @@ -5,14 +5,15 @@ export namespace Types {
export interface GenerateOptions {
filename: string;
plugins: Types.ConfiguredPlugin[];
// TODO: Remove schemaAst and change schema to GraphQLSchema in the next major version
schema: DocumentNode;
schemaAst?: GraphQLSchema;
documents: Types.DocumentFile[];
config: { [key: string]: any };
pluginMap: {
[name: string]: CodegenPlugin;
};
skipDocumentsValidation?: boolean;
skipDocumentsValidation?: Types.SkipDocumentsValidationOptions;
pluginContext?: { [key: string]: any };
}

Expand Down Expand Up @@ -535,6 +536,24 @@ export namespace Types {
*/
beforeAllFileWrite: T;
};

export type SkipDocumentsValidationOptions =
| {
/**
* @description Allows you to skip specific rules while validating the documents.
* See all the rules; https://github.com/graphql/graphql-js/tree/main/src/validation/rules
*/
ignoreRules?: string[];
/**
* @description Ignore duplicate documents validation
*/
skipDuplicateValidation?: boolean;
/**
* @description Skip document validation entirely against the schema
*/
skipValidationAgainstSchema?: boolean;
}
| boolean;
}

export function isComplexPluginOutput(obj: Types.PluginOutput): obj is Types.ComplexPluginOutput {
Expand Down
10 changes: 9 additions & 1 deletion website/docs/getting-started/codegen-config.md
Expand Up @@ -48,7 +48,7 @@ Here are the supported options that you can define in the config file (see [sour
- **`generates` (required)** - A map where the key represents an output path for the generated code and the value represents a set of options which are relevant for that specific file. Below are the possible options that can be specified:

- **`generates.plugins` (required)** - A list of plugins to use when generating the file. Templates are also considered as plugins and they can be specified in this section. A full list of supported plugins can be found [here](../plugins/index.md). You can also point to a custom plugin in a local file (see [Custom Plugins](../custom-codegen/index.md)).

- [**`generates.preset`**](../presets/index.md) - A list of available presets for generated files. Such as [`near-operation-file`](../presets/near-operation-file.md#example), which generates files alongside your documents.

- [**`generates.schema`**](schema-field.md#output-file-level) - Same as root `schema`, but applies only for the specific output file.
Expand Down Expand Up @@ -83,6 +83,14 @@ Here are the supported options that you can define in the config file (see [sour

- **`pluckConfig.globalGqlIdentifierName`** - Overrides the name of the default GraphQL name identifier.

- **`skipDocumentsValidation`** - Allows to configure how to validate documents

- **`skipDocumentsValidation.skipValidationAgainstSchema`** - A flag to disable the validation against the schema

- **`skipDocumentsValidation.ignoreRules`** - An array of rule names to ignore during the validation. You can find a list of the available rules [here](https://github.com/graphql/graphql-js/tree/main/src/validation/rules.

- **`skipDocumentsValidation.skipDuplicateValidation`** - A flag to disable the validation for duplicate documents

## Environment Variables

You can use environment variables in your `codegen.yml` file::
Expand Down

0 comments on commit 7c60e5a

Please sign in to comment.