@envelop/core
Version:
This is the core package for Envelop. You can find a complete documentation here: https://github.com/n1ru4l/envelop
513 lines (512 loc) • 20.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.createEnvelopOrchestrator = void 0;
const utils_js_1 = require("./utils.js");
function createEnvelopOrchestrator({ plugins, parse, execute, subscribe, validate, }) {
let schema = null;
let initDone = false;
// Define the initial method for replacing the GraphQL schema, this is needed in order
// to allow setting the schema from the onPluginInit callback. We also need to make sure
// here not to call the same plugin that initiated the schema switch.
const replaceSchema = (newSchema, ignorePluginIndex = -1) => {
schema = newSchema;
if (initDone) {
for (const [i, plugin] of plugins.entries()) {
if (i !== ignorePluginIndex) {
plugin.onSchemaChange &&
plugin.onSchemaChange({
schema,
replaceSchema: schemaToSet => {
replaceSchema(schemaToSet, i);
},
});
}
}
}
};
const contextErrorHandlers = [];
// Iterate all plugins and trigger onPluginInit
for (const [i, plugin] of plugins.entries()) {
plugin.onPluginInit &&
plugin.onPluginInit({
plugins,
addPlugin: newPlugin => {
plugins.push(newPlugin);
},
setSchema: modifiedSchema => replaceSchema(modifiedSchema, i),
registerContextErrorHandler: handler => contextErrorHandlers.push(handler),
});
}
// A set of before callbacks defined here in order to allow it to be used later
const beforeCallbacks = {
init: [],
parse: [],
validate: [],
subscribe: [],
execute: [],
context: [],
perform: [],
};
for (const { onContextBuilding, onExecute, onParse, onSubscribe, onValidate, onEnveloped, onPerform } of plugins) {
onEnveloped && beforeCallbacks.init.push(onEnveloped);
onContextBuilding && beforeCallbacks.context.push(onContextBuilding);
onExecute && beforeCallbacks.execute.push(onExecute);
onParse && beforeCallbacks.parse.push(onParse);
onSubscribe && beforeCallbacks.subscribe.push(onSubscribe);
onValidate && beforeCallbacks.validate.push(onValidate);
onPerform && beforeCallbacks.perform.push(onPerform);
}
const init = initialContext => {
for (const [i, onEnveloped] of beforeCallbacks.init.entries()) {
onEnveloped({
context: initialContext,
extendContext: extension => {
if (!initialContext) {
return;
}
Object.assign(initialContext, extension);
},
setSchema: modifiedSchema => replaceSchema(modifiedSchema, i),
});
}
};
const customParse = beforeCallbacks.parse.length
? initialContext => (source, parseOptions) => {
let result = null;
let parseFn = parse;
const context = initialContext;
const afterCalls = [];
for (const onParse of beforeCallbacks.parse) {
const afterFn = onParse({
context,
extendContext: extension => {
Object.assign(context, extension);
},
params: { source, options: parseOptions },
parseFn,
setParseFn: newFn => {
parseFn = newFn;
},
setParsedDocument: newDoc => {
result = newDoc;
},
});
afterFn && afterCalls.push(afterFn);
}
if (result === null) {
try {
result = parseFn(source, parseOptions);
}
catch (e) {
result = e;
}
}
for (const afterCb of afterCalls) {
afterCb({
context,
extendContext: extension => {
Object.assign(context, extension);
},
replaceParseResult: newResult => {
result = newResult;
},
result,
});
}
if (result === null) {
throw new Error(`Failed to parse document.`);
}
if (result instanceof Error) {
throw result;
}
return result;
}
: () => parse;
const customValidate = beforeCallbacks.validate.length
? initialContext => (schema, documentAST, rules, typeInfo, validationOptions) => {
let actualRules = rules ? [...rules] : undefined;
let validateFn = validate;
let result = null;
const context = initialContext;
const afterCalls = [];
for (const onValidate of beforeCallbacks.validate) {
const afterFn = onValidate({
context,
extendContext: extension => {
Object.assign(context, extension);
},
params: {
schema,
documentAST,
rules: actualRules,
typeInfo,
options: validationOptions,
},
validateFn,
addValidationRule: rule => {
if (!actualRules) {
// Ideally we should provide default validation rules here.
// eslint-disable-next-line no-console
console.warn('No default validation rules provided.');
actualRules = [];
}
actualRules.push(rule);
},
setValidationFn: newFn => {
validateFn = newFn;
},
setResult: newResults => {
result = newResults;
},
});
afterFn && afterCalls.push(afterFn);
}
if (!result) {
result = validateFn(schema, documentAST, actualRules, typeInfo, validationOptions);
}
if (!result) {
return;
}
const valid = result.length === 0;
for (const afterCb of afterCalls) {
afterCb({
valid,
result,
context,
extendContext: extension => {
Object.assign(context, extension);
},
setResult: newResult => {
result = newResult;
},
});
}
return result;
}
: () => validate;
const customContextFactory = beforeCallbacks.context
.length
? initialContext => async (orchestratorCtx) => {
const afterCalls = [];
// In order to have access to the "last working" context object we keep this outside of the try block:
let context = orchestratorCtx ? { ...initialContext, ...orchestratorCtx } : initialContext;
try {
let isBreakingContextBuilding = false;
for (const onContext of beforeCallbacks.context) {
const afterHookResult = await onContext({
context,
extendContext: extension => {
context = { ...context, ...extension };
},
breakContextBuilding: () => {
isBreakingContextBuilding = true;
},
});
if (typeof afterHookResult === 'function') {
afterCalls.push(afterHookResult);
}
if (isBreakingContextBuilding === true) {
break;
}
}
for (const afterCb of afterCalls) {
afterCb({
context,
extendContext: extension => {
context = { ...context, ...extension };
},
});
}
return context;
}
catch (err) {
let error = err;
for (const errorCb of contextErrorHandlers) {
errorCb({
context,
error,
setError: err => {
error = err;
},
});
}
throw error;
}
}
: initialContext => orchestratorCtx => orchestratorCtx ? { ...initialContext, ...orchestratorCtx } : initialContext;
const useCustomSubscribe = beforeCallbacks.subscribe.length;
const customSubscribe = useCustomSubscribe
? (0, utils_js_1.makeSubscribe)(async (args) => {
let subscribeFn = subscribe;
const afterCalls = [];
const subscribeErrorHandlers = [];
let context = args.contextValue || {};
let result;
for (const onSubscribe of beforeCallbacks.subscribe) {
const after = await onSubscribe({
subscribeFn,
setSubscribeFn: newSubscribeFn => {
subscribeFn = newSubscribeFn;
},
extendContext: extension => {
context = { ...context, ...extension };
},
args: args,
setResultAndStopExecution: stopResult => {
result = stopResult;
},
});
if (after) {
if (after.onSubscribeResult) {
afterCalls.push(after.onSubscribeResult);
}
if (after.onSubscribeError) {
subscribeErrorHandlers.push(after.onSubscribeError);
}
}
if (result !== undefined) {
break;
}
}
if (result === undefined) {
result = await subscribeFn({
...args,
contextValue: context,
// Casted for GraphQL.js 15 compatibility
// Can be removed once we drop support for GraphQL.js 15
});
}
if (!result) {
return;
}
const onNextHandler = [];
const onEndHandler = [];
for (const afterCb of afterCalls) {
const hookResult = afterCb({
args: args,
result,
setResult: newResult => {
result = newResult;
},
});
if (hookResult) {
if (hookResult.onNext) {
onNextHandler.push(hookResult.onNext);
}
if (hookResult.onEnd) {
onEndHandler.push(hookResult.onEnd);
}
}
}
if (onNextHandler.length && (0, utils_js_1.isAsyncIterable)(result)) {
result = (0, utils_js_1.mapAsyncIterator)(result, async (result) => {
for (const onNext of onNextHandler) {
await onNext({
args: args,
result,
setResult: newResult => (result = newResult),
});
}
return result;
});
}
if (onEndHandler.length && (0, utils_js_1.isAsyncIterable)(result)) {
result = (0, utils_js_1.finalAsyncIterator)(result, () => {
for (const onEnd of onEndHandler) {
onEnd();
}
});
}
if (subscribeErrorHandlers.length && (0, utils_js_1.isAsyncIterable)(result)) {
result = (0, utils_js_1.errorAsyncIterator)(result, err => {
let error = err;
for (const handler of subscribeErrorHandlers) {
handler({
error,
setError: err => {
error = err;
},
});
}
throw error;
});
}
return result;
})
: (0, utils_js_1.makeSubscribe)(subscribe);
const useCustomExecute = beforeCallbacks.execute.length;
const customExecute = useCustomExecute
? (0, utils_js_1.makeExecute)(async (args) => {
let executeFn = execute;
let result;
const afterCalls = [];
let context = args.contextValue || {};
for (const onExecute of beforeCallbacks.execute) {
const after = await onExecute({
executeFn,
setExecuteFn: newExecuteFn => {
executeFn = newExecuteFn;
},
setResultAndStopExecution: stopResult => {
result = stopResult;
},
extendContext: extension => {
if (typeof extension === 'object') {
context = {
...context,
...extension,
};
}
else {
throw new Error(`Invalid context extension provided! Expected "object", got: "${JSON.stringify(extension)}" (${typeof extension})`);
}
},
args: args,
});
if (after?.onExecuteDone) {
afterCalls.push(after.onExecuteDone);
}
if (result !== undefined) {
break;
}
}
if (result === undefined) {
result = (await executeFn({
...args,
contextValue: context,
}));
}
const onNextHandler = [];
const onEndHandler = [];
for (const afterCb of afterCalls) {
const hookResult = await afterCb({
args: args,
result,
setResult: newResult => {
result = newResult;
},
});
if (hookResult) {
if (hookResult.onNext) {
onNextHandler.push(hookResult.onNext);
}
if (hookResult.onEnd) {
onEndHandler.push(hookResult.onEnd);
}
}
}
if (onNextHandler.length && (0, utils_js_1.isAsyncIterable)(result)) {
result = (0, utils_js_1.mapAsyncIterator)(result, async (result) => {
for (const onNext of onNextHandler) {
await onNext({
args: args,
result,
setResult: newResult => {
result = newResult;
},
});
}
return result;
});
}
if (onEndHandler.length && (0, utils_js_1.isAsyncIterable)(result)) {
result = (0, utils_js_1.finalAsyncIterator)(result, () => {
for (const onEnd of onEndHandler) {
onEnd();
}
});
}
return result;
})
: (0, utils_js_1.makeExecute)(execute);
initDone = true;
// This is done in order to trigger the first schema available, to allow plugins that needs the schema
// eagerly to have it.
if (schema) {
for (const [i, plugin] of plugins.entries()) {
plugin.onSchemaChange &&
plugin.onSchemaChange({
schema,
replaceSchema: modifiedSchema => replaceSchema(modifiedSchema, i),
});
}
}
const customPerform = initialContext => {
const parse = customParse(initialContext);
const validate = customValidate(initialContext);
const contextFactory = customContextFactory(initialContext);
return async (params, contextExtension) => {
const context = await contextFactory(contextExtension);
let earlyResult = null;
const onDones = [];
for (const onPerform of beforeCallbacks.perform) {
const after = await onPerform({
context,
extendContext: extension => {
Object.assign(context, extension);
},
params,
setParams: newParams => {
params = newParams;
},
setResult: result => {
earlyResult = result;
},
});
after?.onPerformDone && onDones.push(after.onPerformDone);
}
const done = (result) => {
for (const onDone of onDones) {
onDone({
result,
setResult: newResult => {
result = newResult;
},
});
}
return result;
};
if (earlyResult) {
return done(earlyResult);
}
let document;
try {
document = parse(params.query);
}
catch (err) {
return done({ errors: [err] });
}
const validationErrors = validate(schema, document);
if (validationErrors.length) {
return done({ errors: validationErrors });
}
if ((0, utils_js_1.isSubscriptionOperation)(document, params.operationName)) {
return done(await customSubscribe({
document,
schema,
variableValues: params.variables,
contextValue: context,
}));
}
return done(await customExecute({
document,
schema,
variableValues: params.variables,
contextValue: context,
}));
};
};
return {
getCurrentSchema() {
return schema;
},
init,
parse: customParse,
validate: customValidate,
execute: customExecute,
subscribe: customSubscribe,
contextFactory: customContextFactory,
perform: customPerform,
};
}
exports.createEnvelopOrchestrator = createEnvelopOrchestrator;