graphile-build
Version:
Build a GraphQL schema from plugins
546 lines • 27.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.version = exports.defaultPreset = exports.TrimEmptyDescriptionsPlugin = exports.SwallowErrorsPlugin = exports.SubscriptionPlugin = exports.StreamDeferPlugin = exports.RegisterQueryNodePlugin = exports.QueryQueryPlugin = exports.QueryPlugin = exports.PageInfoStartEndCursorPlugin = exports.NodePlugin = exports.NodeIdCodecPipeStringPlugin = exports.NodeIdCodecBase64JSONPlugin = exports.NodeAccessorPlugin = exports.MutationPlugin = exports.MutationPayloadQueryPlugin = exports.MinifySchemaPlugin = exports.CursorTypePlugin = exports.CommonTypesPlugin = exports.CommonBehaviorsPlugin = exports.ClientMutationIdDescriptionPlugin = exports.BuiltinScalarConnectionsPlugin = exports.AddNodeInterfaceToSuitableTypesPlugin = exports.getBuilder = exports.watchGather = exports.gather = exports.buildInflection = exports.SchemaBuilder = exports.upperFirst = exports.upperCamelCase = exports.singularize = exports.pluralize = exports.gatherConfig = exports.formatInsideUnderscores = exports.EXPORTABLE_OBJECT_CLONE = exports.EXPORTABLE_ARRAY_CLONE = exports.EXPORTABLE = exports.constantCaseAll = exports.constantCase = exports.camelCase = exports.isValidBehaviorString = void 0;
exports.buildSchema = buildSchema;
exports.makeSchema = makeSchema;
exports.watchSchema = watchSchema;
const tslib_1 = require("tslib");
require("./global.js");
require("./interfaces.js");
const grafast = tslib_1.__importStar(require("grafast"));
const graphql_1 = require("grafast/graphql");
const graphile_config_1 = require("graphile-config");
var behavior_ts_1 = require("./behavior.js");
Object.defineProperty(exports, "isValidBehaviorString", { enumerable: true, get: function () { return behavior_ts_1.isValidBehaviorString; } });
const extend_ts_1 = tslib_1.__importDefault(require("./extend.js"));
const inflection_ts_1 = require("./inflection.js");
const index_ts_1 = require("./plugins/index.js");
Object.defineProperty(exports, "AddNodeInterfaceToSuitableTypesPlugin", { enumerable: true, get: function () { return index_ts_1.AddNodeInterfaceToSuitableTypesPlugin; } });
Object.defineProperty(exports, "BuiltinScalarConnectionsPlugin", { enumerable: true, get: function () { return index_ts_1.BuiltinScalarConnectionsPlugin; } });
Object.defineProperty(exports, "ClientMutationIdDescriptionPlugin", { enumerable: true, get: function () { return index_ts_1.ClientMutationIdDescriptionPlugin; } });
Object.defineProperty(exports, "CommonBehaviorsPlugin", { enumerable: true, get: function () { return index_ts_1.CommonBehaviorsPlugin; } });
Object.defineProperty(exports, "CommonTypesPlugin", { enumerable: true, get: function () { return index_ts_1.CommonTypesPlugin; } });
Object.defineProperty(exports, "CursorTypePlugin", { enumerable: true, get: function () { return index_ts_1.CursorTypePlugin; } });
Object.defineProperty(exports, "MinifySchemaPlugin", { enumerable: true, get: function () { return index_ts_1.MinifySchemaPlugin; } });
Object.defineProperty(exports, "MutationPayloadQueryPlugin", { enumerable: true, get: function () { return index_ts_1.MutationPayloadQueryPlugin; } });
Object.defineProperty(exports, "MutationPlugin", { enumerable: true, get: function () { return index_ts_1.MutationPlugin; } });
Object.defineProperty(exports, "NodeAccessorPlugin", { enumerable: true, get: function () { return index_ts_1.NodeAccessorPlugin; } });
Object.defineProperty(exports, "NodeIdCodecBase64JSONPlugin", { enumerable: true, get: function () { return index_ts_1.NodeIdCodecBase64JSONPlugin; } });
Object.defineProperty(exports, "NodeIdCodecPipeStringPlugin", { enumerable: true, get: function () { return index_ts_1.NodeIdCodecPipeStringPlugin; } });
Object.defineProperty(exports, "NodePlugin", { enumerable: true, get: function () { return index_ts_1.NodePlugin; } });
Object.defineProperty(exports, "PageInfoStartEndCursorPlugin", { enumerable: true, get: function () { return index_ts_1.PageInfoStartEndCursorPlugin; } });
Object.defineProperty(exports, "QueryPlugin", { enumerable: true, get: function () { return index_ts_1.QueryPlugin; } });
Object.defineProperty(exports, "QueryQueryPlugin", { enumerable: true, get: function () { return index_ts_1.QueryQueryPlugin; } });
Object.defineProperty(exports, "RegisterQueryNodePlugin", { enumerable: true, get: function () { return index_ts_1.RegisterQueryNodePlugin; } });
Object.defineProperty(exports, "StreamDeferPlugin", { enumerable: true, get: function () { return index_ts_1.StreamDeferPlugin; } });
Object.defineProperty(exports, "SubscriptionPlugin", { enumerable: true, get: function () { return index_ts_1.SubscriptionPlugin; } });
Object.defineProperty(exports, "SwallowErrorsPlugin", { enumerable: true, get: function () { return index_ts_1.SwallowErrorsPlugin; } });
Object.defineProperty(exports, "TrimEmptyDescriptionsPlugin", { enumerable: true, get: function () { return index_ts_1.TrimEmptyDescriptionsPlugin; } });
const SchemaBuilder_ts_1 = tslib_1.__importDefault(require("./SchemaBuilder.js"));
exports.SchemaBuilder = SchemaBuilder_ts_1.default;
var utils_ts_1 = require("./utils.js");
Object.defineProperty(exports, "camelCase", { enumerable: true, get: function () { return utils_ts_1.camelCase; } });
Object.defineProperty(exports, "constantCase", { enumerable: true, get: function () { return utils_ts_1.constantCase; } });
Object.defineProperty(exports, "constantCaseAll", { enumerable: true, get: function () { return utils_ts_1.constantCaseAll; } });
Object.defineProperty(exports, "EXPORTABLE", { enumerable: true, get: function () { return utils_ts_1.EXPORTABLE; } });
Object.defineProperty(exports, "EXPORTABLE_ARRAY_CLONE", { enumerable: true, get: function () { return utils_ts_1.EXPORTABLE_ARRAY_CLONE; } });
Object.defineProperty(exports, "EXPORTABLE_OBJECT_CLONE", { enumerable: true, get: function () { return utils_ts_1.EXPORTABLE_OBJECT_CLONE; } });
Object.defineProperty(exports, "formatInsideUnderscores", { enumerable: true, get: function () { return utils_ts_1.formatInsideUnderscores; } });
Object.defineProperty(exports, "gatherConfig", { enumerable: true, get: function () { return utils_ts_1.gatherConfig; } });
Object.defineProperty(exports, "pluralize", { enumerable: true, get: function () { return utils_ts_1.pluralize; } });
Object.defineProperty(exports, "singularize", { enumerable: true, get: function () { return utils_ts_1.singularize; } });
Object.defineProperty(exports, "upperCamelCase", { enumerable: true, get: function () { return utils_ts_1.upperCamelCase; } });
Object.defineProperty(exports, "upperFirst", { enumerable: true, get: function () { return utils_ts_1.upperFirst; } });
const grafast_1 = require("grafast");
const preset_ts_1 = require("./preset.js");
const utils_ts_2 = require("./utils.js");
const EMPTY_OBJECT = Object.freeze(Object.create(null));
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
/**
* Generate 'build.inflection' from the given preset.
*/
const buildInflection = (preset,
/** @internal */
trace) => {
const resolvedPreset = (0, graphile_config_1.resolvePreset)(preset);
const { plugins, inflection: _options = {} } = resolvedPreset;
const inflectors = {};
function add(inflectorName, inflector, source, pluginName, replace = false) {
if (!replace || !inflectors[inflectorName]) {
(0, extend_ts_1.default)(inflectors, { [inflectorName]: inflector }, source);
}
else {
inflectors[inflectorName] = inflector;
}
if (trace) {
let list = trace.get(inflectorName);
if (!list) {
list = [];
trace.set(inflectorName, list);
}
list.push({ pluginName, source });
}
}
const base = (0, inflection_ts_1.makeInitialInflection)();
for (const [inflectorName, inflector] of Object.entries(base)) {
add(inflectorName, inflector, "Builtin", "");
}
// Add the base inflectors
if (plugins) {
for (const plugin of plugins) {
if (plugin.inflection?.add) {
const inflectorsToAdd = plugin.inflection.add;
for (const inflectorName of Object.keys(inflectorsToAdd)) {
const fn = inflectorsToAdd[inflectorName];
if (fn) {
const inflector = fn.bind(inflectors, preset);
add(inflectorName, inflector, `Adding inflectors from ${plugin.name}`, plugin.name, false);
}
}
}
}
// Overwrite the inflectors
(0, graphile_config_1.orderedApply)(plugins, (plugin) => plugin.inflection?.replace, (inflectorName, replacementFunction, plugin) => {
const previous = inflectors[inflectorName];
const ignore = plugin.inflection?.ignoreReplaceIfNotExists ?? [];
if (!previous && ignore?.includes(inflectorName)) {
// Do nothing
}
else {
if (!previous) {
console.warn(`Plugin '${plugin.name}' attempted to overwrite inflector '${inflectorName}', but no such inflector exists.`);
}
const inflector = replacementFunction.bind(inflectors, previous, preset);
add(inflectorName, inflector, `Adding inflectors from ${plugin.name}`, plugin.name, true);
}
});
}
return inflectors;
};
exports.buildInflection = buildInflection;
function pluginNamespace(plugin) {
return plugin.gather?.namespace ?? plugin.name;
}
/**
* @internal
*/
const gatherBase = (preset, shared = {}) => {
const { inflection = (0, exports.buildInflection)(preset) } = shared;
const resolvedPreset = (0, graphile_config_1.resolvePreset)(preset);
const options = resolvedPreset.gather || {};
const plugins = resolvedPreset.plugins;
const globalState = Object.create(null);
const gatherState = Object.create(null);
const helpers = Object.create(null); // GatherHelpers
const hooks = new graphile_config_1.AsyncHooks();
const contextBase = Object.freeze({
// Global libraries/helpers
lib: resolvedPreset.lib,
// DEPRECATED: use `lib` instead:
grafast,
EXPORTABLE: utils_ts_2.EXPORTABLE,
// Established by the start of the gather phase
resolvedPreset,
options,
inflection,
process: hooks.process.bind(hooks),
helpers: helpers,
});
const pluginContext = new Map();
const gatherPlugins = plugins?.filter((p) => p.gather);
if (gatherPlugins) {
// Prepare the plugins to run by preparing their initial states, and registering the helpers (hooks area already done).
for (const plugin of gatherPlugins) {
const spec = plugin.gather;
const specNamespace = pluginNamespace(plugin);
if (specNamespace in globalState) {
// ERRORS: track who registers which namespace, output more helpful error.
throw new Error(`Namespace '${specNamespace}' was already registered, it cannot be registered by two plugins - namespaces must be unique. Latest plugin was '${plugin.name}'.`);
}
const cache = (globalState[specNamespace] =
spec.initialCache?.(contextBase) ?? Object.create(null));
if (typeof cache.then === "function") {
// ENHANCE: can we just make `initialCache` allow promises?
throw new Error(`\`initialCache\` may not return a promise directly; instead set one of the keys on the object it returns to a promise and await that in \`initialState\` (which is allowed to be async)`);
}
const context = Object.seal({
...contextBase,
cache,
state: EMPTY_OBJECT /* This will be overwritten before it's used */,
});
pluginContext.set(plugin, context);
helpers[specNamespace] = Object.create(null);
if (spec.helpers != null) {
if (!spec.namespace) {
throw new Error(`Plugin '${plugin.name}' tries to add helpers but is using an implicit namespace. Please use an explicit \`plugin.gather.namespace\`.`);
}
const specHelpers = spec.helpers;
for (const helperName of Object.keys(specHelpers)) {
helpers[specNamespace][helperName] = (...args) => {
return specHelpers[helperName](context, ...args);
};
}
}
}
// Register the hooks
(0, graphile_config_1.orderedApply)(gatherPlugins, (p) => p.gather.hooks, (name, fn, plugin) => {
const context = pluginContext.get(plugin);
// hooks.hook(name, (...args) => fn(context, ...args));
hooks.hook(name, ((...args) => fn(context, ...args)));
});
}
async function run() {
const output = Object.create(null);
if (gatherPlugins) {
// Reset state
for (const plugin of gatherPlugins) {
const spec = plugin.gather;
const specNamespace = pluginNamespace(plugin);
const context = pluginContext.get(plugin);
const val = typeof spec.initialState === "function"
? await spec.initialState(context.cache, context)
: Object.create(null);
context.state = gatherState[specNamespace] = val;
}
// Now call the main functions
for (const plugin of gatherPlugins) {
const spec = plugin.gather;
if (spec.main) {
const context = pluginContext.get(plugin);
await spec.main(output, context);
}
}
}
return output;
}
async function watch(callback) {
let stopped = false;
const unlisten = [];
let runAgain = false;
let runInProgress = true;
let counter = 0;
const handleChange = () => {
if (stopped) {
return;
}
if (runInProgress) {
runAgain = true;
return;
}
const currentCounter = ++counter;
runAgain = false;
runInProgress = true;
promiseToCallback(run(), async (e, v) => {
if (stopped)
return;
try {
await callback(v ?? null, e, makeRetry(currentCounter));
}
catch {
// ERRORS: this indicates a bug in user code; how to handle?
/*nom nom nom*/
}
runInProgress = false;
if (runAgain)
handleChange();
});
};
const makeRetry = (currentCounter) => {
return () => {
if (counter === currentCounter) {
handleChange();
}
else {
// Another change was already registered; ignore
}
};
};
if (gatherPlugins) {
// Put all the plugins into watch mode.
for (const plugin of gatherPlugins) {
const spec = plugin.gather;
if (spec.watch) {
const context = pluginContext.get(plugin);
unlisten.push(await spec.watch(context, handleChange));
}
}
}
// Trigger the first build, being sure it completes before resolving
try {
// Clear 'runAgain' since we're starting now
runAgain = false;
const firstResult = await run();
callback(firstResult, undefined, makeRetry(counter));
}
catch (e) {
callback(null, e, makeRetry(counter));
}
runInProgress = false;
if (runAgain) {
handleChange();
}
// Return the unlistener.
return () => {
stopped = true;
unlisten.forEach((cb) => cb());
};
}
return {
run,
watch,
};
};
function promiseToCallback(promise, callback) {
return promise.then((v) => callback(undefined, v), (e) => callback(e, undefined));
}
/**
* One-time gather. See `watchGather` for watch mode.
*/
const gather = (preset, shared) => {
const { run } = gatherBase(preset, shared);
return run();
};
exports.gather = gather;
/**
* Tells your gather plugins to monitor their sources, and passes the resulting
* BuildInput to the callback each time a new one is generated. It is
* guaranteed that the `callback` will be called at least once before the
* promise resolves.
*
* @returns A callback to call to stop watching.
*/
const watchGather = (preset, shared, callback) => {
const { watch } = gatherBase(preset, shared);
return watch(callback);
};
exports.watchGather = watchGather;
/**
* Gets a SchemaBuilder object for the given preset and inflection. It's rare
* you would need this, typically you'll want `buildSchema` instead.
*/
const getBuilder = (preset, inflection = (0, exports.buildInflection)(preset)) => {
const resolvedPreset = (0, graphile_config_1.resolvePreset)(preset);
if (!resolvedPreset.plugins || resolvedPreset.plugins.length === 0) {
throw new Error(`You're attempting to build a GraphQL schema, but no plugins are specified in your preset. Please check the 'extends' key in your preset - you may have forgotten to add the relevant presets, or the presets may not have been imported correctly.`);
}
if (!resolvedPreset.plugins.includes(index_ts_1.QueryPlugin)) {
console.warn(`[WARNING] You're attempting to build a GraphQL schema, but the QueryPlugin is missing from your configuration. Unless you have done this very deliberately, this is probably a mistake - please check the 'extends' key in your preset - you may have forgotten to add the relevant presets, or the presets may not have been imported correctly.`);
}
const builder = new SchemaBuilder_ts_1.default(resolvedPreset, inflection);
return builder;
};
exports.getBuilder = getBuilder;
async function writeFileIfDiffers(path, contents) {
// COMPAT: support other environments than Node
const { readFile, writeFile } = await import("node:fs/promises");
let oldContents = null;
try {
oldContents = await readFile(path, "utf8");
}
catch (e) {
/* noop */
}
if (oldContents !== contents) {
await writeFile(path, contents);
}
}
/**
* Builds a GraphQL schema according to the given preset and input data.
*/
function buildSchema(rawPreset, input, shared) {
const { builder, schema } = _buildSchema(rawPreset, input, shared);
writeOutputsDetached(builder, schema);
return schema;
}
function _buildSchema(rawPreset, input, shared = {}) {
const preset = { extends: [preset_ts_1.GraphileBuildLibPreset, rawPreset] };
const builder = (0, exports.getBuilder)(preset, shared.inflection);
const schema = builder.buildSchema(input);
return { builder, schema };
}
/**
* If the builder settings indicate to export SDL and/or introspection, do so.
*/
async function writeOutputs(builder, schema) {
const { exportSchemaSDLPath, exportSchemaIntrospectionResultPath, sortExport = false, } = builder.options;
const schemaToExport = (exportSchemaSDLPath || exportSchemaIntrospectionResultPath) && sortExport
? (0, graphql_1.lexicographicSortSchema)(schema)
: schema;
if (exportSchemaSDLPath) {
const text = (0, graphql_1.printSchema)(schemaToExport) + "\n";
await writeFileIfDiffers(exportSchemaSDLPath, text).catch((e) => {
throw new Error(`Failed to write schema in GraphQL format to '${exportSchemaSDLPath}': ${e.message ?? e}`, { cause: e });
});
}
if (exportSchemaIntrospectionResultPath) {
const introspectionQuery = (0, graphql_1.getIntrospectionQuery)();
const introspectionResult = (0, graphql_1.graphqlSync)({
source: introspectionQuery,
schema: schemaToExport,
});
const text = JSON.stringify(introspectionResult, null, 2) + "\n";
await writeFileIfDiffers(exportSchemaIntrospectionResultPath, text).catch((e) => {
throw new Error(`Failed to write schema introspection results in JSON format to '${exportSchemaIntrospectionResultPath}': ${e.message ?? e}`, { cause: e });
});
}
}
/**
* As `writeOutputs`, but return synchronously (handling any errors in the
* background via logging).
*/
function writeOutputsDetached(builder, schema) {
writeOutputs(builder, schema).catch((e) => void console.error(e));
}
var preset_ts_2 = require("./preset.js");
Object.defineProperty(exports, "defaultPreset", { enumerable: true, get: function () { return preset_ts_2.defaultPreset; } });
/**
* Builds the GraphQL schema by resolving the preset, running inflection then
* gather and building the schema. Returns the results (after waiting for any
* exports to be written if configured).
*
* @experimental
*/
async function makeSchema(preset) {
const resolvedPreset = (0, graphile_config_1.resolvePreset)(preset);
// An error caused here cannot be solved by retrying, so don't catch it.
const inflection = (0, exports.buildInflection)(resolvedPreset);
const shared = { inflection };
const retryOnInitFail = resolvedPreset.schema?.retryOnInitFail;
let phase = "UNKNOWN";
const make = async () => {
phase = "GATHER";
const input = await (0, exports.gather)(resolvedPreset, shared);
phase = "SCHEMA";
const { builder, schema } = _buildSchema(resolvedPreset, input, shared);
phase = "WRITE";
await writeOutputs(builder, schema);
phase = "UNKNOWN";
return { schema, resolvedPreset };
};
if (retryOnInitFail) {
// eslint-disable-next-line no-constant-condition
for (let attempts = 1; true; attempts++) {
try {
const result = await make();
if (attempts > 1) {
console.warn(`Schema constructed successfully on attempt ${attempts}.`);
}
return result;
}
catch (error) {
await sleepFromRetryOnInitFail(retryOnInitFail, phase, attempts, error);
}
}
}
else {
return make();
}
}
async function sleepFromRetryOnInitFail(retryOnInitFail, phase, attempts, error) {
const delay = Math.min(100 * Math.pow(attempts, 2), 30000);
const start = process.hrtime();
const retryOrPromise = typeof retryOnInitFail === "function"
? retryOnInitFail(error, attempts, delay)
: retryOnInitFail;
if (retryOrPromise === false) {
throw error;
}
console.warn(`Error occurred whilst building the schema (phase = ${phase}; attempt ${attempts}). We'll try again ${retryOrPromise === true ? `in ${delay}ms` : `shortly`}.\n ${String(error.stack ?? error).replace(/\n/g, "\n ")}`);
if (retryOrPromise === true) {
await sleep(delay);
}
else if (!(0, grafast_1.isPromiseLike)(retryOrPromise)) {
throw new Error(`Invalid retryOnInitFail setting; must be true, false, or an optionally async function that resolves to true/false`);
}
else {
const retry = await retryOrPromise;
const diff = process.hrtime(start);
const dur = diff[0] * 1e3 + diff[1] * 1e-6;
if (!retry) {
throw error;
}
else if (dur < 50) {
// retryOnInitFail didn't wait long enough; use default wait.
console.error(`Your retryOnInitFail function should include a delay of at least 50ms before resolving; falling back to a ${delay}ms wait (attempts = ${attempts}) to avoid overwhelming the database.`);
await sleep(delay);
}
else {
// The promise already waited long enough, continue
}
}
}
/**
* Runs the "gather" phase in watch mode and calls 'callback' with the
* generated SchemaResult each time a new schema is generated.
*
* It is guaranteed that `callback` will be called at least once before the
* promise resolves.
*
* Returns a function that can be called to stop watching.
*
* @experimental
*/
async function watchSchema(preset, callback) {
const resolvedPreset = (0, graphile_config_1.resolvePreset)(preset);
const shared = {
inflection: (0, exports.buildInflection)(resolvedPreset),
};
const retryOnInitFail = resolvedPreset.schema?.retryOnInitFail;
let attempts = 0;
let haveHadSuccess = false;
const handleErrorWithRetry = async (error, retry) => {
if (retryOnInitFail) {
try {
await sleepFromRetryOnInitFail(retryOnInitFail, "GATHER", attempts, error);
retry();
}
catch (e) {
await callback(e);
}
}
else {
if (!haveHadSuccess) {
// Inability to gather is fatal - database connection issue?
await callback(error);
}
else {
console.error(`Error occurred during watch gather: ${error}`);
}
}
};
const stopWatching = await (0, exports.watchGather)(resolvedPreset, shared, async (input, error, retry) => {
++attempts;
if (error) {
// An error here could be a database connectivity issue or similar
// issue, if retryOnInitFail is set we should automatically retry.
try {
await handleErrorWithRetry(error, retry);
}
catch (e) {
console.error(`Error occurred in the error handler callback to watchSchema: `, e, error);
}
}
else {
if (attempts > 1) {
console.warn(`Gather completed successfully on attempt ${attempts}.`);
}
attempts = 0;
haveHadSuccess = true;
try {
const { builder, schema } = _buildSchema(resolvedPreset, input, shared);
// Don't delay schema update for file writing.
writeOutputsDetached(builder, schema);
await callback(null, { schema, resolvedPreset });
}
catch (e) {
// Retrying this on its own is pointless, we need the gather phase to
// give us more data so we can just await regular watch for that.
console.error(`Error occurred during watch schema generation:`, e);
}
}
});
return stopWatching;
}
var version_ts_1 = require("./version.js");
Object.defineProperty(exports, "version", { enumerable: true, get: function () { return version_ts_1.version; } });
//# sourceMappingURL=index.js.map