graphile-build
Version:
Build a GraphQL schema from plugins
407 lines • 18.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = makeNewBuild;
const tslib_1 = require("tslib");
require("./global.js");
const chalk_1 = tslib_1.__importDefault(require("chalk"));
const graphql_1 = require("grafast/graphql");
const semver = tslib_1.__importStar(require("semver"));
const append_ts_1 = tslib_1.__importDefault(require("./append.js"));
const extend_ts_1 = tslib_1.__importStar(require("./extend.js"));
const utils_ts_1 = require("./utils.js");
const version_ts_1 = require("./version.js");
const BUILTINS = ["Int", "Float", "Boolean", "ID", "String"];
/** Have we warned the user they're using the 5-arg deprecated registerObjectType call? */
let registerObjectType5argsDeprecatedWarned = false;
/**
* Makes a new 'Build' object suitable to be passed through the 'build' hook.
*/
function makeNewBuild(builder, input, inflection) {
const lib = builder.resolvedPreset.lib ?? {};
const building = new Set();
const allTypes = {
Int: graphql_1.GraphQLInt,
Float: graphql_1.GraphQLFloat,
String: graphql_1.GraphQLString,
Boolean: graphql_1.GraphQLBoolean,
ID: graphql_1.GraphQLID,
};
const allTypesSources = {
Int: "GraphQL Built-in",
Float: "GraphQL Built-in",
String: "GraphQL Built-in",
Boolean: "GraphQL Built-in",
ID: "GraphQL Built-in",
};
/**
* Where the type factories are; so we don't construct types until they're needed.
*/
const typeRegistry = Object.create(null);
const scopeByType = new Map();
// TODO: allow registering a previously constructed type.
function register(klass, typeName, scope, specGenerator, origin) {
if (!this.status.isBuildPhaseComplete || this.status.isInitPhaseComplete) {
throw new Error("Types may only be registered in the 'init' phase");
}
if (!typeName) {
throw new Error(`Attempted to register a ${klass.name} with empty (or falsy) type name`);
}
const newTypeSource = origin || `'register' call during hook '${build.status.currentHookName}'`;
if (allTypesSources[typeName]) {
const oldTypeSource = allTypesSources[typeName];
const firstEntityDetails = !oldTypeSource
? "The first type was registered from an unknown origin."
: `The first entity was:\n\n${(0, extend_ts_1.indent)(chalk_1.default.magenta(oldTypeSource))}`;
const secondEntityDetails = !newTypeSource
? "The second type was registered from an unknown origin."
: `The second entity was:\n\n${(0, extend_ts_1.indent)(chalk_1.default.yellow(newTypeSource))}`;
throw new Error(`A type naming conflict has occurred - two entities have tried to define the same type '${chalk_1.default.bold(typeName)}'.\n\n${(0, extend_ts_1.indent)(firstEntityDetails)}\n\n${(0, extend_ts_1.indent)(secondEntityDetails)}`);
}
allTypesSources[typeName] = newTypeSource;
typeRegistry[typeName] = {
typeName,
klass,
scope,
specGenerator,
origin,
};
}
const build = {
// FORBID EXPORTING!
...{ $$export: false },
lib,
// Legacy support for things from lib
EXPORTABLE: lib.graphileBuild.EXPORTABLE,
exportNameHint: lib.graphileBuild.exportNameHint,
EXPORTABLE_ARRAY_CLONE: lib.graphileBuild.EXPORTABLE_ARRAY_CLONE,
EXPORTABLE_OBJECT_CLONE: lib.graphileBuild.EXPORTABLE_OBJECT_CLONE,
grafast: lib.grafast,
graphql: lib.graphql,
options: builder.options,
versions: {
grafast: lib.grafast.version,
graphql: lib.graphql.version,
"graphile-build": version_ts_1.version,
},
input,
hasVersion(packageName, range, options = { includePrerelease: true }) {
const packageVersion = this.versions[packageName];
if (!packageVersion)
return false;
return semver.satisfies(packageVersion, range, options);
},
extend(base, extra, hint, behaviorOnConflict = "throw") {
try {
return (0, extend_ts_1.default)(base, extra, hint);
}
catch (e) {
if (behaviorOnConflict === "recoverable") {
this.handleRecoverableError(e);
return base;
}
else {
throw e;
}
}
},
append(base, extra, getIdentity, hint, behaviorOnConflict = "throw") {
try {
return (0, append_ts_1.default)(base, extra, getIdentity, hint);
}
catch (e) {
if (behaviorOnConflict === "recoverable") {
this.handleRecoverableError(e);
return base;
}
else {
throw e;
}
}
},
getAllTypes() {
return allTypes;
},
scopeByType,
inflection,
handleRecoverableError(e) {
e["recoverable"] = true;
throw e;
},
recoverable(value, callback) {
try {
return callback();
}
catch (e) {
this.handleRecoverableError(e);
return value;
}
},
status: {
isBuildPhaseComplete: false,
isInitPhaseComplete: false,
currentHookName: null,
currentHookEvent: null,
},
wrapDescription: utils_ts_1.wrapDescription,
stringTypeSpec: utils_ts_1.stringTypeSpec,
intTypeSpec: utils_ts_1.intTypeSpec,
registerObjectType(typeName, scope, assertStepOrSpecGenerator, specGeneratorOrOrigin, possiblyOrigin) {
if (typeof specGeneratorOrOrigin === "function") {
// This is the `@deprecated` path; we'll be removing this
if (!registerObjectType5argsDeprecatedWarned) {
registerObjectType5argsDeprecatedWarned = true;
console.trace(`[DEPRECATED] There is a call to 'registerObject(typeName, scope, assertStep, specGenerator, origin)'; this signature has been deprecated. Please move 'assertStep' into the spec returned by 'stepGenerator' and remove the third argument to give: 'registerObject(typeName, scope, specGenerator, origin)'. This compatibility shim will not be supported for long.`);
}
const assertStep = assertStepOrSpecGenerator;
const specGenerator = specGeneratorOrOrigin;
const origin = possiblyOrigin;
const replacementSpecGenerator = (...args) => {
const spec = specGenerator(...args);
return {
assertStep,
...spec,
};
};
register.call(this, graphql_1.GraphQLObjectType, typeName, scope, replacementSpecGenerator, origin);
}
else {
const specGenerator = assertStepOrSpecGenerator;
const origin = specGeneratorOrOrigin;
register.call(this, graphql_1.GraphQLObjectType, typeName, scope, specGenerator, origin);
}
},
registerUnionType(typeName, scope, specGenerator, origin) {
register.call(this, graphql_1.GraphQLUnionType, typeName, scope, specGenerator, origin);
},
registerInterfaceType(typeName, scope, specGenerator, origin) {
register.call(this, graphql_1.GraphQLInterfaceType, typeName, scope, specGenerator, origin);
},
registerInputObjectType(typeName, scope, specGenerator, origin) {
register.call(this, graphql_1.GraphQLInputObjectType, typeName, scope, specGenerator, origin);
},
registerScalarType(typeName, scope, specGenerator, origin) {
register.call(this, graphql_1.GraphQLScalarType, typeName, scope, specGenerator, origin);
},
registerEnumType(typeName, scope, specGenerator, origin) {
register.call(this, graphql_1.GraphQLEnumType, typeName, scope, specGenerator, origin);
},
__postInitAssertions: [],
assertTypeName(typeName, deferrable = false) {
if (!this.status.isBuildPhaseComplete) {
throw new Error("Must not call build.assertTypeName before 'build' phase is complete; use 'init' phase instead");
}
if (typeName in allTypesSources) {
return true;
}
else {
const error = new Error(`Type name '${typeName}' is not registered - be sure to register the type before you attempt to reference it.`);
if (deferrable && !this.status.isInitPhaseComplete) {
this.__postInitAssertions.push(() => {
if (!(typeName in allTypesSources)) {
throw error;
}
});
}
else {
throw error;
}
}
},
getTypeMetaByName(typeName) {
if (!this.status.isBuildPhaseComplete) {
throw new Error("Must not call build.getTypeMetaByName before 'build' phase is complete; use 'init' phase instead");
}
// Meta for builtins
switch (typeName) {
case "String":
case "ID":
case "Boolean":
case "Int":
case "Float":
return Object.assign(Object.create(null), {
Constructor: graphql_1.GraphQLScalarType,
scope: Object.freeze({}),
origin: "GraphQL builtin",
specGenerator: () => {
switch (typeName) {
case "String":
return graphql_1.GraphQLString.toConfig();
case "ID":
return graphql_1.GraphQLID.toConfig();
case "Boolean":
return graphql_1.GraphQLBoolean.toConfig();
case "Int":
return graphql_1.GraphQLInt.toConfig();
case "Float":
return graphql_1.GraphQLFloat.toConfig();
default: {
throw new Error(`Unhandled built-in '${typeName}'`);
}
}
},
});
}
const details = typeRegistry[typeName];
if (details != null) {
const { klass: Constructor, scope, origin, specGenerator } = details;
return Object.assign(Object.create(null), {
Constructor,
scope,
origin,
specGenerator,
});
}
return null;
},
getTypeByName(typeName) {
if (currentTypeDetails &&
currentTypeDetails.klass !== graphql_1.GraphQLScalarType &&
!BUILTINS.includes(typeName)) {
const details = typeRegistry[typeName];
if (!details ||
(details.klass !== graphql_1.GraphQLScalarType &&
details.klass !== graphql_1.GraphQLEnumType)) {
throw new Error(`Error in spec callback for ${currentTypeDetails.klass.name} '${currentTypeDetails.typeName}'; the callback made a call to \`build.getTypeByName(${JSON.stringify(typeName)})\` (directly or indirectly) - this is the wrong time for such a call \
to occur since it can lead to circular dependence. To fix this, ensure that any \
calls to \`getTypeByName\` can only occur inside of the callbacks, such as \
\`fields()\`, \`interfaces()\`, \`types()\`, \`values()\` or similar. Be sure to use the callback \
style for these configuration options (e.g. change \`interfaces: \
[getTypeByName('Foo')]\` to \`interfaces: () => [getTypeByName('Foo')]\``);
}
}
if (!this.status.isInitPhaseComplete) {
throw new Error(mustUseThunkMessage("build.getTypeByName"));
}
if (typeName in allTypes) {
return allTypes[typeName];
}
else if (building.has(typeName)) {
throw new Error(`Construction cycle detected: ${typeName} is already being built (build stack: ${[
...building,
].join(">")}). Most likely this means that you forgot to use a callback for 'fields', 'interfaces', 'types', etc. when defining a type.`);
}
else {
building.add(typeName);
try {
const details = typeRegistry[typeName];
if (details != null) {
const { klass, scope } = details;
const spec = generateSpecFromDetails(details);
// No need to have the user specify name, and they're forbidden from
// changing name (use inflection instead!) so we just set it
// ourselves:
spec.name = typeName;
const finishedBuild = build;
const type = builder.newWithHooks(finishedBuild, klass, spec, scope);
allTypes[typeName] = type;
if (klass === graphql_1.GraphQLObjectType ||
klass === graphql_1.GraphQLInputObjectType) {
// Perform fields check. It's critical that `allTypes[typeName]` is
// set above this to prevent infinite loops in case one of our
// fields is dependent on another type, which is in turn dependent
// on this type - in this case we know there's at least one field
// otherwise the conflict would not occur?
if (Object.keys(type.getFields()).length === 0) {
allTypes[typeName] = null;
return null;
}
}
if (klass === graphql_1.GraphQLEnumType) {
// Perform values check.
if (Object.keys(type.getValues()).length === 0) {
allTypes[typeName] = null;
return null;
}
}
return type;
}
else {
allTypes[typeName] = undefined;
return undefined;
}
}
catch (e) {
// Error occurred, store null
allTypes[typeName] = null;
// Rethrow error
throw e;
}
finally {
building.delete(typeName);
}
}
},
getInputTypeByName(typeName) {
if (!this.status.isInitPhaseComplete) {
throw new Error(mustUseThunkMessage("build.getInputTypeByName"));
}
const type = this.getTypeByName(typeName);
if (!type) {
throw new Error(`Expected an input type named '${typeName}', but ${this.getTypeMetaByName(typeName)
? `that type was not successfully constructed; typically this is because it ended up with no fields.`
: `a type with that name has not been registered.`}`);
}
if (!(0, graphql_1.isInputType)(type)) {
throw new Error(`Expected '${typeName}' to be an input type, but it isn't (${String(type)})`);
}
return type;
},
getOutputTypeByName(typeName) {
if (!this.status.isInitPhaseComplete) {
throw new Error(mustUseThunkMessage("build.getOutputTypeByName"));
}
const type = this.getTypeByName(typeName);
if (!type) {
throw new Error(`Expected an output type named '${typeName}', but ${this.getTypeMetaByName(typeName)
? `that type was not successfully constructed; typically this is because it ended up with no fields.`
: `a type with that name has not been registered.`}`);
}
if (!(0, graphql_1.isOutputType)(type)) {
throw new Error(`Expected '${typeName}' to be an output type, but it isn't (${String(type)})`);
}
return type;
},
nullableIf(condition, type) {
if (condition) {
return type;
}
else {
return new graphql_1.GraphQLNonNull(type);
}
},
assertValidName(name, message, args = [], allowDoubleUnderscorePrefix = false) {
try {
(0, graphql_1.assertName)(name);
if (!allowDoubleUnderscorePrefix && name.startsWith("__")) {
throw new Error(`Names beginning with two underscores are reserved for introspection.`);
}
}
catch (e) {
const placeholders = [name, ...args];
const msg = message.replace(/\$([0-9]+)/g, (_, n) => n == "0" ? JSON.stringify(name) : (placeholders[n] ?? _));
throw new TypeError(msg + " " + e.message);
}
return name;
},
_pluginMeta: Object.create(null),
};
return build;
}
let currentTypeDetails = null;
function generateSpecFromDetails(details) {
currentTypeDetails = details;
try {
return details.specGenerator();
}
finally {
currentTypeDetails = null;
}
}
function mustUseThunkMessage(fn) {
return ("Must not call " +
fn +
" before 'init' phase is complete; please be sure to use the 'thunk' configuration method for GraphQL types (e.g. instead of `fields: { /* ... */ }` use `fields: () => ({ /* ... */ })`), and be sure to only call " +
fn +
" from inside a thunk.");
}
//# sourceMappingURL=makeNewBuild.js.map