UNPKG

graphile-build

Version:

Build a GraphQL schema from plugins

189 lines 8.37 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); require("./global.js"); const debug_1 = tslib_1.__importDefault(require("debug")); const events_1 = require("events"); const graphql_1 = require("grafast/graphql"); const graphile_config_1 = require("graphile-config"); const util_1 = require("util"); const behavior_ts_1 = require("./behavior.js"); const makeNewBuild_ts_1 = tslib_1.__importDefault(require("./makeNewBuild.js")); const index_ts_1 = require("./newWithHooks/index.js"); const SchemaBuilderHooks_ts_1 = require("./SchemaBuilderHooks.js"); const utils_ts_1 = require("./utils.js"); const debug = (0, debug_1.default)("graphile-build:SchemaBuilder"); const INIT_OBJECT = Object.freeze(Object.create(null)); const INDENT = " "; const getSchemaHooks = (plugin) => plugin.schema?.hooks; /** * The class responsible for building a GraphQL schema from graphile-build * plugins by orchestrating the various callback functions. */ class SchemaBuilder extends events_1.EventEmitter { constructor(resolvedPreset, inflection) { super(); this.resolvedPreset = resolvedPreset; this.inflection = inflection; this.options = resolvedPreset.schema ?? {}; // Because hooks can nest, this keeps track of how deep we are. this.depth = -1; this.hooks = (0, SchemaBuilderHooks_ts_1.makeSchemaBuilderHooks)(); this.newWithHooks = (0, index_ts_1.makeNewWithHooks)({ builder: this }).newWithHooks; (0, graphile_config_1.orderedApply)(resolvedPreset.plugins, getSchemaHooks, (hookName, hookFn, plugin) => { this._setPluginName(plugin.name); this.hook(hookName, hookFn); this._setPluginName(null); }); } _setPluginName(name) { this._currentPluginName = name; } /** * Registers 'fn' as a hook for the given 'hookName'. Every hook `fn` takes * three arguments: * * - obj - the object currently being inspected * - build - the current build object (which contains a number of utilities * and the context of the build) * - context - information specific to the current invocation of the hook * * The function must return a replacement object for `obj` or `obj` itself. * Generally we advice that you return the object itself, modifying it as * necessary. In JavaScript, modifying an object object tends to be * significantly faster than returning a modified clone. */ hook(hookName, fn) { if (!this.hooks[hookName]) { // ERRORS: fuzzy-find a similar hook throw new Error(`Sorry, '${hookName}' is not a supported hook`); } if (this._currentPluginName) { fn.displayName = `${this._currentPluginName}/schema.hooks.${hookName}`; } this.hooks[hookName].push(fn); } /** * Applies the given 'hookName' hooks to the given 'input' and returns the * result, which is typically a derivative of 'input'. */ applyHooks(hookName, input, build, context, debugStr = "") { if (!input) { throw new Error(`applyHooks(${JSON.stringify(hookName)}, ...) was called with falsy input ${(0, util_1.inspect)(input, { colors: true, })}`); } this.depth++; const indent = INDENT.repeat(this.depth); try { debug(`%s[%s%s]: Running...`, indent, hookName, debugStr); const hooks = this.hooks[hookName]; if (!hooks) { throw new Error(`Sorry, '${hookName}' is not a registered hook`); } let newObj = input; for (const hook of hooks) { this.depth++; try { const hookDisplayName = hook.displayName || hook.name || "anonymous"; debug(`%s[%s%s]: Executing '%s'`, indent, hookName, debugStr, hookDisplayName); const previousHookName = build.status.currentHookName; const previousHookEvent = build.status.currentHookEvent; build.status.currentHookName = hookDisplayName; build.status.currentHookEvent = hookName; const oldObj = newObj; newObj = hook(newObj, build, context); if (hookName === "build") { /* * Unlike all the other hooks, the `build` hook must always use the * same `build` object - never returning a new object for fear of * causing issues to other build hooks that reference the old * object and don't get the new additions. */ if (newObj !== oldObj) { throw new Error(`Build hook '${hookDisplayName}' returned a new object; 'build' hooks must always return the same Build object - please use 'return build.extend(build, {...})' instead.`); } } build.status.currentHookName = previousHookName; build.status.currentHookEvent = previousHookEvent; if (!newObj) { throw new Error(`GraphileBuild.Hook '${hook.displayName || hook.name || "anonymous"}' for '${hookName}' returned falsy value '${newObj}'`); } debug(`%s[%s%s]: '%s' complete`, indent, hookName, debugStr, hookDisplayName); } finally { this.depth--; } } debug(`%s[%s%s]: Complete`, indent, hookName, debugStr); return newObj; } finally { this.depth--; } } /** * Create the 'Build' object. */ createBuild(input) { const initialBuild = (0, makeNewBuild_ts_1.default)(this, input, this.inflection); const build = this.applyHooks("build", initialBuild, initialBuild, { scope: Object.create(null), type: "build", }); // Bind all functions so they can be dereferenced (0, utils_ts_1.bindAll)(build, Object.keys(build).filter((key) => typeof build[key] === "function")); const finalBuild = build; finalBuild.behavior = new behavior_ts_1.Behavior(this.resolvedPreset, finalBuild); Object.freeze(finalBuild); finalBuild.behavior.freeze(); finalBuild.status.isBuildPhaseComplete = true; return finalBuild; } initBuild(build) { if (build.status.isInitPhaseComplete) { return build; } const initContext = { scope: Object.create(null), type: "init", }; this.applyHooks("init", INIT_OBJECT, build, initContext); build.status.isInitPhaseComplete = true; for (const assertion of build.__postInitAssertions) { assertion(); } return build; } /** * Given the `input` (result of the "gather" phase), builds the GraphQL * schema synchronously. */ buildSchema(input) { const build = this.initBuild(this.createBuild(input)); const schemaSpec = { directives: [...build.graphql.specifiedDirectives], }; const schemaScope = { __origin: `Graphile built-in`, }; const tempSchema = this.newWithHooks(build, graphql_1.GraphQLSchema, schemaSpec, schemaScope); const finalizeContext = { scope: Object.create(null), type: "finalize", }; const schema = tempSchema ? this.applyHooks("finalize", tempSchema, build, finalizeContext, "Finalizing GraphQL schema") : tempSchema; if (!schema) { throw new Error("Schema generation failed"); } const validationErrors = (0, graphql_1.validateSchema)(schema); if (validationErrors.length) { throw new AggregateError(validationErrors, `Schema construction failed due to ${validationErrors.length} validation failure(s). First failure was: ${String(validationErrors[0])}`); } return schema; } } exports.default = SchemaBuilder; //# sourceMappingURL=SchemaBuilder.js.map