@netlify/content-engine
Version:
632 lines (629 loc) • 25.8 kB
JavaScript
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
;