UNPKG

@netlify/content-engine

Version:
632 lines (629 loc) 25.8 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.defaultResolver = exports.defaultFieldResolver = void 0; exports.getMaybeResolvedValue = getMaybeResolvedValue; exports.findOne = findOne; exports.findManyPaginated = findManyPaginated; exports.createDistinctResolver = createDistinctResolver; exports.createMinResolver = createMinResolver; exports.createMaxResolver = createMaxResolver; exports.createSumResolver = createSumResolver; exports.createGroupResolver = createGroupResolver; exports.paginate = paginate; exports.link = link; exports.fileByPath = fileByPath; exports.wrappingResolver = wrappingResolver; const path_1 = __importDefault(require("path")); const normalize_path_1 = __importDefault(require("normalize-path")); const graphql_1 = require("graphql"); const reporter_1 = __importDefault(require("../reporter")); const utils_1 = require("../query/utils"); const get_value_at_1 = require("../utils/get-value-at"); const iterable_1 = require("../datastore/common/iterable"); const utils_2 = require("./utils"); const handle_resolver_context_info_1 = require("./extensions/handle-resolver-context-info"); function getMaybeResolvedValue(node, field, nodeInterfaceName) { if (typeof field !== `string`) { field = (0, utils_2.pathObjectToPathString)(field).path; } if ((0, utils_2.fieldPathNeedToResolve)({ selector: field, type: nodeInterfaceName, })) { return (0, get_value_at_1.getValueAt)((0, utils_2.getResolvedFields)(node), field, { keepNonMatches: false }); } else { return (0, get_value_at_1.getValueAt)(node, field, { keepNonMatches: false }); } } function addAllCacheTag(typeName, context) { if (!(`cacheTags` in context)) { return; } context.cacheTags.add(typeName); } function addCacheTagsFromNode(node, context) { if (!node || !(`cacheTags` in context)) { return; } // instanceof is slow at scale so look for .then first // if there is .then we do need to check instanceof because a node could have a field called `then` if (`then` in node && node instanceof Promise) { node.then((resolvedNode) => { if (resolvedNode?.internal?.type && resolvedNode?.id) { context.cacheTags.add(`${resolvedNode.internal.type}:${resolvedNode.id}`); } }); } else if (node?.internal?.type && node?.id) { context.cacheTags.add(`${node.internal.type}:${node.id}`); } } function findOne(typeName) { return function findOneResolver(_source, args, context, info) { const { authorizationStatus } = (0, handle_resolver_context_info_1.handleResolverContextInfo)(context, info); if (authorizationStatus !== `AUTHORIZED`) { return getEmptyResolverResponseForField(info); } if (context.stats) { context.stats.totalRunQuery++; } const node = context.nodeModel.findOne({ query: { filter: args }, type: info.schema.getType(typeName), stats: context.stats, tracer: context.tracer, }, { path: context.path }); addCacheTagsFromNode(node, context); return node; }; } function findManyPaginated(typeName) { return async function findManyPaginatedResolver(_source, args, context, info) { const { authorizationStatus } = (0, handle_resolver_context_info_1.handleResolverContextInfo)(context, info); // Peek into selection set and pass on the `field` arg of `group` and // `distinct` which might need to be resolved. const group = getProjectedField(info, `group`); const distinct = getProjectedField(info, `distinct`); const max = getProjectedField(info, `max`); const min = getProjectedField(info, `min`); const sum = getProjectedField(info, `sum`); // Apply paddings for pagination // (for previous/next node and also to detect if there is a previous/next page) const skip = typeof args.skip === `number` ? Math.max(0, args.skip - 1) : 0; const limit = typeof args.limit === `number` ? args.limit + 2 : undefined; if (authorizationStatus !== `AUTHORIZED`) { const resZero = () => Promise.resolve(0); return { totalCount: resZero, edges: [], nodes: [], pageInfo: { hasPreviousPage: false, hasNextPage: false, currentPage: 0, perPage: 0, itemCount: 0, totalCount: resZero, pageCount: resZero, }, }; } const extendedArgs = { ...args, group: group || [], distinct: distinct || [], max: max || [], min: min || [], sum: sum || [], skip, limit, }; // Note: stats are passed to telemetry in src/commands/build.ts if (context.stats) { context.stats.totalRunQuery++; context.stats.totalPluralRunQuery++; } const result = await context.nodeModel.findAll({ query: extendedArgs, type: info.schema.getType(typeName), stats: context.stats, tracer: context.tracer, }, { path: context.path, connectionType: typeName }); const shouldUseAllCacheTag = // if the query isn't paginated or the query is returning more than 100 top level nodes use the all cache tag !limit || limit > 100; if (!shouldUseAllCacheTag) { for (const node of result.entries) { addCacheTagsFromNode(node, context); } } else { addAllCacheTag(typeName, context); } return paginate(result, { resultOffset: skip, skip: args.skip, limit: args.limit, }); }; } function createDistinctResolver(nodeInterfaceName) { return function distinctResolver(source, args) { const { field } = args; const { edges } = source; const values = new Set(); edges.forEach(({ node }) => { const value = getMaybeResolvedValue(node, field, nodeInterfaceName); if (value === null || value === undefined) { return; } if (Array.isArray(value)) { value.forEach((subValue) => values.add(subValue instanceof Date ? subValue.toISOString() : subValue)); } else if (value instanceof Date) { values.add(value.toISOString()); } else { values.add(value); } }); return Array.from(values).sort(); }; } function createMinResolver(nodeInterfaceName) { return function minResolver(source, args) { const { field } = args; const { edges } = source; let min = Number.MAX_SAFE_INTEGER; edges.forEach(({ node }) => { let value = getMaybeResolvedValue(node, field, nodeInterfaceName); if (typeof value !== `number`) { value = Number(value); } if (!isNaN(value) && value < min) { min = value; } }); if (min === Number.MAX_SAFE_INTEGER) { return null; } return min; }; } function createMaxResolver(nodeInterfaceName) { return function maxResolver(source, args) { const { field } = args; const { edges } = source; let max = Number.MIN_SAFE_INTEGER; edges.forEach(({ node }) => { let value = getMaybeResolvedValue(node, field, nodeInterfaceName); if (typeof value !== `number`) { value = Number(value); } if (!isNaN(value) && value > max) { max = value; } }); if (max === Number.MIN_SAFE_INTEGER) { return null; } return max; }; } function createSumResolver(nodeInterfaceName) { return function sumResolver(source, args) { const { field } = args; const { edges } = source; return edges.reduce((prev, { node }) => { let value = getMaybeResolvedValue(node, field, nodeInterfaceName); if (typeof value !== `number`) { value = Number(value); } if (!isNaN(value)) { return (prev || 0) + value; } return prev; }, null); }; } function createGroupResolver(nodeInterfaceName) { return function groupResolver(source, args) { const { field } = args; const { edges } = source; const groupedResults = edges.reduce((acc, { node }) => { const value = getMaybeResolvedValue(node, field, nodeInterfaceName); const values = Array.isArray(value) ? value : [value]; values .filter((value) => value != null) .forEach((value) => { const key = value instanceof Date ? value.toISOString() : value; acc[key] = (acc[key] || []).concat(node); }); return acc; // Note: using Object.create on purpose: // object key may be arbitrary string including reserved words (i.e. `constructor`) // see: https://github.com/gatsbyjs/gatsby/issues/22508 }, Object.create(null)); return Object.keys(groupedResults) .sort() .reduce((acc, fieldValue) => { const entries = groupedResults[fieldValue] || []; acc.push({ ...paginate({ entries: new iterable_1.GatsbyIterable(entries), totalCount: async () => entries.length, }, args), field: typeof field === `string` ? field : (0, utils_2.pathObjectToPathString)(field).path, fieldValue, }); return acc; }, []); }; } function paginate(results, params) { const { resultOffset = 0, skip = 0, limit } = params; if (resultOffset > skip) { throw new Error("Result offset cannot be greater than `skip` argument"); } const allItems = Array.from(results.entries); const start = skip - resultOffset; const items = allItems.slice(start, limit && start + limit); const totalCount = results.totalCount; const pageCount = async () => { const count = await totalCount(); return limit ? Math.ceil(skip / limit) + Math.ceil((count - skip) / limit) : skip ? 2 : 1; }; const currentPage = limit ? Math.ceil(skip / limit) + 1 : skip ? 2 : 1; const hasPreviousPage = currentPage > 1; const hasNextPage = limit ? allItems.length - start > limit : false; return { totalCount, edges: items.map((item, i, arr) => { return { node: item, next: arr[i + 1], previous: arr[i - 1], }; }), nodes: items, pageInfo: { currentPage, hasPreviousPage, hasNextPage, itemCount: items.length, pageCount, perPage: limit, totalCount, }, }; } function link(options = { by: `id`, }, fieldConfig) { // Note: we explicitly make an attempt to prevent using the `async` keyword because often // it does not return a promise and this makes a significant difference at scale. return function linkResolver(source, args, context, info) { const { authorizationStatus } = (0, handle_resolver_context_info_1.handleResolverContextInfo)(context, info); if (authorizationStatus !== `AUTHORIZED`) { return getEmptyResolverResponseForField(info); } const resolver = fieldConfig.resolve || context.defaultFieldResolver; const fieldValueOrPromise = resolver(source, args, context, { ...info, from: options.from || info.from, keepObjects: options.keepObjects, }); // Note: for this function, at scale, conditional .then is more efficient than generic await if (typeof fieldValueOrPromise?.then === `function`) { return fieldValueOrPromise.then((fieldValue) => linkResolverValue(fieldValue, args, context, info)); } return linkResolverValue(fieldValueOrPromise, args, context, info); }; function linkResolverValue(fieldValue, args, context, info) { if (fieldValue == null) { return null; } const returnType = (0, graphql_1.getNullableType)(options.type || info.returnType); const type = (0, graphql_1.getNamedType)(returnType); if (options.by === `id`) { if (Array.isArray(fieldValue)) { const nodes = context.nodeModel.getNodesByIds({ ids: fieldValue, keepObjects: options.keepObjects, type: type }, { path: context.path }); for (const node of nodes) { addCacheTagsFromNode(node, context); } return nodes; } else { const id = fieldValue?.id || typeof fieldValue === `string` ? fieldValue?.id || fieldValue : undefined; const node = context.nodeModel.getNodeById({ id, type: type, }, { path: context.path }) || (options.keepObjects ? fieldValue : null); addCacheTagsFromNode(node, context); return node; } } // Return early if fieldValue is [] since { in: [] } doesn't make sense if (Array.isArray(fieldValue) && fieldValue.length === 0) { return fieldValue; } const runQueryArgs = args; runQueryArgs.filter = options.by.split(`.`).reduceRight((acc, key) => { const obj = {}; obj[key] = acc; return obj; }, Array.isArray(fieldValue) ? { in: fieldValue } : { eq: fieldValue }); const firstOnly = !(returnType instanceof graphql_1.GraphQLList); if (context.stats) { context.stats.totalRunQuery++; if (firstOnly) { context.stats.totalPluralRunQuery++; } } if (firstOnly) { return context.nodeModel .findOne({ query: runQueryArgs, type, stats: context.stats, tracer: context.tracer, }, { path: context.path }) .then((result) => linkResolverQueryResult(fieldValue, result, returnType)); } return context.nodeModel .findAll({ query: runQueryArgs, type, stats: context.stats, tracer: context.tracer, }, { path: context.path }) .then(({ entries }) => linkResolverQueryResult(fieldValue, Array.from(entries), returnType)); } function linkResolverQueryResult(fieldValue, queryResult, returnType) { if (returnType instanceof graphql_1.GraphQLList && Array.isArray(fieldValue) && Array.isArray(queryResult)) { return fieldValue.map((value) => queryResult.find((obj) => (0, get_value_at_1.getValueAt)(obj, options.by, { keepNonMatches: false }) === value)); } else { return queryResult; } } } function fileByPath(options = {}, fieldConfig) { return async function fileByPathResolver(source, args, context, info) { const resolver = fieldConfig.resolve || context.defaultFieldResolver; const fieldValue = await resolver(source, args, context, { ...info, from: options.from || info.from, }); if (fieldValue == null) { return null; } // Find the File node for this node (we assume the node is something // like markdown which would be a child node of a File node). const parentFileNode = context.nodeModel.findRootNodeAncestor(source, (node) => node.internal && node.internal.type === `File`); async function queryNodesByPath(relPaths) { const arr = []; for (let i = 0; i < relPaths.length; ++i) { arr[i] = await (Array.isArray(relPaths[i]) ? queryNodesByPath(relPaths[i]) : queryNodeByPath(relPaths[i])); } return arr; } function queryNodeByPath(relPath) { return context.nodeModel.findOne({ query: { filter: { absolutePath: { eq: (0, normalize_path_1.default)(path_1.default.resolve(parentFileNode.dir, relPath)), }, }, }, type: `File`, }); } if (Array.isArray(fieldValue)) { return queryNodesByPath(fieldValue); } else { return queryNodeByPath(fieldValue); } }; } function getProjectedField(info, fieldName) { const selectionSet = info.fieldNodes[0].selectionSet; if (selectionSet) { const fieldNodes = getFieldNodeByNameInSelectionSet(selectionSet, fieldName, info); if (fieldNodes.length === 0) { return []; } const returnType = (0, graphql_1.getNullableType)(info.returnType); if ((0, graphql_1.isObjectType)(returnType) || (0, graphql_1.isInterfaceType)(returnType)) { const field = returnType.getFields()[fieldName]; const fieldArg = field?.args?.find((arg) => arg.name === `field`); if (fieldArg) { const fieldTC = (0, graphql_1.getNullableType)(fieldArg.type); if ((0, graphql_1.isEnumType)(fieldTC) || (0, graphql_1.isInputObjectType)(fieldTC)) { return fieldNodes.reduce((acc, fieldNode) => { const fieldArg = fieldNode.arguments?.find((arg) => arg.name.value === `field`); if ((0, graphql_1.isEnumType)(fieldTC)) { if (fieldArg?.value.kind === graphql_1.Kind.ENUM) { const enumKey = fieldArg.value.value; const enumValue = fieldTC.getValue(enumKey); if (enumValue) { acc.push(enumValue.value); } } } else if ((0, graphql_1.isInputObjectType)(fieldTC)) { const path = []; let currentValue = fieldArg?.value; while (currentValue) { if (currentValue.kind === graphql_1.Kind.OBJECT) { if (currentValue.fields.length !== 1) { throw new Error(`Invalid field arg`); } const fieldArg = currentValue.fields[0]; path.push(fieldArg.name.value); currentValue = fieldArg.value; } else { currentValue = undefined; } } if (path.length > 0) { const sortPath = path.join(`.`); acc.push(sortPath); } } return acc; }, []); } } } } return []; } function getFieldNodeByNameInSelectionSet(selectionSet, fieldName, info) { return selectionSet.selections.reduce((acc, selection) => { if (selection.kind === graphql_1.Kind.FRAGMENT_SPREAD) { const fragmentDef = info.fragments[selection.name.value]; if (fragmentDef) { return [ ...acc, ...getFieldNodeByNameInSelectionSet(fragmentDef.selectionSet, fieldName, info), ]; } } else if (selection.kind === graphql_1.Kind.INLINE_FRAGMENT) { return [ ...acc, ...getFieldNodeByNameInSelectionSet(selection.selectionSet, fieldName, info), ]; } /* FIELD_NODE */ else { if (selection.name.value === fieldName) { return [...acc, selection]; } } return acc; }, []); } function getEmptyResolverResponseForField(info) { return (0, graphql_1.isListType)((0, graphql_1.getNullableType)(info.returnType)) ? [] : null; } const defaultFieldResolver = function defaultFieldResolver(source, args, context, info) { const { authorizationStatus } = (0, handle_resolver_context_info_1.handleResolverContextInfo)(context, info); if (authorizationStatus !== `AUTHORIZED`) { return getEmptyResolverResponseForField(info); } if ((typeof source == `object` && source !== null) || typeof source === `function`) { if (info.from) { const val = (0, get_value_at_1.getValueAt)(source, info.from, { keepNonMatches: info.keepObjects, }); // if keepNonMatches returned the source object, it didn't find anything if (info.keepObjects && val === source) { return null; } if ( // allow turning off JSON field stringification via env var process.env.CONTENT_ENGINE_RETURN_JSON_OBJECTS !== `true` && // otherwise if this is a JSON field type (0, graphql_1.getNamedType)(info.returnType).name === `JSON` && // and the val is not a string typeof val !== `string`) { // return a string to be backwards compatible with production Connect return (0, graphql_1.isListType)((0, graphql_1.getNullableType)(info.returnType)) ? val.map((j) => JSON.stringify(j)) : JSON.stringify(val); } return val; } const property = source[info.fieldName]; if (typeof property === `function`) { return source[info.fieldName](args, context, info); } return property; } return null; }; exports.defaultFieldResolver = defaultFieldResolver; let WARNED_ABOUT_RESOLVERS = false; function badResolverInvocationMessage(missingVar, path) { const resolverName = path ? `${(0, utils_1.pathToArray)(path)} ` : ``; return `GraphQL Resolver ${resolverName}got called without "${missingVar}" argument. This might cause unexpected errors. It's likely that this has happened in a schemaCustomization with manually invoked resolver. If manually invoking resolvers, it's best to invoke them as follows: resolve(parent, args, context, info) `; } function wrappingResolver(resolver) { // Note: we explicitly make an attempt to prevent using the `async` keyword because often // it does not return a promise and this makes a significant difference at scale. // GraphQL will gracefully handle the resolver result of a promise or non-promise. if (resolver[`isTracingResolver`]) { return resolver; } const wrappedTracingResolver = function wrappedTracingResolver(parent, args, context, info) { if (!WARNED_ABOUT_RESOLVERS) { if (!info) { reporter_1.default.warn(badResolverInvocationMessage(`info`)); WARNED_ABOUT_RESOLVERS = true; } else if (!context) { reporter_1.default.warn(badResolverInvocationMessage(`context`, info.path)); WARNED_ABOUT_RESOLVERS = true; } } let activity; let time; if (context?.tracer) { activity = context.tracer.createResolverActivity(info.path, `${info.parentType.name}.${info.fieldName}`); activity.start(); } if (context?.telemetryResolverTimings) { time = process.hrtime.bigint(); } const result = resolver(parent, args, context, info); if (!activity && !time) { return result; } const endActivity = () => { if (context?.telemetryResolverTimings) { context.telemetryResolverTimings.push({ name: `${info.parentType}.${info.fieldName}`, duration: Number(process.hrtime.bigint() - time) / 1000 / 1000, }); } if (activity) { activity.end(); } }; if (typeof result?.then === `function`) { result.then(endActivity, endActivity); } else { endActivity(); } return result; }; wrappedTracingResolver.isTracingResolver = true; return wrappedTracingResolver; } exports.defaultResolver = wrappingResolver(exports.defaultFieldResolver); //# sourceMappingURL=resolvers.js.map