contexture-mongo
Version:
Mongo Provider for Contexture
168 lines (167 loc) • 5.76 kB
JavaScript
import F from "futil";
import _ from "lodash/fp.js";
let checkPopulate = ({ include: nodeIncludes, populate }) => _.isEmpty(nodeIncludes) || _.reduce(
(incs, { as, localFieldName, localField, include }) => {
localFieldName = as || localFieldName;
if (!_.includes(localFieldName, incs) && !_.find(_.startsWith(`${localFieldName}.`), incs)) {
throw Error(`Cannot populate an unincluded field: ${localField}`);
}
return _.concat(
incs,
_.map((inc) => `${localFieldName}.${inc}`, include)
);
},
nodeIncludes,
F.unkeyBy("localFieldName", populate)
);
let omitFromInclude = (schema, include, as) => {
let allTargetFields = _.keys(_.get("fields", schema));
let omittedFields = _.reject(
(field) => _.some(
// do NOT omit if:
// - include field is an exact match of schema field
// - schema field is part of an object field specified by the include field (schemaField = 'topLevel.nestedField' and includeField = 'topLevel')
// - include field is a dotted field that is part of a schema object field (schemaField = 'topLevel' and includeField = 'topLevel.nestedField')
(includeField) => _.startsWith(`${includeField}.`, `${field}.`) || _.startsWith(`${field}.`, `${includeField}.`),
include
),
allTargetFields
);
return F.arrayToObject(
(field) => `${as}.${field}`,
_.constant(0)
)(omittedFields);
};
let convertPopulate = (getSchema) => _.flow(
F.mapIndexed((x, as) => {
as = x.as || as;
let { unwind, schema, include, localField, foreignField = "_id" } = x;
let targetSchema = getSchema(schema);
if (!targetSchema)
throw Error(`Couldn't find schema configuration for ${schema}`);
if (!targetSchema.mongo)
throw Error("Populating from a non-mongo schema is not supported");
let targetCollection = _.get("mongo.collection", targetSchema);
if (!targetCollection)
throw Error(
`The ${schema} schema has a mongo configuration without a 'collection' property`
);
let $lookup = {
$lookup: {
as,
from: targetCollection,
localField,
foreignField
// || node.schema, <-- needs schema lookup
}
};
let $project = include ? { $project: omitFromInclude(targetSchema, include, as) } : null;
let $unwind = unwind && {
$unwind: {
path: `$${as}`,
preserveNullAndEmptyArrays: true
}
};
return _.compact([$lookup, $unwind, $project]);
}),
_.flatten
);
let getStartRecord = ({ page, pageSize }) => {
page = page < 1 ? 0 : page - 1;
return page * pageSize;
};
let parentPath = (path) => path.replace(/(\.[^.]+)$/, "");
let isParentPathProjected = (include) => (path) => _.some(_.eq(parentPath(path)), _.pull(path, include));
let projectFromInclude = (include) => _.flow(
_.remove(isParentPathProjected(include)),
_.countBy(_.identity)
)(include);
let getSortStage = ({ sort, sortField, sortDir }) => {
if (!_.isEmpty(sort)) {
return [
{
$sort: Object.fromEntries(
sort.map(({ field, desc }) => [field, desc ? -1 : 1])
)
}
];
}
if (sortField) {
return [{ $sort: { [sortField]: sortDir === "asc" ? 1 : -1 } }];
}
return [];
};
let getResultsQuery = (node, getSchema, startRecord) => {
let { pageSize, sortField, sort, populate, include, skipCount } = node;
let $sort = getSortStage(node);
let $limit = { $limit: F.when(skipCount, _.add(1), pageSize) };
let skipLimit = _.compact([{ $skip: startRecord }, pageSize > 0 && $limit]);
let sortSkipLimit = _.compact([...$sort, ...skipLimit]);
let sortOnJoinField = _.some((x) => {
let lookupField = _.getOr(x, `${x}.as`, populate);
return _.some(
({ field: sortField2 }) => _.startsWith(`${lookupField}.`, sortField2) || sortField2 === lookupField,
sort ?? [{ field: sortField }]
);
}, _.keys(populate));
let hasMany = _.some(_.get("hasMany"), populate);
let $project = _.isEmpty(include) ? [] : [{ $project: projectFromInclude(include) }];
return [
...!sortOnJoinField && !hasMany ? sortSkipLimit : [],
// if "hasMany" is set on a "populate" field but we are not sorting on a
// "populate" field, sort as early as possible
...hasMany && !sortOnJoinField ? $sort : [],
...convertPopulate(getSchema)(populate),
...sortOnJoinField ? sortSkipLimit : [],
...hasMany && !sortOnJoinField ? skipLimit : [],
...$project
];
};
let defaults = _.defaults({
page: 1,
pageSize: 10,
sortDir: "desc",
skipCount: false,
// F.when doesn't like undefined
include: []
});
let getResponse = (node, results, count) => {
let startRecord = getStartRecord(node);
return {
totalRecords: count,
startRecord: startRecord + 1,
endRecord: startRecord + _.min([results.length, node.pageSize]),
...node.skipCount && { hasMore: results.length > node.pageSize },
results: _.take(node.pageSize, results)
};
};
let result = async (node, search, schema, { getSchema }) => {
node = defaults(node);
checkPopulate(node);
let hasMany = _.some(_.get("hasMany"), node.populate);
let resultsQuery = getResultsQuery(node, getSchema, getStartRecord(node));
let countQuery = [
...hasMany ? convertPopulate(getSchema)(node.populate) : [],
{ $group: { _id: null, count: { $sum: 1 } } }
];
let [results, count] = await Promise.all([
search(resultsQuery),
!node.skipCount && search(countQuery)
]);
return { response: getResponse(node, results, _.get("0.count", count)) };
};
var results_default = {
getStartRecord,
getResultsQuery,
getResponse,
defaults,
projectFromInclude,
convertPopulate,
checkPopulate,
// API
result
};
export {
results_default as default,
getSortStage
};