@netlify/content-engine
Version:
1,110 lines • 54.6 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildSchema = void 0;
const lodash_camelcase_1 = __importDefault(require("lodash.camelcase"));
const lodash_isempty_1 = __importDefault(require("lodash.isempty"));
const invariant_1 = __importDefault(require("invariant"));
const graphql_1 = require("graphql");
const graphql_compose_1 = require("graphql-compose");
const datastore_1 = require("../datastore");
const api_runner_node_1 = __importDefault(require("../utils/api-runner-node"));
const reporter_1 = __importDefault(require("../reporter"));
const node_interface_1 = require("./types/node-interface");
const built_in_types_1 = require("./types/built-in-types");
const index_1 = require("./infer/index");
const remote_file_interface_1 = require("./types/remote-file-interface");
const resolvers_1 = require("./resolvers");
const extensions_1 = require("./extensions");
const pagination_1 = require("./types/pagination");
const sort_1 = require("./types/sort");
const filter_1 = require("./types/filter");
const type_builders_1 = require("./types/type-builders");
const type_defs_1 = require("./types/type-defs");
const print_1 = require("./print");
const buildSchema = async ({ schemaComposer, types, typeMapping, fieldExtensions, thirdPartySchemas, printConfig, enginePrintConfig, typeConflictReporter, inferenceMetadata, parentSpan, }) => {
// FIXME: consider removing .ready here - it is needed for various tests to pass (although probably harmless)
await (0, datastore_1.getDataStore)().ready();
await updateSchemaComposer({
schemaComposer,
types,
typeMapping,
fieldExtensions,
thirdPartySchemas,
printConfig,
enginePrintConfig,
typeConflictReporter,
inferenceMetadata,
parentSpan,
});
const schema = schemaComposer.buildSchema();
freezeTypeComposers(schemaComposer);
// const { printSchema } = require(`graphql`);
// console.log(`SCHEMA_PRINTED`, printSchema(schema));
return schema;
};
exports.buildSchema = buildSchema;
// Workaround for https://github.com/graphql-compose/graphql-compose/issues/319
// FIXME: remove this when fixed in graphql-compose
const freezeTypeComposers = (schemaComposer, excluded = new Set()) => {
Array.from(schemaComposer.values()).forEach((tc) => {
const isCompositeTC = tc instanceof graphql_compose_1.ObjectTypeComposer || tc instanceof graphql_compose_1.InterfaceTypeComposer;
if (isCompositeTC && !excluded.has(tc.getTypeName())) {
// typeComposer.getType() actually mutates the underlying GraphQL type
// and always re-assigns type._fields with a thunk.
// It causes continuous redundant field re-definitions when running queries
// (affects performance significantly).
// Prevent the mutation and "freeze" the type:
const type = tc.getType();
// @ts-ignore
tc.getType = () => type;
}
});
};
const updateSchemaComposer = async ({ schemaComposer, types, typeMapping, fieldExtensions, thirdPartySchemas, printConfig, enginePrintConfig, typeConflictReporter, inferenceMetadata, parentSpan, }) => {
let activity = reporter_1.default.phantomActivity(`Add explicit types`, {
parentSpan: parentSpan,
});
activity.start();
await addTypes({ schemaComposer, parentSpan: activity.span, types });
activity.end();
activity = reporter_1.default.phantomActivity(`Add inferred types`, {
parentSpan: parentSpan,
});
activity.start();
(0, index_1.addInferredTypes)({
schemaComposer,
typeConflictReporter,
typeMapping,
inferenceMetadata,
// parentSpan: activity.span,
});
addInferredChildOfExtensions({
schemaComposer,
});
activity.end();
activity = reporter_1.default.phantomActivity(`Processing types`, {
parentSpan: parentSpan,
});
activity.start();
if (!process.env.GATSBY_SKIP_WRITING_SCHEMA_TO_FILE) {
await (0, print_1.printTypeDefinitions)({
config: printConfig,
schemaComposer,
// parentSpan: activity.span,
});
if (enginePrintConfig) {
// make sure to print schema that will be used when bundling graphql-engine
await (0, print_1.printTypeDefinitions)({
config: enginePrintConfig,
schemaComposer,
// parentSpan: activity.span,
});
}
}
await addSetFieldsOnGraphQLNodeTypeFields({
schemaComposer,
parentSpan: activity.span,
});
await addConvenienceChildrenFields({
schemaComposer,
// parentSpan: activity.span,
});
await Promise.all(
// @ts-ignore
Array.from(new Set(schemaComposer.values())).map((typeComposer) => processTypeComposer({
schemaComposer,
typeComposer,
fieldExtensions,
parentSpan: activity.span,
})));
await checkQueryableInterfaces({
schemaComposer,
// parentSpan: activity.span
});
await addThirdPartySchemas({
schemaComposer,
thirdPartySchemas,
// @ts-ignore
parentSpan: activity.span,
});
await addCustomResolveFunctions({
schemaComposer,
parentSpan: activity.span,
});
attachTracingResolver({
schemaComposer,
// parentSpan: activity.span
});
// TODO: instead of patching the schema like this we should prevent these types from being added to the schema.
// I haven't done that because these node types seem to be inferred, the node data is needed for caching/replication, and I don't want to break things and have no time right now.
//
// these fields and types are a security risk so they're removed
schemaComposer.Query.removeField([
`sitePlugin`,
`allSitePlugin`,
`site`,
`allSite`,
`allSiteBuildMetadata`,
]);
schemaComposer.delete(`SitePlugin`);
schemaComposer.delete(`Site`);
schemaComposer.delete(`SiteBuildMetadata`);
// TODO: end
activity.end();
};
const processTypeComposer = async ({ schemaComposer, typeComposer, fieldExtensions, parentSpan, }) => {
if (typeComposer instanceof graphql_compose_1.ObjectTypeComposer) {
await (0, extensions_1.processFieldExtensions)({
schemaComposer,
typeComposer,
fieldExtensions,
parentSpan,
});
if (typeComposer.hasInterface(`Node`)) {
(0, node_interface_1.addNodeInterfaceFields)({ schemaComposer, typeComposer });
}
if (typeComposer.hasInterface(`RemoteFile`)) {
(0, remote_file_interface_1.addRemoteFileInterfaceFields)(schemaComposer, typeComposer);
}
determineSearchableFields({
schemaComposer,
typeComposer,
// parentSpan,
});
if (typeComposer.hasInterface(`Node`)) {
addTypeToRootQuery({
schemaComposer,
typeComposer,
// parentSpan
});
}
}
else if (typeComposer instanceof graphql_compose_1.InterfaceTypeComposer) {
if (isNodeInterface(typeComposer)) {
(0, node_interface_1.addNodeInterfaceFields)({
schemaComposer,
// @ts-ignore
typeComposer,
// parentSpan
});
// We only process field extensions for queryable Node interfaces, so we get
// the input args on the root query type, e.g. `formatString` etc. for `dateformat`
(0, extensions_1.processFieldExtensions)({
schemaComposer,
typeComposer,
fieldExtensions,
parentSpan,
});
determineSearchableFields({
schemaComposer,
typeComposer,
// parentSpan,
});
addTypeToRootQuery({
schemaComposer,
typeComposer,
// parentSpan
});
}
}
};
const fieldNames = {
query: (typeName) => (0, lodash_camelcase_1.default)(typeName),
queryAll: (typeName) => (0, lodash_camelcase_1.default)(`all ${typeName}`),
convenienceChild: (typeName) => (0, lodash_camelcase_1.default)(`child ${typeName}`),
convenienceChildren: (typeName) => (0, lodash_camelcase_1.default)(`children ${typeName}`),
};
const addTypes = ({ schemaComposer, types, parentSpan }) => {
types.forEach(({ typeOrTypeDef, plugin }) => {
if (typeof typeOrTypeDef === `string`) {
typeOrTypeDef = (0, type_defs_1.parseTypeDef)(typeOrTypeDef);
}
if ((0, type_defs_1.isASTDocument)(typeOrTypeDef)) {
let parsedTypes;
const createdFrom = `sdl`;
try {
parsedTypes = parseTypes({
doc: typeOrTypeDef,
plugin,
createdFrom,
schemaComposer,
parentSpan,
});
}
catch (error) {
(0, type_defs_1.reportParsingError)(error);
return;
}
parsedTypes.forEach((type) => {
processAddedType({
schemaComposer,
type,
// parentSpan,
createdFrom,
plugin,
});
});
}
else if ((0, type_builders_1.isGatsbyType)(typeOrTypeDef)) {
const type = createTypeComposerFromGatsbyType({
schemaComposer,
type: typeOrTypeDef,
// parentSpan,
});
if (type) {
const typeName = type.getTypeName();
const createdFrom = `typeBuilder`;
checkIsAllowedTypeName(typeName);
if (schemaComposer.has(typeName)) {
const typeComposer = schemaComposer.get(typeName);
mergeTypes({
schemaComposer,
typeComposer,
type,
plugin,
createdFrom,
// parentSpan,
});
}
else {
processAddedType({
schemaComposer,
type,
// parentSpan,
createdFrom,
plugin,
});
}
}
}
else {
const typeName = typeOrTypeDef.name;
const createdFrom = `graphql-js`;
checkIsAllowedTypeName(typeName);
if (schemaComposer.has(typeName)) {
const typeComposer = schemaComposer.get(typeName);
mergeTypes({
schemaComposer,
typeComposer,
type: typeOrTypeDef,
plugin,
createdFrom,
// parentSpan,
});
}
else {
processAddedType({
schemaComposer,
type: typeOrTypeDef,
// parentSpan,
createdFrom,
plugin,
});
}
}
});
};
const mergeTypes = ({ schemaComposer, typeComposer, type, plugin, createdFrom,
// parentSpan,
}) => {
// The merge is considered safe when a user or a plugin owning the type extend this type
// TODO: add proper conflicts detection and reporting (on the field level)
const typeOwner = typeComposer.getExtension(`plugin`);
const isSafeMerge = !plugin ||
plugin.name === `default-site-plugin` ||
plugin.name === typeOwner;
if (!isSafeMerge) {
if (typeOwner) {
reporter_1.default.warn(`Plugin \`${plugin.name}\` has customized the GraphQL type ` +
`\`${typeComposer.getTypeName()}\`, which has already been defined ` +
`by the plugin \`${typeOwner}\`. ` +
`This could potentially cause conflicts.`);
}
else {
reporter_1.default.warn(`Plugin \`${plugin.name}\` has customized the built-in Gatsby GraphQL type ` +
`\`${typeComposer.getTypeName()}\`. ` +
`This is allowed, but could potentially cause conflicts.`);
}
}
if (type instanceof graphql_compose_1.ObjectTypeComposer ||
type instanceof graphql_compose_1.InterfaceTypeComposer ||
type instanceof graphql_1.GraphQLObjectType ||
type instanceof graphql_1.GraphQLInterfaceType) {
mergeFields({ typeComposer, fields: type.getFields() });
type.getInterfaces().forEach((iface) => typeComposer.addInterface(iface));
}
if (type instanceof graphql_1.GraphQLInterfaceType ||
type instanceof graphql_compose_1.InterfaceTypeComposer ||
type instanceof graphql_1.GraphQLUnionType ||
type instanceof graphql_compose_1.UnionTypeComposer) {
mergeResolveType({ typeComposer, type });
}
let extensions = {};
if (isNamedTypeComposer(type)) {
if (createdFrom === `sdl`) {
extensions = convertDirectivesToExtensions(type, type.getDirectives());
}
else {
typeComposer.extendExtensions(type.getExtensions());
}
}
addExtensions({
schemaComposer,
typeComposer,
extensions,
plugin,
createdFrom,
});
return true;
};
const processAddedType = ({ schemaComposer, type,
// parentSpan,
createdFrom, plugin, }) => {
const typeName = schemaComposer.add(type);
const typeComposer = schemaComposer.get(typeName);
if (typeComposer instanceof graphql_compose_1.InterfaceTypeComposer ||
typeComposer instanceof graphql_compose_1.UnionTypeComposer) {
if (!typeComposer.getResolveType()) {
typeComposer.setResolveType((node) => {
return node?.internal?.type || node?.__typename;
});
}
}
schemaComposer.addSchemaMustHaveType(typeComposer);
let extensions = {};
if (createdFrom === `sdl`) {
extensions = convertDirectivesToExtensions(typeComposer, typeComposer.getDirectives());
}
addExtensions({
schemaComposer,
typeComposer,
extensions,
plugin,
createdFrom,
});
return typeComposer;
};
/**
* @param {import("graphql-compose").AnyTypeComposer} typeComposer
* @param {Array<import("graphql-compose").Directive>} directives
* @return {{infer?: boolean, mimeTypes?: { types: Array<string> }, childOf?: { types: Array<string> }, nodeInterface?: boolean}}
*/
const convertDirectivesToExtensions = (typeComposer, directives) => {
const extensions = {};
directives.forEach(({ name, args }) => {
switch (name) {
case `infer`:
case `dontInfer`: {
extensions[`infer`] = name === `infer`;
break;
}
case `mimeTypes`:
extensions[`mimeTypes`] = args;
break;
case `childOf`:
extensions[`childOf`] = args;
break;
case `nodeInterface`:
if (typeComposer instanceof graphql_compose_1.InterfaceTypeComposer) {
extensions[`nodeInterface`] = true;
}
break;
case `authorization`:
extensions[`authorization`] = args;
break;
case `runtime`:
extensions[`runtime`] = true;
break;
default:
}
});
return extensions;
};
const addExtensions = ({ schemaComposer, typeComposer, extensions = {}, plugin, createdFrom, }) => {
typeComposer.setExtension(`createdFrom`, createdFrom);
typeComposer.setExtension(`plugin`, plugin ? plugin.name : null);
typeComposer.extendExtensions(extensions);
if (typeComposer instanceof graphql_compose_1.InterfaceTypeComposer &&
isNodeInterface(typeComposer)) {
const hasCorrectIdField = typeComposer.hasField(`id`) &&
typeComposer.getFieldType(`id`).toString() === `ID!`;
if (!hasCorrectIdField) {
reporter_1.default.panic(`Interfaces with the \`nodeInterface\` extension must have a field ` +
`\`id\` of type \`ID!\`. Check the type definition of ` +
`\`${typeComposer.getTypeName()}\`.`);
}
}
if (typeComposer instanceof graphql_compose_1.ObjectTypeComposer ||
typeComposer instanceof graphql_compose_1.InterfaceTypeComposer ||
typeComposer instanceof graphql_compose_1.InputTypeComposer) {
typeComposer.getFieldNames().forEach((fieldName) => {
typeComposer.setFieldExtension(fieldName, `createdFrom`, createdFrom);
typeComposer.setFieldExtension(fieldName, `plugin`, plugin ? plugin.name : null);
if (createdFrom === `sdl`) {
const directives = typeComposer.getFieldDirectives(fieldName);
directives.forEach(({ name, args }) => {
typeComposer.setFieldExtension(fieldName, name, args);
});
}
// Validate field extension args. `graphql-compose` already checks the
// type of directive args in `parseDirectives`, but we want to check
// extensions provided with type builders as well. Also, we warn if an
// extension option was provided which does not exist in the field
// extension definition.
const fieldExtensions = typeComposer.getFieldExtensions(fieldName);
const typeName = typeComposer.getTypeName();
Object.keys(fieldExtensions)
.filter((name) => !extensions_1.internalExtensionNames.includes(name))
.forEach((name) => {
const args = fieldExtensions[name];
if (!args || typeof args !== `object`) {
reporter_1.default.error(`Field extension arguments must be provided as an object. ` +
`Received "${args}" on \`${typeName}.${fieldName}\`.`);
return;
}
try {
const definition = schemaComposer.getDirective(name);
// Handle `defaultValue` when not provided as directive
definition.args.forEach(({ name, defaultValue }) => {
if (args[name] === undefined && defaultValue !== undefined) {
args[name] = defaultValue;
}
});
Object.keys(args).forEach((arg) => {
const argumentDef = definition.args.find(({ name }) => name === arg);
if (!argumentDef) {
reporter_1.default.error(`Field extension \`${name}\` on \`${typeName}.${fieldName}\` ` +
`has invalid argument \`${arg}\`.`);
return;
}
const value = args[arg];
try {
validate(argumentDef.type, value);
}
catch (error) {
reporter_1.default.error(`Field extension \`${name}\` on \`${typeName}.${fieldName}\` ` +
`has argument \`${arg}\` with invalid value "${value}". ` +
error.message);
}
});
}
catch (error) {
reporter_1.default.error(`Field extension \`${name}\` on \`${typeName}.${fieldName}\` ` +
`is not available.`);
}
});
});
}
return typeComposer;
};
const checkIsAllowedTypeName = (name) => {
(0, invariant_1.default)(name !== `Node`, `The GraphQL type \`Node\` is reserved for internal use.`);
(0, invariant_1.default)(!name.endsWith(`FilterInput`) && !name.endsWith(`SortInput`), `GraphQL type names ending with "FilterInput" or "SortInput" are ` +
`reserved for internal use. Please rename \`${name}\`.`);
(0, invariant_1.default)(!built_in_types_1.builtInScalarTypeNames.includes(name), `The GraphQL type \`${name}\` is reserved for internal use by ` +
`built-in scalar types.`);
(0, graphql_1.assertValidName)(name);
};
const createTypeComposerFromGatsbyType = ({ schemaComposer, type }) => {
let typeComposer;
switch (type.kind) {
case type_builders_1.GatsbyGraphQLTypeKind.OBJECT: {
typeComposer = graphql_compose_1.ObjectTypeComposer.createTemp({
...type.config,
fields: () => schemaComposer.typeMapper.convertOutputFieldConfigMap(type.config.fields),
interfaces: () => {
if (type.config.interfaces) {
return type.config.interfaces.map((iface) => {
if (typeof iface === `string`) {
// Sadly, graphql-compose runs this function too early - before we have
// all of those interfaces actually created in the schema, so have to create
// a temporary placeholder composer :/
if (!schemaComposer.has(iface)) {
const tmpComposer = schemaComposer.createInterfaceTC(iface);
tmpComposer.setExtension(`isPlaceholder`, true);
return tmpComposer;
}
return schemaComposer.getIFTC(iface);
}
else {
return iface;
}
});
}
else {
return [];
}
},
});
break;
}
case type_builders_1.GatsbyGraphQLTypeKind.INPUT_OBJECT: {
typeComposer = graphql_compose_1.InputTypeComposer.createTemp({
...type.config,
fields: schemaComposer.typeMapper.convertInputFieldConfigMap(type.config.fields),
});
break;
}
case type_builders_1.GatsbyGraphQLTypeKind.UNION: {
typeComposer = graphql_compose_1.UnionTypeComposer.createTemp({
...type.config,
types: () => {
if (type.config.types) {
return type.config.types.map((typeName) => {
if (!schemaComposer.has(typeName)) {
// Sadly, graphql-compose runs this function too early - before we have
// all of those types actually created in the schema, so have to create
// a temporary placeholder composer :/
const tmpComposer = schemaComposer.createObjectTC(typeName);
tmpComposer.setExtension(`isPlaceholder`, true);
return tmpComposer;
}
return schemaComposer.getOTC(typeName);
});
}
else {
return [];
}
},
});
break;
}
case type_builders_1.GatsbyGraphQLTypeKind.INTERFACE: {
typeComposer = graphql_compose_1.InterfaceTypeComposer.createTemp({
...type.config,
fields: () => schemaComposer.typeMapper.convertOutputFieldConfigMap(type.config.fields),
interfaces: () => {
if (type.config.interfaces) {
return type.config.interfaces.map((iface) => {
if (typeof iface === `string`) {
// Sadly, graphql-compose runs this function too early - before we have
// all of those interfaces actually created in the schema, so have to create
// a temporary placeholder composer :/
if (!schemaComposer.has(iface)) {
const tmpComposer = schemaComposer.createInterfaceTC(iface);
tmpComposer.setExtension(`isPlaceholder`, true);
return tmpComposer;
}
return schemaComposer.getIFTC(iface);
}
else {
return iface;
}
});
}
else {
return [];
}
},
});
break;
}
case type_builders_1.GatsbyGraphQLTypeKind.ENUM: {
typeComposer = graphql_compose_1.EnumTypeComposer.createTemp(type.config);
break;
}
case type_builders_1.GatsbyGraphQLTypeKind.SCALAR: {
typeComposer = graphql_compose_1.ScalarTypeComposer.createTemp(type.config);
break;
}
default: {
reporter_1.default.warn(`Illegal type definition: ${JSON.stringify(type.config)}`);
typeComposer = null;
}
}
if (typeComposer) {
// Workaround for https://github.com/graphql-compose/graphql-compose/issues/311
typeComposer.schemaComposer = schemaComposer;
}
return typeComposer;
};
const addSetFieldsOnGraphQLNodeTypeFields = ({ schemaComposer, parentSpan }) => Promise.all(Array.from(schemaComposer.values()).map(async (tc) => {
if (tc instanceof graphql_compose_1.ObjectTypeComposer && tc.hasInterface(`Node`)) {
const typeName = tc.getTypeName();
const result = await (0, api_runner_node_1.default)(`setFieldsOnGraphQLNodeType`, {
type: {
name: typeName,
get nodes() {
// TODO STRICT_MODE: return iterator instead of array
return (0, datastore_1.getNodesByType)(typeName);
},
},
traceId: `initial-setFieldsOnGraphQLNodeType`,
parentSpan,
});
if (result) {
// NOTE: `setFieldsOnGraphQLNodeType` only allows setting
// nested fields with a path as property name, i.e.
// `{ 'frontmatter.published': 'Boolean' }`, but not in the form
// `{ frontmatter: { published: 'Boolean' }}`
// @ts-ignore
result.forEach((fields) => tc.addNestedFields(fields));
}
}
}));
const addThirdPartySchemas = ({ schemaComposer, thirdPartySchemas,
// parentSpan,
}) => {
thirdPartySchemas.forEach((schema) => {
const schemaQueryType = schema.getQueryType();
const schemaMutationType = schema.getMutationType();
if (schemaMutationType) {
const mutationTC = schemaComposer.createTempTC(schemaMutationType);
schemaComposer.Mutation.addFields(mutationTC.getFields());
}
const queryTC = schemaComposer.createTempTC(schemaQueryType);
processThirdPartyTypeFields({
typeComposer: queryTC,
type: schemaQueryType,
schemaQueryType,
});
schemaComposer.Query.addFields(queryTC.getFields());
// Explicitly add the third-party schema's types, so they can be targeted
// in `createResolvers` API.
const types = schema.getTypeMap();
Object.keys(types).forEach((typeName) => {
const type = types[typeName];
if (type !== schemaMutationType &&
type !== schemaQueryType &&
!(0, graphql_1.isSpecifiedScalarType)(type) &&
!(0, graphql_1.isIntrospectionType)(type) &&
type.name !== `Date` &&
type.name !== `JSON`) {
const typeHasFields = type instanceof graphql_1.GraphQLObjectType ||
type instanceof graphql_1.GraphQLInterfaceType;
// Workaround for an edge case typical for Relay Classic-compatible schemas.
// For example, GitHub API contains this piece:
// type Query { relay: Query }
// And gatsby-source-graphql transforms it to:
// type Query { github: GitHub }
// type GitHub { relay: Query }
// The problem:
// schemaComposer.createTC(type) for type `GitHub` will eagerly create type composers
// for all fields (including `relay` and it's type: `Query` of the third-party schema)
// This unexpected `Query` composer messes up with our own Query type composer and produces duplicate types.
// The workaround is to make sure fields of the GitHub type are lazy and are evaluated only when
// this Query type is already replaced with our own root `Query` type (see processThirdPartyTypeFields):
// @ts-ignore
if (typeHasFields && typeof type._fields === `object`) {
// @ts-ignore
const fields = type._fields;
// @ts-ignore
type._fields = () => fields;
}
// ^^^ workaround done
const typeComposer = schemaComposer.createTC(type);
if (typeHasFields) {
processThirdPartyTypeFields({
typeComposer,
type,
schemaQueryType,
});
}
typeComposer.setExtension(`createdFrom`, `thirdPartySchema`);
schemaComposer.addSchemaMustHaveType(typeComposer);
}
});
});
};
const resetOverriddenThirdPartyTypeFields = ({ typeComposer }) => {
// The problem: createResolvers API mutates third party schema instance.
// For example it can add a new field referencing a type from our main schema
// Then if we rebuild the schema this old type instance will sneak into
// the new schema and produce the famous error:
// "Schema must contain uniquely named types but contains multiple types named X"
// This function only affects schema rebuilding pathway.
// It cleans up artifacts created by the `createResolvers` API of the previous build
// so that we return the third party schema to its initial state (hence can safely re-add)
// TODO: the right way to fix this would be not to mutate the third party schema in
// the first place. But unfortunately mutation happens in the `graphql-compose`
// and we don't have an easy way to avoid it without major rework
typeComposer.getFieldNames().forEach((fieldName) => {
const createdFrom = typeComposer.getFieldExtension(fieldName, `createdFrom`);
if (createdFrom === `createResolvers`) {
typeComposer.removeField(fieldName);
return;
}
const config = typeComposer.getFieldExtension(fieldName, `originalFieldConfig`);
if (config) {
typeComposer.removeField(fieldName);
typeComposer.addFields({
[fieldName]: config,
});
}
});
};
const processThirdPartyTypeFields = ({ typeComposer, type, schemaQueryType, }) => {
// Fix for types that refer to Query. Thanks Relay Classic!
const fields = type.getFields();
Object.keys(fields).forEach((fieldName) => {
// Remove customization that we could have added via `createResolvers`
// to make it work with schema rebuilding
const fieldType = String(fields[fieldName].type);
if (fieldType.replace(/[[\]!]/g, ``) === schemaQueryType.name) {
typeComposer.extendField(fieldName, {
type: fieldType.replace(schemaQueryType.name, `Query`),
});
}
});
resetOverriddenThirdPartyTypeFields({ typeComposer });
};
const addCustomResolveFunctions = async ({ schemaComposer, parentSpan }) => {
const intermediateSchema = schemaComposer.buildSchema();
const createResolvers = (resolvers, { ignoreNonexistentTypes = false } = {}) => {
Object.keys(resolvers).forEach((typeName) => {
const fields = resolvers[typeName];
if (schemaComposer.has(typeName)) {
const tc = schemaComposer.getOTC(typeName);
Object.keys(fields).forEach((fieldName) => {
const fieldConfig = fields[fieldName];
if (tc.hasField(fieldName)) {
const originalFieldConfig = tc.getFieldConfig(fieldName);
const originalTypeName = originalFieldConfig.type.toString();
const originalResolver = originalFieldConfig.resolve;
let fieldTypeName;
if (fieldConfig.type) {
fieldTypeName = Array.isArray(fieldConfig.type)
? stringifyArray(fieldConfig.type)
: fieldConfig.type.toString();
}
if (!fieldTypeName ||
fieldTypeName.replace(/!/g, ``) ===
originalTypeName.replace(/!/g, ``) ||
tc.getExtension(`createdFrom`) === `thirdPartySchema`) {
const newConfig = {};
if (fieldConfig.type) {
// @ts-ignore
newConfig.type = fieldConfig.type;
}
if (fieldConfig.args) {
// @ts-ignore
newConfig.args = fieldConfig.args;
}
if (fieldConfig.resolve) {
// @ts-ignore
newConfig.resolve = (source, args, context, info) => fieldConfig.resolve(source, args, context, {
...info,
originalResolver: originalResolver || context.defaultFieldResolver,
});
tc.extendFieldExtensions(fieldName, {
needsResolve: true,
});
}
tc.extendField(fieldName, newConfig);
// See resetOverriddenThirdPartyTypeFields for explanation
if (tc.getExtension(`createdFrom`) === `thirdPartySchema`) {
tc.setFieldExtension(fieldName, `originalFieldConfig`, originalFieldConfig);
}
}
else if (fieldTypeName) {
reporter_1.default.warn(`\`createResolvers\` passed resolvers for field ` +
`\`${typeName}.${fieldName}\` with type \`${fieldTypeName}\`. ` +
`Such a field with type \`${originalTypeName}\` already exists ` +
`on the type. Use \`createTypes\` to override type fields.`);
}
}
else {
tc.addFields({
[fieldName]: fieldConfig,
});
// See resetOverriddenThirdPartyTypeFields for explanation
tc.setFieldExtension(fieldName, `createdFrom`, `createResolvers`);
}
});
}
else if (!ignoreNonexistentTypes) {
reporter_1.default.warn(`\`createResolvers\` passed resolvers for type \`${typeName}\` that ` +
`doesn't exist in the schema. Use \`createTypes\` to add the type ` +
`before adding resolvers.`);
}
});
};
await (0, api_runner_node_1.default)(`createResolvers`, {
intermediateSchema,
createResolvers,
traceId: `initial-createResolvers`,
parentSpan,
});
};
function attachTracingResolver({ schemaComposer }) {
schemaComposer.forEach((typeComposer) => {
if (typeComposer instanceof graphql_compose_1.ObjectTypeComposer ||
typeComposer instanceof graphql_compose_1.InterfaceTypeComposer) {
typeComposer.getFieldNames().forEach((fieldName) => {
const field = typeComposer.getField(fieldName);
const resolver = (0, resolvers_1.wrappingResolver)(field.resolve || resolvers_1.defaultResolver);
typeComposer.extendField(fieldName, {
resolve: resolver,
});
});
}
});
}
const determineSearchableFields = ({ schemaComposer: _schemaComposer, typeComposer, }) => {
const isRuntime = typeComposer.hasExtension(`runtime`);
typeComposer.getFieldNames().forEach((fieldName) => {
const field = typeComposer.getField(fieldName);
const extensions = typeComposer.getFieldExtensions(fieldName);
if (field.resolve) {
if (extensions.dateformat) {
typeComposer.extendFieldExtensions(fieldName, {
searchable: filter_1.SEARCHABLE_ENUM.SEARCHABLE,
sortable: sort_1.SORTABLE_ENUM.SORTABLE,
needsResolve: extensions.proxy ? true : false,
runtime: !!isRuntime,
});
}
else if (!(0, lodash_isempty_1.default)(field.args)) {
typeComposer.extendFieldExtensions(fieldName, {
searchable: filter_1.SEARCHABLE_ENUM.DEPRECATED_SEARCHABLE,
sortable: sort_1.SORTABLE_ENUM.DEPRECATED_SORTABLE,
needsResolve: true,
runtime: !!isRuntime,
});
}
else {
typeComposer.extendFieldExtensions(fieldName, {
searchable: filter_1.SEARCHABLE_ENUM.SEARCHABLE,
sortable: sort_1.SORTABLE_ENUM.SORTABLE,
needsResolve: true,
runtime: !!isRuntime,
});
}
}
else {
typeComposer.extendFieldExtensions(fieldName, {
searchable: filter_1.SEARCHABLE_ENUM.SEARCHABLE,
sortable: sort_1.SORTABLE_ENUM.SORTABLE,
needsResolve: false,
runtime: !!isRuntime,
});
}
});
};
const addConvenienceChildrenFields = ({ schemaComposer }) => {
const parentTypesToChildren = new Map();
const mimeTypesToChildren = new Map();
const typesHandlingMimeTypes = new Map();
schemaComposer.forEach((type) => {
if ((type instanceof graphql_compose_1.ObjectTypeComposer ||
type instanceof graphql_compose_1.InterfaceTypeComposer) &&
type.hasExtension(`mimeTypes`)) {
// @ts-ignore
const { types } = type.getExtension(`mimeTypes`);
new Set(types).forEach((mimeType) => {
if (!typesHandlingMimeTypes.has(mimeType)) {
typesHandlingMimeTypes.set(mimeType, new Set());
}
typesHandlingMimeTypes.get(mimeType).add(type);
});
}
if ((type instanceof graphql_compose_1.ObjectTypeComposer ||
type instanceof graphql_compose_1.InterfaceTypeComposer) &&
type.hasExtension(`childOf`)) {
if (type instanceof graphql_compose_1.ObjectTypeComposer && !type.hasInterface(`Node`)) {
reporter_1.default.error(`The \`childOf\` extension can only be used on types that implement the \`Node\` interface.\n` +
`Check the type definition of \`${type.getTypeName()}\`.`);
return;
}
if (type instanceof graphql_compose_1.InterfaceTypeComposer && !isNodeInterface(type)) {
reporter_1.default.error(`The \`childOf\` extension can only be used on types that implement the \`Node\` interface.\n` +
`Check the type definition of \`${type.getTypeName()}\`.`);
return;
}
// @ts-ignore
const { types, mimeTypes } = type.getExtension(`childOf`);
new Set(types).forEach((parentType) => {
if (!parentTypesToChildren.has(parentType)) {
parentTypesToChildren.set(parentType, new Set());
}
parentTypesToChildren.get(parentType).add(type);
});
new Set(mimeTypes).forEach((mimeType) => {
if (!mimeTypesToChildren.has(mimeType)) {
mimeTypesToChildren.set(mimeType, new Set());
}
mimeTypesToChildren.get(mimeType).add(type);
});
}
});
parentTypesToChildren.forEach((children, parent) => {
if (!schemaComposer.has(parent))
return;
const typeComposer = schemaComposer.getAnyTC(parent);
if (typeComposer instanceof graphql_compose_1.InterfaceTypeComposer &&
!isNodeInterface(typeComposer)) {
reporter_1.default.error(`With the \`childOf\` extension, children fields can only be added to ` +
`interfaces which implement the \`Node\` interface.\n` +
`Check the type definition of \`${typeComposer.getTypeName()}\`.`);
return;
}
children.forEach((child) => {
typeComposer.addFields(createChildrenField(child.getTypeName()));
typeComposer.addFields(createChildField(child.getTypeName()));
});
});
mimeTypesToChildren.forEach((children, mimeType) => {
const parentTypes = typesHandlingMimeTypes.get(mimeType);
if (parentTypes) {
parentTypes.forEach((typeComposer) => {
if (typeComposer instanceof graphql_compose_1.InterfaceTypeComposer &&
!isNodeInterface(typeComposer)) {
reporter_1.default.error(`With the \`childOf\` extension, children fields can only be added to ` +
`interfaces which implement the \`Node\` interface.\n` +
`Check the type definition of \`${typeComposer.getTypeName()}\`.`);
return;
}
children.forEach((child) => {
typeComposer.addFields(createChildrenField(child.getTypeName()));
typeComposer.addFields(createChildField(child.getTypeName()));
});
});
}
});
};
const isExplicitChild = ({ typeComposer, childTypeComposer }) => {
if (!childTypeComposer.hasExtension(`childOf`)) {
return false;
}
const childOfExtension = childTypeComposer.getExtension(`childOf`);
const { types: parentMimeTypes = [] } = typeComposer.getExtension(`mimeTypes`) ?? {};
return (childOfExtension?.types?.includes(typeComposer.getTypeName()) ||
childOfExtension?.mimeTypes?.some((mimeType) => parentMimeTypes.includes(mimeType)));
};
const addInferredChildOfExtensions = ({ schemaComposer }) => {
schemaComposer.forEach((typeComposer) => {
if (typeComposer instanceof graphql_compose_1.ObjectTypeComposer &&
typeComposer.hasInterface(`Node`)) {
addInferredChildOfExtension({
schemaComposer,
typeComposer,
});
}
});
};
const addInferredChildOfExtension = ({ schemaComposer, typeComposer }) => {
const shouldInfer = typeComposer.getExtension(`infer`);
// With `@dontInfer`, only parent-child
// relations explicitly set with the `@childOf` extension are added.
if (shouldInfer === false)
return;
const parentTypeName = typeComposer.getTypeName();
// This is expensive.
// TODO: We should probably collect this info during inference metadata pass
const childNodeTypes = new Set();
for (const node of (0, datastore_1.getDataStore)().iterateNodesByType(parentTypeName)) {
const children = (node.children || []).map(datastore_1.getNode);
for (const childNode of children) {
if (childNode?.internal?.type) {
childNodeTypes.add(childNode.internal.type);
}
}
}
childNodeTypes.forEach((typeName) => {
const childTypeComposer = schemaComposer.getAnyTC(typeName);
let childOfExtension = childTypeComposer.getExtension(`childOf`);
if (isExplicitChild({ typeComposer, childTypeComposer })) {
return;
}
// Set `@childOf` extension automatically
// This will cause convenience children fields like `childImageSharp`
// to be added in `addConvenienceChildrenFields` method.
// Also required for proper printing of the `@childOf` directive in the snapshot plugin
if (!childOfExtension) {
childOfExtension = {};
}
if (!childOfExtension.types) {
childOfExtension.types = [];
}
childOfExtension.types.push(parentTypeName);
childTypeComposer.setExtension(`childOf`, childOfExtension);
});
};
const createChildrenField = (typeName) => {
return {
[fieldNames.convenienceChildren(typeName)]: {
type: () => [typeName],
description: `Returns all children nodes filtered by type ${typeName}`,
resolve(source, _args, context) {
const { path } = context;
return context.nodeModel.getNodesByIds({ ids: source.children, type: typeName }, { path });
},
},
};
};
const createChildField = (typeName) => {
return {
[fieldNames.convenienceChild(typeName)]: {
type: () => typeName,
description: `Returns the first child node of type ${typeName} ` +
`or null if there are no children of given type on this node`,
resolve(source, _args, context) {
const { path } = context;
const result = context.nodeModel.getNodesByIds({ ids: source.children, type: typeName }, { path });
if (result && result.length > 0) {
return result[0];
}
else {
return null;
}
},
},
};
};
const addTypeToRootQuery = ({ schemaComposer, typeComposer }) => {
const filterInputTC = (0, filter_1.getFilterInput)({
schemaComposer,
typeComposer,
});
const paginationTC = (0, pagination_1.getPagination)({
schemaComposer,
typeComposer,
});
const typeName = typeComposer.getTypeName();
// not strictly correctly, result is `npmPackage` and `allNpmPackage` from type `NPMPackage`
const queryName = fieldNames.query(typeName);
const queryNamePlural = fieldNames.queryAll(typeName);
schemaComposer.Query.addFields({
[queryName]: {
type: typeComposer,
args: {
...filterInputTC.getFields(),
},
resolve: (0, resolvers_1.findOne)(typeName),
},
[queryNamePlural]: {
type: paginationTC,
args: {
filter: filterInputTC,
sort: (0, sort_1.getSortInputNestedObjects)({ schemaComposer, typeComposer }),
skip: `Int`,
limit: `Int`,
},
resolve: (0, resolvers_1.findManyPaginated)(typeName),
},
}).makeFieldNonNull(queryNamePlural);
};
const parseTypes = ({ doc, plugin, createdFrom, schemaComposer, parentSpan, }) => {
const types = [];
doc.definitions.forEach((def) => {
const name = def.name.value;
checkIsAllowedTypeName(name);
if (schemaComposer.has(name)) {
// We don't check if ast.kind matches composer type, but rely
// that this will throw when something is wrong and get
// reported by `reportParsingError`.
// Keep the original type composer around
const typeComposer = schemaComposer.get(name);
// After this, the parsed type composer will be registered as the composer
// handling the type name (requires cleanup after merging, see below)
const parsedType = schemaComposer.typeMapper.makeSchemaDef(def);
// Merging types require implemented interfaces to already exist.
// Depending on type creation order, interface might have not been
// processed yet. We check if interface already exist and create
// placeholder for it, if it doesn't exist yet.
if (parsedType.getInterfaces) {
parsedType.getInterfaces().forEach((iface) => {
const ifaceName =