UNPKG

contexture-mongo

Version:
168 lines (167 loc) 5.76 kB
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 };