UNPKG

popurelate

Version:

Easily populate and filter documents using data from related collections.

355 lines (354 loc) 13.1 kB
"use strict"; var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.pipelinesToMongodbPipelines = exports.addMongooseRelations = exports.defaultEngines = void 0; /* eslint-disable max-lines-per-function */ const optimizer_1 = require("../optimizer"); const path_1 = require("../path"); const optimizer_2 = require("./optimizer"); const relations_1 = require("./relations"); Object.defineProperty(exports, "addMongooseRelations", { enumerable: true, get: function () { return relations_1.addMongooseRelations; } }); const createMongooLikeEngine = (_a) => { var { getAggregateFn, name, debugger: debuggerFn } = _a, rest = __rest(_a, ["getAggregateFn", "name", "debugger"]); return Object.assign({ name, aggregator: async (props) => { const { findOne, pipelines } = props; const normalizedPipelines = exports.pipelinesToMongodbPipelines(pipelines); const singleDoc = normalizedPipelines.singleDoc || findOne; const countKey = normalizedPipelines.countKey; if (debuggerFn) { debuggerFn({ modelName: props.modelName, pipelines, normalizedPipelines: normalizedPipelines.pipelines, }); } const aggregate = getAggregateFn(props); return aggregate(normalizedPipelines.pipelines).then(data => { if (singleDoc) { if (data.length === 0 && countKey !== null) { return { [countKey]: 0 }; // if count agggregation is used and 0 documents is returned, return count: 0 } return data[0]; } return data; }); }, optimizer: mongoOptimizer, pipeLineIsHelper: optimizer_2.mongoOptimizationHelper.pipelineIs }, rest); }; const mognodbAggregator = ({ db, transformedModelName, }) => db.collection(transformedModelName).aggregate; const mongooseAggregator = ({ model, }) => { return model.aggregate.bind(model); }; const mongoOptimizer = optimizer_1.createDefaultOptimizer(optimizer_2.mongoOptimizationHelper); const mongodbEngine = ({ useOptimizer = false, debugger: debuggerFn, } = {}) => createMongooLikeEngine({ name: "mongodb", getAggregateFn: mognodbAggregator, useOptimizer, debugger: debuggerFn, }); const mongooseEngine = ({ useOptimizer = false, debugger: debuggerFn, } = {}) => createMongooLikeEngine({ name: "mongoose", getAggregateFn: mongooseAggregator, transformModelName: ({ model }) => model.collection.collectionName, useOptimizer, debugger: debuggerFn, }); exports.defaultEngines = { mongodb: mongodbEngine, mongoose: mongooseEngine, }; const pipelinesToMongodbPipelines = (pipelines) => { const realPipelines = []; let singleDoc = false; let countKey = null; for (let i = 0; i < pipelines.length; i++) { const pipeline = pipelines[i]; if (pipeline.limit !== undefined) { realPipelines.push({ $limit: pipeline.limit }); } else if (pipeline.skip !== undefined) { realPipelines.push({ $skip: pipeline.skip }); } else if (pipeline.sort !== undefined) { realPipelines.push({ $sort: pipeline.sort }); } else if (pipeline.project !== undefined) { realPipelines.push({ $project: pipeline.project }); } else if (pipeline.query !== undefined) { realPipelines.push({ $match: pipeline.query }); } else if (pipeline.rawPipeline !== undefined) { realPipelines.push(pipeline.rawPipeline); } else if (pipeline.populate !== undefined) { realPipelines.push(...populatePipeline(pipeline)); } else if (pipeline.withCount) { realPipelines.push(...withCountPipeline(pipeline)); // do not change countKey variable here singleDoc = true; } else if (pipeline.addFields !== undefined) { realPipelines.push({ $addFields: pipeline.addFields }); } else if (pipeline.count) { realPipelines.push({ $count: pipeline.countKey }); countKey = pipeline.countKey; singleDoc = true; } else if (!pipeline.invisible) { throw new Error("unsupported pipeline: " + JSON.stringify(pipeline)); } } return { pipelines: realPipelines, singleDoc, countKey }; }; exports.pipelinesToMongodbPipelines = pipelinesToMongodbPipelines; // eslint-disable-next-line max-lines-per-function const populatePipeline = ({ parentIdField, myIdField, field, populate }, parentInfo = [ { field: "", parentIdField: "", myIdField: parentIdField, asPath: "", }, ]) => { const realPipelines = []; const unwrappedPaths = []; let pth = populate.localField; let bracketsIndex = pth.indexOf("[]"); while (bracketsIndex !== -1 && bracketsIndex !== pth.length - "[]".length) { const subPath = pth.substr(0, bracketsIndex); const unwrappedPath = path_1.normalizeQueryPath(path_1.joinQueryPath(populate.globalPathPrefix, subPath)); realPipelines.push({ $unwind: { path: "$" + unwrappedPath, }, }); unwrappedPaths.push(unwrappedPath); pth = pth.substr(bracketsIndex + "[]".length); bracketsIndex = pth.indexOf("[]"); } const asPath = path_1.normalizeQueryPath(path_1.joinQueryPath(populate.globalPathPrefix, field)); if (!populate.alreadyPopulated) { const isForeignArray = populate.foreignField.indexOf("[]") !== -1; const isLocalArray = populate.localField.lastIndexOf("[]") === populate.localField.length - "[]".length; realPipelines.push({ $lookup: getLookupPipeline({ isLocalArray, isForeignArray, populate, asPath, }), }); } if (!populate.matchesMany || populate.children) { realPipelines.push({ $unwind: { path: "$" + asPath, preserveNullAndEmptyArrays: populate.matchesMany ? true : !populate.required, }, }); } if (populate.children) { for (const key in populate.children) { const child = populate.children[key]; realPipelines.push(...populatePipeline(child, [ ...parentInfo, { field, parentIdField: myIdField, myIdField: child.myIdField, asPath, }, ])); } } if (unwrappedPaths.length > 0 || (populate.matchesMany && populate.children)) { const p = parentInfo .slice(1) .map((e, index) => ["x" + index, e.asPath ? e.asPath : e.myIdField]); if (unwrappedPaths.length > 0) { for (const un of unwrappedPaths) { p.push(["x" + p.length, un]); } } else { p.push(["x" + p.length, asPath]); } const groupPerservingFields = {}; for (let i = 0; i < p.length; ++i) { const e = p[i]; if (i < p.length - 1 || (!populate.matchesMany && unwrappedPaths.length === 0)) { groupPerservingFields[e[0]] = { $first: "$" + e[1] }; } else { groupPerservingFields[e[0]] = { $push: "$" + e[1] }; } } realPipelines.push({ $group: Object.assign({ _id: getGroupId(parentInfo), doc: { $first: "$$ROOT", } }, groupPerservingFields), }, ...p.map(([key, path]) => ({ $addFields: { ["doc." + path]: "$" + key, }, })), { $replaceRoot: { newRoot: "$doc", }, }); } return realPipelines; }; const getLookupPipeline = ({ isLocalArray, isForeignArray, populate, asPath, }) => { const localField = path_1.normalizeQueryPath(path_1.joinQueryPath(populate.globalPathPrefix, populate.localField)); const foreignField = path_1.normalizeQueryPath(populate.foreignField); const useSimplePipeline = !isForeignArray; if (useSimplePipeline) { return { from: populate.transformedModelName, localField, foreignField, as: asPath, }; } if (!isLocalArray && !isForeignArray) { return { from: populate.transformedModelName, let: { x0: "$" + localField, }, pipeline: [ { $match: { $expr: { $eq: ["$$x0", "$" + foreignField], }, }, }, ], as: asPath, }; } if (!isLocalArray && isForeignArray) { return { from: populate.transformedModelName, let: { x0: "$" + localField, }, pipeline: [ { $match: { $expr: { $in: ["$$x0", { $ifNull: ["$" + foreignField, []] }], }, }, }, ], as: asPath, }; } if (isLocalArray && !isForeignArray) { return { from: "users", let: { x0: "$" + localField, }, pipeline: [ { $match: { $expr: { $in: ["$" + foreignField, "$$x0"], }, }, }, ], as: asPath, }; } if (isLocalArray && isForeignArray) { return { from: populate.transformedModelName, let: { x0: "$" + localField, }, pipeline: [ { $match: { $expr: { $gt: [ { $size: { $ifNull: [{ $setUnion: ["$" + foreignField, "$$x0"] }, []], }, }, 0, ], }, }, }, ], as: asPath, }; } }; const getGroupId = (parentInfo) => { if (parentInfo.length === 1) { const parent = parentInfo[0]; return ("$" + path_1.normalizeQueryPath(path_1.joinQueryPath(parent.field, parent.myIdField))); } const q = { $concat: [] }; for (let i = 0; i < parentInfo.length; ++i) { const parent = parentInfo[i]; q.$concat.push({ $toString: "$" + path_1.normalizeQueryPath(path_1.joinQueryPath(parent.field, parent.myIdField)), }); if (i < parentInfo.length - 1) { q.$concat.push("-%%-"); } } return q; }; const withCountPipeline = (pipeline) => { const realPipelines = []; const countKey = pipeline.countKey; const docsKey = pipeline.docsKey; const childPipelines = exports.pipelinesToMongodbPipelines(pipeline.docsPipelines) .pipelines; if (childPipelines.length === 0) childPipelines.push({ $skip: 0 }); // sub-pipeline in $facet stage cannot be empty realPipelines.push({ $facet: { a: [{ $count: "z" }], [docsKey]: childPipelines, }, }); realPipelines.push({ $addFields: { a: { $arrayElemAt: ["$a", 0] }, }, }); realPipelines.push({ $project: { [countKey]: { $ifNull: ["$a.z", 0] }, [docsKey]: 1, }, }); return realPipelines; };