@envelop/extended-validation 8.0.0-alpha-20260116142832-a2277336f0a5a4d64d168c98cf97b7513f70b9f7 → 8.0.0-alpha-20260116185633-bd914add1fcad0f37d83c1b72ae6d57e300e62c4
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/LICENSE +21 -0
- package/cjs/common.js +9 -0
- package/cjs/index.js +6 -0
- package/cjs/package.json +1 -0
- package/cjs/plugin.js +121 -0
- package/cjs/rules/one-of.js +75 -0
- package/esm/common.js +6 -0
- package/esm/index.js +3 -0
- package/esm/plugin.js +117 -0
- package/esm/rules/one-of.js +71 -0
- package/package.json +31 -51
- package/typings/common.d.cts +5 -0
- package/typings/common.d.ts +5 -0
- package/typings/index.d.cts +3 -0
- package/typings/index.d.ts +3 -0
- package/typings/plugin.d.cts +33 -0
- package/typings/plugin.d.ts +33 -0
- package/typings/rules/one-of.d.cts +3 -0
- package/typings/rules/one-of.d.ts +3 -0
- package/dist/common.cjs +0 -10
- package/dist/common.d.cts +0 -9
- package/dist/common.d.mts +0 -9
- package/dist/common.mjs +0 -9
- package/dist/index.cjs +0 -5
- package/dist/index.d.cts +0 -3
- package/dist/index.d.mts +0 -3
- package/dist/index.mjs +0 -4
- package/dist/plugin.cjs +0 -102
- package/dist/plugin.d.cts +0 -36
- package/dist/plugin.d.mts +0 -36
- package/dist/plugin.mjs +0 -102
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020 Dotan Simha
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/cjs/common.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getDirectiveFromAstNode = getDirectiveFromAstNode;
|
|
4
|
+
function getDirectiveFromAstNode(astNode, names) {
|
|
5
|
+
const directives = astNode.directives || [];
|
|
6
|
+
const namesArr = Array.isArray(names) ? names : [names];
|
|
7
|
+
const authDirective = directives.find(d => namesArr.includes(d.name.value));
|
|
8
|
+
return authDirective || null;
|
|
9
|
+
}
|
package/cjs/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tslib_1 = require("tslib");
|
|
4
|
+
tslib_1.__exportStar(require("./plugin.js"), exports);
|
|
5
|
+
tslib_1.__exportStar(require("./common.js"), exports);
|
|
6
|
+
tslib_1.__exportStar(require("./rules/one-of.js"), exports);
|
package/cjs/package.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"commonjs"}
|
package/cjs/plugin.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useExtendedValidation = void 0;
|
|
4
|
+
const graphql_1 = require("graphql");
|
|
5
|
+
const core_1 = require("@envelop/core");
|
|
6
|
+
const symbolExtendedValidationRules = Symbol('extendedValidationContext');
|
|
7
|
+
const useExtendedValidation = (options) => {
|
|
8
|
+
let schemaTypeInfo;
|
|
9
|
+
function getTypeInfo() {
|
|
10
|
+
return schemaTypeInfo;
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
onSchemaChange({ schema }) {
|
|
14
|
+
schemaTypeInfo = new graphql_1.TypeInfo(schema);
|
|
15
|
+
},
|
|
16
|
+
onContextBuilding({ context, extendContext }) {
|
|
17
|
+
// We initialize the validationRules context in onContextBuilding as onExecute is already too late!
|
|
18
|
+
let validationRulesContext = context[symbolExtendedValidationRules];
|
|
19
|
+
if (validationRulesContext === undefined) {
|
|
20
|
+
validationRulesContext = {
|
|
21
|
+
rules: [],
|
|
22
|
+
didRun: false,
|
|
23
|
+
};
|
|
24
|
+
extendContext({
|
|
25
|
+
...context,
|
|
26
|
+
[symbolExtendedValidationRules]: validationRulesContext,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
validationRulesContext.rules.push(...options.rules);
|
|
30
|
+
},
|
|
31
|
+
onSubscribe: buildHandler('subscribe', getTypeInfo, options.onDocument, options.onValidationFailed, options.rejectOnErrors !== false),
|
|
32
|
+
onExecute: buildHandler('execute', getTypeInfo, options.onDocument, options.onValidationFailed, options.rejectOnErrors !== false),
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
exports.useExtendedValidation = useExtendedValidation;
|
|
36
|
+
function buildHandler(name, getTypeInfo, onDocument, onValidationFailed, rejectOnErrors = true) {
|
|
37
|
+
return function handler({ args, setResultAndStopExecution, }) {
|
|
38
|
+
// We hook into onExecute/onSubscribe even though this is a validation pattern. The reasoning behind
|
|
39
|
+
// it is that hooking right after validation and before execution has started is the
|
|
40
|
+
// same as hooking into the validation step. The benefit of this approach is that
|
|
41
|
+
// we may use execution context in the validation rules.
|
|
42
|
+
const validationRulesContext = args.contextValue[symbolExtendedValidationRules];
|
|
43
|
+
if (validationRulesContext === undefined) {
|
|
44
|
+
throw new Error('Plugin has not been properly set up. ' +
|
|
45
|
+
`The 'contextFactory' function is not invoked and the result has not been passed to '${name}'.`);
|
|
46
|
+
}
|
|
47
|
+
// we only want to run the extended execution once.
|
|
48
|
+
if (validationRulesContext.didRun === false) {
|
|
49
|
+
validationRulesContext.didRun = true;
|
|
50
|
+
if (validationRulesContext.rules.length !== 0) {
|
|
51
|
+
const errors = [];
|
|
52
|
+
// We replicate the default validation step manually before execution starts.
|
|
53
|
+
const typeInfo = getTypeInfo() ?? new graphql_1.TypeInfo(args.schema);
|
|
54
|
+
const validationContext = new graphql_1.ValidationContext(args.schema, args.document, typeInfo, e => {
|
|
55
|
+
errors.push(e);
|
|
56
|
+
});
|
|
57
|
+
const visitor = (0, graphql_1.visitInParallel)(validationRulesContext.rules.map(rule => rule(validationContext, args)));
|
|
58
|
+
args.document = (0, graphql_1.visit)(args.document, (0, graphql_1.visitWithTypeInfo)(typeInfo, visitor));
|
|
59
|
+
if (onDocument) {
|
|
60
|
+
args.document = onDocument(args.document);
|
|
61
|
+
}
|
|
62
|
+
if (errors.length > 0) {
|
|
63
|
+
if (rejectOnErrors) {
|
|
64
|
+
let result = {
|
|
65
|
+
data: null,
|
|
66
|
+
errors,
|
|
67
|
+
};
|
|
68
|
+
if (onValidationFailed) {
|
|
69
|
+
onValidationFailed({ args, result, setResult: newResult => (result = newResult) });
|
|
70
|
+
}
|
|
71
|
+
setResultAndStopExecution(result);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
// eslint-disable-next-line no-inner-declarations
|
|
75
|
+
function onResult({ result, setResult, }) {
|
|
76
|
+
if ((0, core_1.isAsyncIterable)(result)) {
|
|
77
|
+
// rejectOnErrors is false doesn't work with async iterables
|
|
78
|
+
setResult({
|
|
79
|
+
data: null,
|
|
80
|
+
errors,
|
|
81
|
+
});
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const newResult = {
|
|
85
|
+
...result,
|
|
86
|
+
errors: [...(result.errors || []), ...errors],
|
|
87
|
+
};
|
|
88
|
+
function visitPath(path, data = {}) {
|
|
89
|
+
let currentData = (data ||= typeof path[0] === 'number' ? [] : {});
|
|
90
|
+
for (const pathItemIndex in path.slice(0, -1)) {
|
|
91
|
+
const pathItem = path[pathItemIndex];
|
|
92
|
+
currentData = currentData[pathItem] ||=
|
|
93
|
+
typeof path[Number(pathItemIndex) + 1] === 'number' ? [] : {};
|
|
94
|
+
if (Array.isArray(currentData)) {
|
|
95
|
+
let pathItemIndexInArray = Number(pathItemIndex) + 1;
|
|
96
|
+
if (path[pathItemIndexInArray] === '@') {
|
|
97
|
+
pathItemIndexInArray = Number(pathItemIndex) + 2;
|
|
98
|
+
}
|
|
99
|
+
currentData = currentData.map((c, i) => visitPath(path.slice(pathItemIndexInArray), c));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
currentData[path[path.length - 1]] = null;
|
|
103
|
+
return data;
|
|
104
|
+
}
|
|
105
|
+
errors.forEach(e => {
|
|
106
|
+
if (e.path?.length) {
|
|
107
|
+
newResult.data = visitPath(e.path, newResult.data);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
setResult(newResult);
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
onSubscribeResult: onResult,
|
|
114
|
+
onExecuteDone: onResult,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OneOfInputObjectsRule = exports.ONE_OF_DIRECTIVE_SDL = void 0;
|
|
4
|
+
const graphql_1 = require("graphql");
|
|
5
|
+
const utils_1 = require("@graphql-tools/utils");
|
|
6
|
+
const common_js_1 = require("../common.js");
|
|
7
|
+
exports.ONE_OF_DIRECTIVE_SDL = `
|
|
8
|
+
directive @oneOf on INPUT_OBJECT | FIELD_DEFINITION
|
|
9
|
+
`;
|
|
10
|
+
const OneOfInputObjectsRule = (validationContext, executionArgs) => {
|
|
11
|
+
return {
|
|
12
|
+
Field: node => {
|
|
13
|
+
if (node.arguments?.length) {
|
|
14
|
+
const fieldType = validationContext.getFieldDef();
|
|
15
|
+
if (!fieldType) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const values = (0, utils_1.getArgumentValues)(fieldType, node, executionArgs.variableValues || undefined);
|
|
19
|
+
const isOneOfFieldType = fieldType.extensions?.oneOf ||
|
|
20
|
+
(fieldType.astNode && (0, common_js_1.getDirectiveFromAstNode)(fieldType.astNode, 'oneOf'));
|
|
21
|
+
if (isOneOfFieldType && Object.keys(values).length !== 1) {
|
|
22
|
+
validationContext.reportError(new graphql_1.GraphQLError(`Exactly one key must be specified for input for field "${fieldType.type.toString()}.${node.name.value}"`, [node]));
|
|
23
|
+
}
|
|
24
|
+
for (const arg of node.arguments) {
|
|
25
|
+
const argType = fieldType.args.find(typeArg => typeArg.name === arg.name.value);
|
|
26
|
+
if (argType) {
|
|
27
|
+
traverseVariables(validationContext, arg, argType.type, values[arg.name.value]);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
exports.OneOfInputObjectsRule = OneOfInputObjectsRule;
|
|
35
|
+
function getNonNullType(ttype) {
|
|
36
|
+
if (ttype instanceof graphql_1.GraphQLNonNull) {
|
|
37
|
+
return ttype.ofType;
|
|
38
|
+
}
|
|
39
|
+
return ttype;
|
|
40
|
+
}
|
|
41
|
+
function traverseVariables(validationContext, arg, graphqlType, currentValue) {
|
|
42
|
+
// if the current value is empty we don't need to traverse deeper
|
|
43
|
+
// if it shouldn't be empty, the "original" validation phase should complain.
|
|
44
|
+
if (currentValue == null) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const unwrappedType = getNonNullType(graphqlType);
|
|
48
|
+
if ((0, graphql_1.isListType)(unwrappedType)) {
|
|
49
|
+
if (!Array.isArray(currentValue)) {
|
|
50
|
+
// because of graphql type coercion a single object should be treated as an array of one object
|
|
51
|
+
currentValue = [currentValue];
|
|
52
|
+
}
|
|
53
|
+
currentValue.forEach(value => {
|
|
54
|
+
traverseVariables(validationContext, arg, unwrappedType.ofType, value);
|
|
55
|
+
});
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (typeof currentValue !== 'object' || currentValue == null) {
|
|
59
|
+
// in case the value is not an object, the "original" validation phase should complain.
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const inputType = (0, graphql_1.getNamedType)(graphqlType);
|
|
63
|
+
const isOneOfInputType = inputType.extensions?.oneOf ||
|
|
64
|
+
(inputType.astNode && (0, common_js_1.getDirectiveFromAstNode)(inputType.astNode, 'oneOf'));
|
|
65
|
+
if (isOneOfInputType && Object.keys(currentValue).length !== 1) {
|
|
66
|
+
validationContext.reportError((0, utils_1.createGraphQLError)(`Exactly one key must be specified for input type "${inputType.name}"`, {
|
|
67
|
+
nodes: [arg],
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
if (inputType instanceof graphql_1.GraphQLInputObjectType) {
|
|
71
|
+
for (const [name, fieldConfig] of Object.entries(inputType.getFields())) {
|
|
72
|
+
traverseVariables(validationContext, arg, fieldConfig.type, currentValue[name]);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
package/esm/common.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export function getDirectiveFromAstNode(astNode, names) {
|
|
2
|
+
const directives = astNode.directives || [];
|
|
3
|
+
const namesArr = Array.isArray(names) ? names : [names];
|
|
4
|
+
const authDirective = directives.find(d => namesArr.includes(d.name.value));
|
|
5
|
+
return authDirective || null;
|
|
6
|
+
}
|
package/esm/index.js
ADDED
package/esm/plugin.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { TypeInfo, ValidationContext, visit, visitInParallel, visitWithTypeInfo, } from 'graphql';
|
|
2
|
+
import { isAsyncIterable, } from '@envelop/core';
|
|
3
|
+
const symbolExtendedValidationRules = Symbol('extendedValidationContext');
|
|
4
|
+
export const useExtendedValidation = (options) => {
|
|
5
|
+
let schemaTypeInfo;
|
|
6
|
+
function getTypeInfo() {
|
|
7
|
+
return schemaTypeInfo;
|
|
8
|
+
}
|
|
9
|
+
return {
|
|
10
|
+
onSchemaChange({ schema }) {
|
|
11
|
+
schemaTypeInfo = new TypeInfo(schema);
|
|
12
|
+
},
|
|
13
|
+
onContextBuilding({ context, extendContext }) {
|
|
14
|
+
// We initialize the validationRules context in onContextBuilding as onExecute is already too late!
|
|
15
|
+
let validationRulesContext = context[symbolExtendedValidationRules];
|
|
16
|
+
if (validationRulesContext === undefined) {
|
|
17
|
+
validationRulesContext = {
|
|
18
|
+
rules: [],
|
|
19
|
+
didRun: false,
|
|
20
|
+
};
|
|
21
|
+
extendContext({
|
|
22
|
+
...context,
|
|
23
|
+
[symbolExtendedValidationRules]: validationRulesContext,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
validationRulesContext.rules.push(...options.rules);
|
|
27
|
+
},
|
|
28
|
+
onSubscribe: buildHandler('subscribe', getTypeInfo, options.onDocument, options.onValidationFailed, options.rejectOnErrors !== false),
|
|
29
|
+
onExecute: buildHandler('execute', getTypeInfo, options.onDocument, options.onValidationFailed, options.rejectOnErrors !== false),
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
function buildHandler(name, getTypeInfo, onDocument, onValidationFailed, rejectOnErrors = true) {
|
|
33
|
+
return function handler({ args, setResultAndStopExecution, }) {
|
|
34
|
+
// We hook into onExecute/onSubscribe even though this is a validation pattern. The reasoning behind
|
|
35
|
+
// it is that hooking right after validation and before execution has started is the
|
|
36
|
+
// same as hooking into the validation step. The benefit of this approach is that
|
|
37
|
+
// we may use execution context in the validation rules.
|
|
38
|
+
const validationRulesContext = args.contextValue[symbolExtendedValidationRules];
|
|
39
|
+
if (validationRulesContext === undefined) {
|
|
40
|
+
throw new Error('Plugin has not been properly set up. ' +
|
|
41
|
+
`The 'contextFactory' function is not invoked and the result has not been passed to '${name}'.`);
|
|
42
|
+
}
|
|
43
|
+
// we only want to run the extended execution once.
|
|
44
|
+
if (validationRulesContext.didRun === false) {
|
|
45
|
+
validationRulesContext.didRun = true;
|
|
46
|
+
if (validationRulesContext.rules.length !== 0) {
|
|
47
|
+
const errors = [];
|
|
48
|
+
// We replicate the default validation step manually before execution starts.
|
|
49
|
+
const typeInfo = getTypeInfo() ?? new TypeInfo(args.schema);
|
|
50
|
+
const validationContext = new ValidationContext(args.schema, args.document, typeInfo, e => {
|
|
51
|
+
errors.push(e);
|
|
52
|
+
});
|
|
53
|
+
const visitor = visitInParallel(validationRulesContext.rules.map(rule => rule(validationContext, args)));
|
|
54
|
+
args.document = visit(args.document, visitWithTypeInfo(typeInfo, visitor));
|
|
55
|
+
if (onDocument) {
|
|
56
|
+
args.document = onDocument(args.document);
|
|
57
|
+
}
|
|
58
|
+
if (errors.length > 0) {
|
|
59
|
+
if (rejectOnErrors) {
|
|
60
|
+
let result = {
|
|
61
|
+
data: null,
|
|
62
|
+
errors,
|
|
63
|
+
};
|
|
64
|
+
if (onValidationFailed) {
|
|
65
|
+
onValidationFailed({ args, result, setResult: newResult => (result = newResult) });
|
|
66
|
+
}
|
|
67
|
+
setResultAndStopExecution(result);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
// eslint-disable-next-line no-inner-declarations
|
|
71
|
+
function onResult({ result, setResult, }) {
|
|
72
|
+
if (isAsyncIterable(result)) {
|
|
73
|
+
// rejectOnErrors is false doesn't work with async iterables
|
|
74
|
+
setResult({
|
|
75
|
+
data: null,
|
|
76
|
+
errors,
|
|
77
|
+
});
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const newResult = {
|
|
81
|
+
...result,
|
|
82
|
+
errors: [...(result.errors || []), ...errors],
|
|
83
|
+
};
|
|
84
|
+
function visitPath(path, data = {}) {
|
|
85
|
+
let currentData = (data ||= typeof path[0] === 'number' ? [] : {});
|
|
86
|
+
for (const pathItemIndex in path.slice(0, -1)) {
|
|
87
|
+
const pathItem = path[pathItemIndex];
|
|
88
|
+
currentData = currentData[pathItem] ||=
|
|
89
|
+
typeof path[Number(pathItemIndex) + 1] === 'number' ? [] : {};
|
|
90
|
+
if (Array.isArray(currentData)) {
|
|
91
|
+
let pathItemIndexInArray = Number(pathItemIndex) + 1;
|
|
92
|
+
if (path[pathItemIndexInArray] === '@') {
|
|
93
|
+
pathItemIndexInArray = Number(pathItemIndex) + 2;
|
|
94
|
+
}
|
|
95
|
+
currentData = currentData.map((c, i) => visitPath(path.slice(pathItemIndexInArray), c));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
currentData[path[path.length - 1]] = null;
|
|
99
|
+
return data;
|
|
100
|
+
}
|
|
101
|
+
errors.forEach(e => {
|
|
102
|
+
if (e.path?.length) {
|
|
103
|
+
newResult.data = visitPath(e.path, newResult.data);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
setResult(newResult);
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
onSubscribeResult: onResult,
|
|
110
|
+
onExecuteDone: onResult,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { getNamedType, GraphQLError, GraphQLInputObjectType, GraphQLNonNull, isListType, } from 'graphql';
|
|
2
|
+
import { createGraphQLError, getArgumentValues } from '@graphql-tools/utils';
|
|
3
|
+
import { getDirectiveFromAstNode } from '../common.js';
|
|
4
|
+
export const ONE_OF_DIRECTIVE_SDL = /* GraphQL */ `
|
|
5
|
+
directive @oneOf on INPUT_OBJECT | FIELD_DEFINITION
|
|
6
|
+
`;
|
|
7
|
+
export const OneOfInputObjectsRule = (validationContext, executionArgs) => {
|
|
8
|
+
return {
|
|
9
|
+
Field: node => {
|
|
10
|
+
if (node.arguments?.length) {
|
|
11
|
+
const fieldType = validationContext.getFieldDef();
|
|
12
|
+
if (!fieldType) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const values = getArgumentValues(fieldType, node, executionArgs.variableValues || undefined);
|
|
16
|
+
const isOneOfFieldType = fieldType.extensions?.oneOf ||
|
|
17
|
+
(fieldType.astNode && getDirectiveFromAstNode(fieldType.astNode, 'oneOf'));
|
|
18
|
+
if (isOneOfFieldType && Object.keys(values).length !== 1) {
|
|
19
|
+
validationContext.reportError(new GraphQLError(`Exactly one key must be specified for input for field "${fieldType.type.toString()}.${node.name.value}"`, [node]));
|
|
20
|
+
}
|
|
21
|
+
for (const arg of node.arguments) {
|
|
22
|
+
const argType = fieldType.args.find(typeArg => typeArg.name === arg.name.value);
|
|
23
|
+
if (argType) {
|
|
24
|
+
traverseVariables(validationContext, arg, argType.type, values[arg.name.value]);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
function getNonNullType(ttype) {
|
|
32
|
+
if (ttype instanceof GraphQLNonNull) {
|
|
33
|
+
return ttype.ofType;
|
|
34
|
+
}
|
|
35
|
+
return ttype;
|
|
36
|
+
}
|
|
37
|
+
function traverseVariables(validationContext, arg, graphqlType, currentValue) {
|
|
38
|
+
// if the current value is empty we don't need to traverse deeper
|
|
39
|
+
// if it shouldn't be empty, the "original" validation phase should complain.
|
|
40
|
+
if (currentValue == null) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const unwrappedType = getNonNullType(graphqlType);
|
|
44
|
+
if (isListType(unwrappedType)) {
|
|
45
|
+
if (!Array.isArray(currentValue)) {
|
|
46
|
+
// because of graphql type coercion a single object should be treated as an array of one object
|
|
47
|
+
currentValue = [currentValue];
|
|
48
|
+
}
|
|
49
|
+
currentValue.forEach(value => {
|
|
50
|
+
traverseVariables(validationContext, arg, unwrappedType.ofType, value);
|
|
51
|
+
});
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (typeof currentValue !== 'object' || currentValue == null) {
|
|
55
|
+
// in case the value is not an object, the "original" validation phase should complain.
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const inputType = getNamedType(graphqlType);
|
|
59
|
+
const isOneOfInputType = inputType.extensions?.oneOf ||
|
|
60
|
+
(inputType.astNode && getDirectiveFromAstNode(inputType.astNode, 'oneOf'));
|
|
61
|
+
if (isOneOfInputType && Object.keys(currentValue).length !== 1) {
|
|
62
|
+
validationContext.reportError(createGraphQLError(`Exactly one key must be specified for input type "${inputType.name}"`, {
|
|
63
|
+
nodes: [arg],
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
if (inputType instanceof GraphQLInputObjectType) {
|
|
67
|
+
for (const [name, fieldConfig] of Object.entries(inputType.getFields())) {
|
|
68
|
+
traverseVariables(validationContext, arg, fieldConfig.type, currentValue[name]);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@envelop/extended-validation",
|
|
3
|
-
"version": "8.0.0-alpha-
|
|
4
|
-
"
|
|
3
|
+
"version": "8.0.0-alpha-20260116185633-bd914add1fcad0f37d83c1b72ae6d57e300e62c4",
|
|
4
|
+
"sideEffects": false,
|
|
5
|
+
"peerDependencies": {
|
|
6
|
+
"graphql": "^14.0.0 || ^15.0.0 || ^16.0.0",
|
|
7
|
+
"@envelop/core": "^5.5.0-alpha-20260116185633-bd914add1fcad0f37d83c1b72ae6d57e300e62c4"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"@graphql-tools/utils": "^11.0.0",
|
|
11
|
+
"tslib": "^2.5.0"
|
|
12
|
+
},
|
|
5
13
|
"repository": {
|
|
6
14
|
"type": "git",
|
|
7
|
-
"url": "https://github.com/graphql-hive/
|
|
15
|
+
"url": "https://github.com/graphql-hive/envelop.git",
|
|
8
16
|
"directory": "packages/plugins/extended-validation"
|
|
9
17
|
},
|
|
10
18
|
"author": "Dotan Simha <dotansimha@gmail.com>",
|
|
@@ -12,70 +20,42 @@
|
|
|
12
20
|
"engines": {
|
|
13
21
|
"node": ">=18.0.0"
|
|
14
22
|
},
|
|
15
|
-
"main": "
|
|
16
|
-
"
|
|
17
|
-
"
|
|
23
|
+
"main": "cjs/index.js",
|
|
24
|
+
"module": "esm/index.js",
|
|
25
|
+
"typings": "typings/index.d.ts",
|
|
26
|
+
"typescript": {
|
|
27
|
+
"definition": "typings/index.d.ts"
|
|
28
|
+
},
|
|
29
|
+
"type": "module",
|
|
18
30
|
"exports": {
|
|
19
31
|
".": {
|
|
20
32
|
"require": {
|
|
21
|
-
"types": "./
|
|
22
|
-
"default": "./
|
|
33
|
+
"types": "./typings/index.d.cts",
|
|
34
|
+
"default": "./cjs/index.js"
|
|
23
35
|
},
|
|
24
36
|
"import": {
|
|
25
|
-
"types": "./
|
|
26
|
-
"default": "./
|
|
37
|
+
"types": "./typings/index.d.ts",
|
|
38
|
+
"default": "./esm/index.js"
|
|
27
39
|
},
|
|
28
40
|
"default": {
|
|
29
|
-
"types": "./
|
|
30
|
-
"default": "./
|
|
41
|
+
"types": "./typings/index.d.ts",
|
|
42
|
+
"default": "./esm/index.js"
|
|
31
43
|
}
|
|
32
44
|
},
|
|
33
45
|
"./*": {
|
|
34
46
|
"require": {
|
|
35
|
-
"types": "./
|
|
36
|
-
"default": "./
|
|
47
|
+
"types": "./typings/*.d.cts",
|
|
48
|
+
"default": "./cjs/*.js"
|
|
37
49
|
},
|
|
38
50
|
"import": {
|
|
39
|
-
"types": "./
|
|
40
|
-
"default": "./
|
|
51
|
+
"types": "./typings/*.d.ts",
|
|
52
|
+
"default": "./esm/*.js"
|
|
41
53
|
},
|
|
42
54
|
"default": {
|
|
43
|
-
"types": "./
|
|
44
|
-
"default": "./
|
|
55
|
+
"types": "./typings/*.d.ts",
|
|
56
|
+
"default": "./esm/*.js"
|
|
45
57
|
}
|
|
46
58
|
},
|
|
47
59
|
"./package.json": "./package.json"
|
|
48
|
-
},
|
|
49
|
-
"files": [
|
|
50
|
-
"dist"
|
|
51
|
-
],
|
|
52
|
-
"scripts": {
|
|
53
|
-
"build": "tsdown",
|
|
54
|
-
"check": "tsc --pretty --noEmit"
|
|
55
|
-
},
|
|
56
|
-
"peerDependencies": {
|
|
57
|
-
"@envelop/core": "workspace:^",
|
|
58
|
-
"graphql": "^14.0.0 || ^15.0.0 || ^16.0.0"
|
|
59
|
-
},
|
|
60
|
-
"dependencies": {
|
|
61
|
-
"@graphql-tools/utils": "^10.0.0",
|
|
62
|
-
"tslib": "^2.5.0"
|
|
63
|
-
},
|
|
64
|
-
"devDependencies": {
|
|
65
|
-
"@envelop/core": "workspace:^",
|
|
66
|
-
"@graphql-tools/schema": "10.0.30",
|
|
67
|
-
"graphql": "16.12.0",
|
|
68
|
-
"tsdown": "^0.20.0-beta.1",
|
|
69
|
-
"typescript": "5.9.3"
|
|
70
|
-
},
|
|
71
|
-
"publishConfig": {
|
|
72
|
-
"access": "public"
|
|
73
|
-
},
|
|
74
|
-
"sideEffects": false,
|
|
75
|
-
"buildOptions": {
|
|
76
|
-
"input": "./src/index.ts"
|
|
77
|
-
},
|
|
78
|
-
"typescript": {
|
|
79
|
-
"definition": "dist/index.d.mts"
|
|
80
60
|
}
|
|
81
|
-
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { ASTVisitor, DirectiveNode, ExecutionArgs, ValidationContext } from 'graphql';
|
|
2
|
+
export type ExtendedValidationRule = (context: ValidationContext, executionArgs: ExecutionArgs) => ASTVisitor;
|
|
3
|
+
export declare function getDirectiveFromAstNode(astNode: {
|
|
4
|
+
directives?: ReadonlyArray<DirectiveNode>;
|
|
5
|
+
}, names: string | string[]): null | DirectiveNode;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { ASTVisitor, DirectiveNode, ExecutionArgs, ValidationContext } from 'graphql';
|
|
2
|
+
export type ExtendedValidationRule = (context: ValidationContext, executionArgs: ExecutionArgs) => ASTVisitor;
|
|
3
|
+
export declare function getDirectiveFromAstNode(astNode: {
|
|
4
|
+
directives?: ReadonlyArray<DirectiveNode>;
|
|
5
|
+
}, names: string | string[]): null | DirectiveNode;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { DocumentNode, ExecutionArgs, ExecutionResult } from 'graphql';
|
|
2
|
+
import { Plugin } from '@envelop/core';
|
|
3
|
+
import { ExtendedValidationRule } from './common.cjs';
|
|
4
|
+
declare const symbolExtendedValidationRules: unique symbol;
|
|
5
|
+
type ExtendedValidationContext = {
|
|
6
|
+
rules: Array<ExtendedValidationRule>;
|
|
7
|
+
didRun: boolean;
|
|
8
|
+
};
|
|
9
|
+
type OnValidationFailedCallback = (params: {
|
|
10
|
+
args: ExecutionArgs;
|
|
11
|
+
result: ExecutionResult;
|
|
12
|
+
setResult: (result: ExecutionResult) => void;
|
|
13
|
+
}) => void;
|
|
14
|
+
export declare const useExtendedValidation: <PluginContext extends Record<string, any> = {}>(options: {
|
|
15
|
+
rules: Array<ExtendedValidationRule>;
|
|
16
|
+
/**
|
|
17
|
+
* Callback that is invoked when the document is assembled by the visitor.
|
|
18
|
+
*/
|
|
19
|
+
onDocument?: (document: DocumentNode) => DocumentNode;
|
|
20
|
+
/**
|
|
21
|
+
* Callback that is invoked if the extended validation yields any errors.
|
|
22
|
+
*/
|
|
23
|
+
onValidationFailed?: OnValidationFailedCallback;
|
|
24
|
+
/**
|
|
25
|
+
* Reject the execution if the validation fails.
|
|
26
|
+
*
|
|
27
|
+
* @default true
|
|
28
|
+
*/
|
|
29
|
+
rejectOnErrors?: boolean;
|
|
30
|
+
}) => Plugin<PluginContext & {
|
|
31
|
+
[symbolExtendedValidationRules]?: ExtendedValidationContext;
|
|
32
|
+
}>;
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { DocumentNode, ExecutionArgs, ExecutionResult } from 'graphql';
|
|
2
|
+
import { Plugin } from '@envelop/core';
|
|
3
|
+
import { ExtendedValidationRule } from './common.js';
|
|
4
|
+
declare const symbolExtendedValidationRules: unique symbol;
|
|
5
|
+
type ExtendedValidationContext = {
|
|
6
|
+
rules: Array<ExtendedValidationRule>;
|
|
7
|
+
didRun: boolean;
|
|
8
|
+
};
|
|
9
|
+
type OnValidationFailedCallback = (params: {
|
|
10
|
+
args: ExecutionArgs;
|
|
11
|
+
result: ExecutionResult;
|
|
12
|
+
setResult: (result: ExecutionResult) => void;
|
|
13
|
+
}) => void;
|
|
14
|
+
export declare const useExtendedValidation: <PluginContext extends Record<string, any> = {}>(options: {
|
|
15
|
+
rules: Array<ExtendedValidationRule>;
|
|
16
|
+
/**
|
|
17
|
+
* Callback that is invoked when the document is assembled by the visitor.
|
|
18
|
+
*/
|
|
19
|
+
onDocument?: (document: DocumentNode) => DocumentNode;
|
|
20
|
+
/**
|
|
21
|
+
* Callback that is invoked if the extended validation yields any errors.
|
|
22
|
+
*/
|
|
23
|
+
onValidationFailed?: OnValidationFailedCallback;
|
|
24
|
+
/**
|
|
25
|
+
* Reject the execution if the validation fails.
|
|
26
|
+
*
|
|
27
|
+
* @default true
|
|
28
|
+
*/
|
|
29
|
+
rejectOnErrors?: boolean;
|
|
30
|
+
}) => Plugin<PluginContext & {
|
|
31
|
+
[symbolExtendedValidationRules]?: ExtendedValidationContext;
|
|
32
|
+
}>;
|
|
33
|
+
export {};
|
package/dist/common.cjs
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
//#region src/common.ts
|
|
3
|
-
function getDirectiveFromAstNode(astNode, names) {
|
|
4
|
-
const directives = astNode.directives || [];
|
|
5
|
-
const namesArr = Array.isArray(names) ? names : [names];
|
|
6
|
-
return directives.find((d) => namesArr.includes(d.name.value)) || null;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
//#endregion
|
|
10
|
-
exports.getDirectiveFromAstNode = getDirectiveFromAstNode;
|
package/dist/common.d.cts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { ASTVisitor, DirectiveNode, ExecutionArgs, ValidationContext } from "graphql";
|
|
2
|
-
|
|
3
|
-
//#region src/common.d.ts
|
|
4
|
-
type ExtendedValidationRule = (context: ValidationContext, executionArgs: ExecutionArgs) => ASTVisitor;
|
|
5
|
-
declare function getDirectiveFromAstNode(astNode: {
|
|
6
|
-
directives?: ReadonlyArray<DirectiveNode>;
|
|
7
|
-
}, names: string | string[]): null | DirectiveNode;
|
|
8
|
-
//#endregion
|
|
9
|
-
export { ExtendedValidationRule, getDirectiveFromAstNode };
|
package/dist/common.d.mts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { ASTVisitor, DirectiveNode, ExecutionArgs, ValidationContext } from "graphql";
|
|
2
|
-
|
|
3
|
-
//#region src/common.d.ts
|
|
4
|
-
type ExtendedValidationRule = (context: ValidationContext, executionArgs: ExecutionArgs) => ASTVisitor;
|
|
5
|
-
declare function getDirectiveFromAstNode(astNode: {
|
|
6
|
-
directives?: ReadonlyArray<DirectiveNode>;
|
|
7
|
-
}, names: string | string[]): null | DirectiveNode;
|
|
8
|
-
//#endregion
|
|
9
|
-
export { ExtendedValidationRule, getDirectiveFromAstNode };
|
package/dist/common.mjs
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
//#region src/common.ts
|
|
2
|
-
function getDirectiveFromAstNode(astNode, names) {
|
|
3
|
-
const directives = astNode.directives || [];
|
|
4
|
-
const namesArr = Array.isArray(names) ? names : [names];
|
|
5
|
-
return directives.find((d) => namesArr.includes(d.name.value)) || null;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
//#endregion
|
|
9
|
-
export { getDirectiveFromAstNode };
|
package/dist/index.cjs
DELETED
package/dist/index.d.cts
DELETED
package/dist/index.d.mts
DELETED
package/dist/index.mjs
DELETED
package/dist/plugin.cjs
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
let graphql = require("graphql");
|
|
2
|
-
let _envelop_core = require("@envelop/core");
|
|
3
|
-
|
|
4
|
-
//#region src/plugin.ts
|
|
5
|
-
const symbolExtendedValidationRules = Symbol("extendedValidationContext");
|
|
6
|
-
const useExtendedValidation = (options) => {
|
|
7
|
-
let schemaTypeInfo;
|
|
8
|
-
function getTypeInfo() {
|
|
9
|
-
return schemaTypeInfo;
|
|
10
|
-
}
|
|
11
|
-
return {
|
|
12
|
-
onSchemaChange({ schema }) {
|
|
13
|
-
schemaTypeInfo = new graphql.TypeInfo(schema);
|
|
14
|
-
},
|
|
15
|
-
onContextBuilding({ context, extendContext }) {
|
|
16
|
-
let validationRulesContext = context[symbolExtendedValidationRules];
|
|
17
|
-
if (validationRulesContext === void 0) {
|
|
18
|
-
validationRulesContext = {
|
|
19
|
-
rules: [],
|
|
20
|
-
didRun: false
|
|
21
|
-
};
|
|
22
|
-
extendContext({
|
|
23
|
-
...context,
|
|
24
|
-
[symbolExtendedValidationRules]: validationRulesContext
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
validationRulesContext.rules.push(...options.rules);
|
|
28
|
-
},
|
|
29
|
-
onSubscribe: buildHandler("subscribe", getTypeInfo, options.onDocument, options.onValidationFailed, options.rejectOnErrors !== false),
|
|
30
|
-
onExecute: buildHandler("execute", getTypeInfo, options.onDocument, options.onValidationFailed, options.rejectOnErrors !== false)
|
|
31
|
-
};
|
|
32
|
-
};
|
|
33
|
-
function buildHandler(name, getTypeInfo, onDocument, onValidationFailed, rejectOnErrors = true) {
|
|
34
|
-
return function handler({ args, setResultAndStopExecution }) {
|
|
35
|
-
const validationRulesContext = args.contextValue[symbolExtendedValidationRules];
|
|
36
|
-
if (validationRulesContext === void 0) throw new Error(`Plugin has not been properly set up. The 'contextFactory' function is not invoked and the result has not been passed to '${name}'.`);
|
|
37
|
-
if (validationRulesContext.didRun === false) {
|
|
38
|
-
validationRulesContext.didRun = true;
|
|
39
|
-
if (validationRulesContext.rules.length !== 0) {
|
|
40
|
-
const errors = [];
|
|
41
|
-
const typeInfo = getTypeInfo() ?? new graphql.TypeInfo(args.schema);
|
|
42
|
-
const validationContext = new graphql.ValidationContext(args.schema, args.document, typeInfo, (e) => {
|
|
43
|
-
errors.push(e);
|
|
44
|
-
});
|
|
45
|
-
const visitor = (0, graphql.visitInParallel)(validationRulesContext.rules.map((rule) => rule(validationContext, args)));
|
|
46
|
-
args.document = (0, graphql.visit)(args.document, (0, graphql.visitWithTypeInfo)(typeInfo, visitor));
|
|
47
|
-
if (onDocument) args.document = onDocument(args.document);
|
|
48
|
-
if (errors.length > 0) if (rejectOnErrors) {
|
|
49
|
-
let result = {
|
|
50
|
-
data: null,
|
|
51
|
-
errors
|
|
52
|
-
};
|
|
53
|
-
if (onValidationFailed) onValidationFailed({
|
|
54
|
-
args,
|
|
55
|
-
result,
|
|
56
|
-
setResult: (newResult) => result = newResult
|
|
57
|
-
});
|
|
58
|
-
setResultAndStopExecution(result);
|
|
59
|
-
} else {
|
|
60
|
-
function onResult({ result, setResult }) {
|
|
61
|
-
if ((0, _envelop_core.isAsyncIterable)(result)) {
|
|
62
|
-
setResult({
|
|
63
|
-
data: null,
|
|
64
|
-
errors
|
|
65
|
-
});
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
const newResult = {
|
|
69
|
-
...result,
|
|
70
|
-
errors: [...result.errors || [], ...errors]
|
|
71
|
-
};
|
|
72
|
-
function visitPath(path, data = {}) {
|
|
73
|
-
let currentData = data ||= typeof path[0] === "number" ? [] : {};
|
|
74
|
-
for (const pathItemIndex in path.slice(0, -1)) {
|
|
75
|
-
const pathItem = path[pathItemIndex];
|
|
76
|
-
currentData = currentData[pathItem] ||= typeof path[Number(pathItemIndex) + 1] === "number" ? [] : {};
|
|
77
|
-
if (Array.isArray(currentData)) {
|
|
78
|
-
let pathItemIndexInArray = Number(pathItemIndex) + 1;
|
|
79
|
-
if (path[pathItemIndexInArray] === "@") pathItemIndexInArray = Number(pathItemIndex) + 2;
|
|
80
|
-
currentData = currentData.map((c) => visitPath(path.slice(pathItemIndexInArray), c));
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
currentData[path[path.length - 1]] = null;
|
|
84
|
-
return data;
|
|
85
|
-
}
|
|
86
|
-
errors.forEach((e) => {
|
|
87
|
-
if (e.path?.length) newResult.data = visitPath(e.path, newResult.data);
|
|
88
|
-
});
|
|
89
|
-
setResult(newResult);
|
|
90
|
-
}
|
|
91
|
-
return {
|
|
92
|
-
onSubscribeResult: onResult,
|
|
93
|
-
onExecuteDone: onResult
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
//#endregion
|
|
102
|
-
exports.useExtendedValidation = useExtendedValidation;
|
package/dist/plugin.d.cts
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { ExtendedValidationRule } from "./common.cjs";
|
|
2
|
-
import { DocumentNode, ExecutionArgs, ExecutionResult } from "graphql";
|
|
3
|
-
import { Plugin } from "@envelop/core";
|
|
4
|
-
|
|
5
|
-
//#region src/plugin.d.ts
|
|
6
|
-
declare const symbolExtendedValidationRules: unique symbol;
|
|
7
|
-
type ExtendedValidationContext = {
|
|
8
|
-
rules: Array<ExtendedValidationRule>;
|
|
9
|
-
didRun: boolean;
|
|
10
|
-
};
|
|
11
|
-
type OnValidationFailedCallback = (params: {
|
|
12
|
-
args: ExecutionArgs;
|
|
13
|
-
result: ExecutionResult;
|
|
14
|
-
setResult: (result: ExecutionResult) => void;
|
|
15
|
-
}) => void;
|
|
16
|
-
declare const useExtendedValidation: <PluginContext extends Record<string, any> = {}>(options: {
|
|
17
|
-
rules: Array<ExtendedValidationRule>;
|
|
18
|
-
/**
|
|
19
|
-
* Callback that is invoked when the document is assembled by the visitor.
|
|
20
|
-
*/
|
|
21
|
-
onDocument?: (document: DocumentNode) => DocumentNode;
|
|
22
|
-
/**
|
|
23
|
-
* Callback that is invoked if the extended validation yields any errors.
|
|
24
|
-
*/
|
|
25
|
-
onValidationFailed?: OnValidationFailedCallback;
|
|
26
|
-
/**
|
|
27
|
-
* Reject the execution if the validation fails.
|
|
28
|
-
*
|
|
29
|
-
* @default true
|
|
30
|
-
*/
|
|
31
|
-
rejectOnErrors?: boolean;
|
|
32
|
-
}) => Plugin<PluginContext & {
|
|
33
|
-
[symbolExtendedValidationRules]?: ExtendedValidationContext;
|
|
34
|
-
}>;
|
|
35
|
-
//#endregion
|
|
36
|
-
export { useExtendedValidation };
|
package/dist/plugin.d.mts
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { ExtendedValidationRule } from "./common.mjs";
|
|
2
|
-
import { DocumentNode, ExecutionArgs, ExecutionResult } from "graphql";
|
|
3
|
-
import { Plugin } from "@envelop/core";
|
|
4
|
-
|
|
5
|
-
//#region src/plugin.d.ts
|
|
6
|
-
declare const symbolExtendedValidationRules: unique symbol;
|
|
7
|
-
type ExtendedValidationContext = {
|
|
8
|
-
rules: Array<ExtendedValidationRule>;
|
|
9
|
-
didRun: boolean;
|
|
10
|
-
};
|
|
11
|
-
type OnValidationFailedCallback = (params: {
|
|
12
|
-
args: ExecutionArgs;
|
|
13
|
-
result: ExecutionResult;
|
|
14
|
-
setResult: (result: ExecutionResult) => void;
|
|
15
|
-
}) => void;
|
|
16
|
-
declare const useExtendedValidation: <PluginContext extends Record<string, any> = {}>(options: {
|
|
17
|
-
rules: Array<ExtendedValidationRule>;
|
|
18
|
-
/**
|
|
19
|
-
* Callback that is invoked when the document is assembled by the visitor.
|
|
20
|
-
*/
|
|
21
|
-
onDocument?: (document: DocumentNode) => DocumentNode;
|
|
22
|
-
/**
|
|
23
|
-
* Callback that is invoked if the extended validation yields any errors.
|
|
24
|
-
*/
|
|
25
|
-
onValidationFailed?: OnValidationFailedCallback;
|
|
26
|
-
/**
|
|
27
|
-
* Reject the execution if the validation fails.
|
|
28
|
-
*
|
|
29
|
-
* @default true
|
|
30
|
-
*/
|
|
31
|
-
rejectOnErrors?: boolean;
|
|
32
|
-
}) => Plugin<PluginContext & {
|
|
33
|
-
[symbolExtendedValidationRules]?: ExtendedValidationContext;
|
|
34
|
-
}>;
|
|
35
|
-
//#endregion
|
|
36
|
-
export { useExtendedValidation };
|
package/dist/plugin.mjs
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import { TypeInfo, ValidationContext, visit, visitInParallel, visitWithTypeInfo } from "graphql";
|
|
2
|
-
import { isAsyncIterable } from "@envelop/core";
|
|
3
|
-
|
|
4
|
-
//#region src/plugin.ts
|
|
5
|
-
const symbolExtendedValidationRules = Symbol("extendedValidationContext");
|
|
6
|
-
const useExtendedValidation = (options) => {
|
|
7
|
-
let schemaTypeInfo;
|
|
8
|
-
function getTypeInfo() {
|
|
9
|
-
return schemaTypeInfo;
|
|
10
|
-
}
|
|
11
|
-
return {
|
|
12
|
-
onSchemaChange({ schema }) {
|
|
13
|
-
schemaTypeInfo = new TypeInfo(schema);
|
|
14
|
-
},
|
|
15
|
-
onContextBuilding({ context, extendContext }) {
|
|
16
|
-
let validationRulesContext = context[symbolExtendedValidationRules];
|
|
17
|
-
if (validationRulesContext === void 0) {
|
|
18
|
-
validationRulesContext = {
|
|
19
|
-
rules: [],
|
|
20
|
-
didRun: false
|
|
21
|
-
};
|
|
22
|
-
extendContext({
|
|
23
|
-
...context,
|
|
24
|
-
[symbolExtendedValidationRules]: validationRulesContext
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
validationRulesContext.rules.push(...options.rules);
|
|
28
|
-
},
|
|
29
|
-
onSubscribe: buildHandler("subscribe", getTypeInfo, options.onDocument, options.onValidationFailed, options.rejectOnErrors !== false),
|
|
30
|
-
onExecute: buildHandler("execute", getTypeInfo, options.onDocument, options.onValidationFailed, options.rejectOnErrors !== false)
|
|
31
|
-
};
|
|
32
|
-
};
|
|
33
|
-
function buildHandler(name, getTypeInfo, onDocument, onValidationFailed, rejectOnErrors = true) {
|
|
34
|
-
return function handler({ args, setResultAndStopExecution }) {
|
|
35
|
-
const validationRulesContext = args.contextValue[symbolExtendedValidationRules];
|
|
36
|
-
if (validationRulesContext === void 0) throw new Error(`Plugin has not been properly set up. The 'contextFactory' function is not invoked and the result has not been passed to '${name}'.`);
|
|
37
|
-
if (validationRulesContext.didRun === false) {
|
|
38
|
-
validationRulesContext.didRun = true;
|
|
39
|
-
if (validationRulesContext.rules.length !== 0) {
|
|
40
|
-
const errors = [];
|
|
41
|
-
const typeInfo = getTypeInfo() ?? new TypeInfo(args.schema);
|
|
42
|
-
const validationContext = new ValidationContext(args.schema, args.document, typeInfo, (e) => {
|
|
43
|
-
errors.push(e);
|
|
44
|
-
});
|
|
45
|
-
const visitor = visitInParallel(validationRulesContext.rules.map((rule) => rule(validationContext, args)));
|
|
46
|
-
args.document = visit(args.document, visitWithTypeInfo(typeInfo, visitor));
|
|
47
|
-
if (onDocument) args.document = onDocument(args.document);
|
|
48
|
-
if (errors.length > 0) if (rejectOnErrors) {
|
|
49
|
-
let result = {
|
|
50
|
-
data: null,
|
|
51
|
-
errors
|
|
52
|
-
};
|
|
53
|
-
if (onValidationFailed) onValidationFailed({
|
|
54
|
-
args,
|
|
55
|
-
result,
|
|
56
|
-
setResult: (newResult) => result = newResult
|
|
57
|
-
});
|
|
58
|
-
setResultAndStopExecution(result);
|
|
59
|
-
} else {
|
|
60
|
-
function onResult({ result, setResult }) {
|
|
61
|
-
if (isAsyncIterable(result)) {
|
|
62
|
-
setResult({
|
|
63
|
-
data: null,
|
|
64
|
-
errors
|
|
65
|
-
});
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
const newResult = {
|
|
69
|
-
...result,
|
|
70
|
-
errors: [...result.errors || [], ...errors]
|
|
71
|
-
};
|
|
72
|
-
function visitPath(path, data = {}) {
|
|
73
|
-
let currentData = data ||= typeof path[0] === "number" ? [] : {};
|
|
74
|
-
for (const pathItemIndex in path.slice(0, -1)) {
|
|
75
|
-
const pathItem = path[pathItemIndex];
|
|
76
|
-
currentData = currentData[pathItem] ||= typeof path[Number(pathItemIndex) + 1] === "number" ? [] : {};
|
|
77
|
-
if (Array.isArray(currentData)) {
|
|
78
|
-
let pathItemIndexInArray = Number(pathItemIndex) + 1;
|
|
79
|
-
if (path[pathItemIndexInArray] === "@") pathItemIndexInArray = Number(pathItemIndex) + 2;
|
|
80
|
-
currentData = currentData.map((c) => visitPath(path.slice(pathItemIndexInArray), c));
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
currentData[path[path.length - 1]] = null;
|
|
84
|
-
return data;
|
|
85
|
-
}
|
|
86
|
-
errors.forEach((e) => {
|
|
87
|
-
if (e.path?.length) newResult.data = visitPath(e.path, newResult.data);
|
|
88
|
-
});
|
|
89
|
-
setResult(newResult);
|
|
90
|
-
}
|
|
91
|
-
return {
|
|
92
|
-
onSubscribeResult: onResult,
|
|
93
|
-
onExecuteDone: onResult
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
//#endregion
|
|
102
|
-
export { useExtendedValidation };
|