UNPKG

popurelate

Version:

Easily populate and filter documents using data from related collections.

546 lines (545 loc) 24.6 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.getLastSkipAndLimitIndex = exports.createPopurelation = void 0; /* eslint-disable max-lines */ /* eslint-disable max-params */ /* eslint-disable sonarjs/cognitive-complexity */ const errors_1 = require("./errors"); const is_1 = require("./optimizer/is"); const path_1 = require("./path"); function createPopurelation() { return new QueryBuilderCreator(); } exports.createPopurelation = createPopurelation; class QueryBuilderCreator { constructor() { this.engines = []; this.dbs = []; this.modelInfoByNames = {}; this.addEngine = engine => { this.engines.push(engine); return this; }; this.newQueryBuilder = fn => { return fn; }; this.getModelsByEngine = (engineName) => { const matchedModels = this.dbs .filter(e => e.engineName === engineName) .map(e => e.models); return matchedModels.reduce((prev, newModels) => (Object.assign(Object.assign({}, prev), newModels)), {}); }; this.getModelsByDb = (db) => { const matchedModels = this.dbs.filter(e => e.db === db).map(e => e.models); return matchedModels.reduce((prev, newModels) => (Object.assign(Object.assign({}, prev), newModels)), {}); }; this.addDb = db => { const engine = this.engines.find(e => e.name === db.engine); if (!engine) { throw new errors_1.QueryBuilderEngineNotFoundError(db.engine); } const dbInfo = Object.assign(Object.assign({}, db), { engine, engineName: db.engine }); this.dbs.push(dbInfo); const dbInfoWithoutModles = Object.assign({}, dbInfo); delete dbInfoWithoutModles.models; for (const modelName in db.models) { this.modelInfoByNames[modelName] = { model: db.models[modelName], db: dbInfoWithoutModles, forwardRelations: [], backwardRelations: [], }; } return this; }; this.addRelation = (model, relationOptions) => { const model1Info = this.getModelInfoByName(model); for (const field in relationOptions) { const rawRelationInfo = relationOptions[field]; let relation; if (typeof rawRelationInfo !== "object") { relation = { model: rawRelationInfo, }; } else { relation = rawRelationInfo; } if (typeof relation.matchesMany === "undefined" && field.substr(-2) === "[]") { relation.matchesMany = true; } const model2Info = this.getModelInfoByName(relation.model); let isRequired; if (typeof relation.required === "boolean") { isRequired = relation.required; } else if (relation.matchesMany) isRequired = false; else isRequired = model1Info.db.defaultRequired; const normalizedRelation = { model1: { name: model, localField: relation.localField || field, field: field, matchesMany: relation.matchesMany, required: isRequired, }, model2: { name: relation.model, localField: relation.foreignField || model2Info.db.idField, matchesMany: true, required: false, }, through: relation.through, }; if (normalizedRelation.model1.field === normalizedRelation.model1.localField) { delete normalizedRelation.model1.field; } model1Info.forwardRelations.push(normalizedRelation); model2Info.backwardRelations.push(normalizedRelation); if (normalizedRelation.through !== undefined) { this.getModelInfoByName(normalizedRelation.through.name); } } return this; }; this.model = (modelName) => { return { findMany: this.getFindMethod(false, modelName), findOne: this.getFindMethod(true, modelName), fillPopulations: this.getNormalizePopulationsMethod(modelName), }; }; this.getFindMethod = (one, modelName) => { return query => { const options = { findOne: one, modelName, pipelines: query ? [{ query: query }] : [], transformedModelName: this.getModelTransformedName(modelName), }; return this.getQueryHelpers(options); }; }; this.getModelInfoByName = (modelName) => { const info = this.modelInfoByNames[modelName]; if (!info) { throw new errors_1.QueryBuilderModelNotFoundError(modelName); } return info; }; this.getAggregatorOptions = (options, disableOptimizer) => { const modelName = options.modelName; const modelInfo = this.getModelInfoByName(modelName); const dbInfo = modelInfo.db; const engine = modelInfo.db.engine; if (options.findOne) { options = Object.assign(Object.assign({}, options), { pipelines: addPipelineForFindOne(options.pipelines, engine) }); } let aggreagatorOptions = Object.assign(Object.assign({}, options), { db: dbInfo.db, model: modelInfo.model }); if (!disableOptimizer && engine.optimizer) { aggreagatorOptions = engine.optimizer(!!engine.useOptimizer, aggreagatorOptions); } return aggreagatorOptions; }; this.getPrmise = async (aggregatorOptions) => { const modelName = aggregatorOptions.modelName; const modelInfo = this.getModelInfoByName(modelName); const engine = modelInfo.db.engine; return engine.aggregator(aggregatorOptions); }; // eslint-disable-next-line max-lines-per-function this.getQueryHelpers = (options) => { const createAddPipelineFn = (opt) => { return (...pipelines) => this.getQueryHelpers(Object.assign(Object.assign({}, opt), { pipelines: opt.pipelines.concat(pipelines) })); }; const addPipeline = createAddPipelineFn(options); const obj = Object.assign({ withCount: (include) => { if (include === false) { return obj; } if (typeof include !== "object" || include === null) { return addPipeline({ withCount: true, countKey: "count", docsKey: "docs", docsPipelines: [], }); } let defaultPipelines = []; if (typeof include.skip === "number") { defaultPipelines.push({ skip: include.skip, }); } if (typeof include.limit === "number") { defaultPipelines.push({ limit: include.limit, }); } const newBuilder = this.getQueryHelpers(Object.assign(Object.assign({}, options), { pipelines: defaultPipelines })); if (include.queryBuilder) { const newBuilder2 = include.queryBuilder(newBuilder); defaultPipelines = newBuilder2.__getOptions() .pipelines; } return addPipeline({ withCount: true, countKey: include.countKey || "count", docsKey: include.docsKey || "docs", docsPipelines: defaultPipelines, }); }, limit: limit => { if (typeof limit !== "number") { return obj; } return addPipeline({ limit }); }, skip: skip => { if (typeof skip !== "number") { return obj; } return addPipeline({ skip }); }, sort: sort => { if (typeof sort !== "object" || sort === null) { return obj; } if (typeof sort === "object" && isEmptyObject(sort)) return obj; return addPipeline({ sort }); }, project: project => { if (typeof project !== "object" || project === null) { return obj; } return addPipeline({ project }); }, addFields: fields => { if (typeof fields !== "object" || fields === null) { return obj; } return addPipeline({ addFields: fields }); }, count: () => { return addPipeline({ count: true, countKey: "count" }); }, rawPipeline: rawPipeline => { if (rawPipeline === undefined || rawPipeline === null) { return obj; } return addPipeline({ rawPipeline }); }, where: query => { if (typeof query !== "object" || query === null) { return obj; } if (isEmptyObject(query)) return obj; return addPipeline({ query }); }, populate: (...args) => { const all = []; if (args.length === 1 && typeof args[0] !== "object") { const field = args[0]; all.push({ field, manual: {}, }); } else if (args.length === 2) { all.push(this.populationToObjects({ [args[0]]: args[1] })[args[0]]); } else { const pop = this.populationToObjects(args[1]); for (const field in pop) { all.push(pop[field]); } } const newOptions = Object.assign({}, options); const populatePipelines = all.map(p => this.populationToPipeline(newOptions, options.modelName, p)); return createAddPipelineFn(newOptions)(...populatePipelines); }, queryBuilder: queryBuilder => { if (!queryBuilder) return obj; return queryBuilder(obj); }, as: () => obj, optimizer: opt => { if (opt === undefined) opt = {}; else if (typeof opt === "boolean") opt = { use: opt }; return addPipeline({ invisible: true, optimizer: true, useOptimizer: opt.use, hints: opt.hints, }); }, inspect: fn => { if (!fn) return obj; fn({ unoptimizedOptions: this.getAggregatorOptions(options, true), optimizedOptions: this.getAggregatorOptions(options, false), }); return obj; }, exec: () => { return this.getPrmise(this.getAggregatorOptions(options)); } }, { __getOptions: () => options }); return obj; }; this.getModelRelations = (modelName) => { const modelInfo = this.getModelInfoByName(modelName); return modelInfo.forwardRelations; // return modelInfo.forwardRelations.concat(modelInfo.backwardRelations); }; this.getMatchedRelation = (primaryModelName, field, localField, secondaryModelName, foreignField) => { const normalizedLocalField = localField ? path_1.normalizeQueryPath(localField) : null; const normalizedField = path_1.normalizeQueryPath(field); const normalizedForeignField = foreignField ? path_1.normalizeQueryPath(foreignField) : null; const modelRelations = this.getModelRelations(primaryModelName); const matchedRelations = modelRelations.find(each => { if (each.model1.name !== primaryModelName) return false; if (secondaryModelName && each.model2.name !== secondaryModelName) { return false; } if (foreignField && path_1.normalizeQueryPath(each.model2.localField) !== normalizedForeignField) { return false; } if (localField && path_1.normalizeQueryPath(each.model1.localField) !== normalizedLocalField) { return false; } if (!localField) { if (path_1.normalizeQueryPath(each.model1.field || each.model1.localField) !== normalizedField) { return false; } } return true; }); if (!matchedRelations) return null; return matchedRelations.model1.name === primaryModelName ? matchedRelations : swapRelation(matchedRelations); }; this.getModelTransformedName = (modelName) => { const modelInfo = this.getModelInfoByName(modelName); if (!modelInfo.db.engine.transformModelName) return modelName; return modelInfo.db.engine.transformModelName({ model: modelInfo.model, modelName, db: modelInfo.db.db, }); }; this.getNormalizePopulationsMethod = (modelName) => (populationObject) => { const normalized = {}; const populations = this.populationToObjects(populationObject); for (const key in populations) { const pipeline = this.populationToPipeline({ modelName, transformedModelName: this.getModelTransformedName(modelName), findOne: false, pipelines: [], }, modelName, populations[key]); if (pipeline.populate) { normalized[pipeline.field] = populationOptionsToRequiredPopulationOptions(pipeline.populate); } } return normalized; }; } // eslint-disable-next-line max-lines-per-function populationToPipeline(options, modelName, populationsInfo, parentInfo = [], parentGlobalPathPrefix = "", parentLocalPathPrefix = "") { const { field, manual } = populationsInfo; const relation = this.getMatchedRelation(modelName, field, manual.localField, manual.model || null, manual.foreignField); let foreignField = ""; if (manual.foreignField) { foreignField = manual.foreignField; } else if (manual.model) { const modelInfo = this.getModelInfoByName(manual.model); foreignField = modelInfo.db.idField; } else if (relation) { if (relation.model2.localField) { foreignField = relation.model2.localField; } else { const modelInfo = this.getModelInfoByName(relation.model2.name); foreignField = modelInfo.db.idField; } } if (!foreignField) { throw new errors_1.QueryBuilderError(`Cannot find foreign field while populating \`${field}\` field on model ${modelName.toString()}`); } const localField = manual.localField || (relation === null || relation === void 0 ? void 0 : relation.model1.localField) || field; const localFieldPrefix = (parentGlobalPathPrefix ? parentGlobalPathPrefix + "." : "") + field; if (!localField) { throw new errors_1.QueryBuilderError(`localField not found for ${localFieldPrefix}`); } const secondaryModelName = manual.model || (relation === null || relation === void 0 ? void 0 : relation.model2.name); if (!secondaryModelName) { throw new errors_1.QueryBuilderError(`model not found for ${localFieldPrefix}`); } let matchesMany = false; if (typeof manual.matchesMany === "boolean") { matchesMany = manual.matchesMany; } else if (typeof (relation === null || relation === void 0 ? void 0 : relation.model1.matchesMany) === "boolean") { matchesMany = relation.model1.matchesMany; } let appendBrackets = matchesMany; if (localField.substr(-2) === "[]") { matchesMany = true; appendBrackets = false; } let required = undefined; if (typeof manual.required === "boolean") required = manual.required; else if (typeof (relation === null || relation === void 0 ? void 0 : relation.model1.required) === "boolean") { required = relation === null || relation === void 0 ? void 0 : relation.model1.required; } if (typeof required === "undefined" && !matchesMany) { required = this.getModelInfoByName(modelName).db.defaultRequired; } else if (typeof required === "undefined") required = false; const localPathPrefix = (parentLocalPathPrefix ? parentLocalPathPrefix + "." : "") + localField; const globalPathPrefix = (parentGlobalPathPrefix ? parentGlobalPathPrefix + "." : "") + field; const pipeline = { parentIdField: this.getModelInfoByName(modelName).db.idField, field, myIdField: this.getModelInfoByName(secondaryModelName).db.idField, populate: { localField, globalPathPrefix: parentGlobalPathPrefix, localPathPrefix: parentLocalPathPrefix, foreignField, matchesMany: matchesMany, modelName: secondaryModelName, transformedModelName: this.getModelTransformedName(secondaryModelName), required, through: manual.through || (relation === null || relation === void 0 ? void 0 : relation.through), }, }; if (manual.children) { pipeline.populate.children = {}; const myInfo = [...parentInfo, pipeline]; for (const child in manual.children) { pipeline.populate.children[child] = this.populationToPipeline(options, pipeline.populate.modelName, manual.children[child], myInfo, globalPathPrefix + (appendBrackets ? "[]" : ""), localPathPrefix + (appendBrackets ? "[]" : "")); } } return pipeline; } populationToObjects(populationObject) { const arr = {}; for (const field in populationObject) { const popul = populationObject[field]; if (popul === 1 || popul === true) { arr[field] = { field, manual: {}, }; } else if (typeof popul === "object" && popul !== null) { const { populate } = popul, restPppulate = __rest(popul, ["populate"]); arr[field] = { field, manual: restPppulate, }; if (populate !== undefined) { const children = this.populationToObjects(populate); if (Object.keys(children).length > 0) { arr[field].manual.children = children; } } } else if (popul === false || popul === undefined) { } else { throw new errors_1.QueryBuilderError(`Incorrect populate ${JSON.stringify(popul)}`); } } return arr; } } function swapRelation(relation) { return { model1: relation.model2, model2: relation.model1, through: !relation.through ? relation.through : Object.assign(Object.assign({}, relation.through), { model1Field: relation.through.model2Field, model2Field: relation.through.model1Field }), }; } const populationOptionsToRequiredPopulationOptions = (options) => { let childrenObj = undefined; if (options.children) { childrenObj = {}; for (const key in options.children) { childrenObj[key] = populationOptionsToRequiredPopulationOptions(options.children[key].populate); } } return { localField: options.localField, foreignField: options.foreignField, matchesMany: options.matchesMany, required: options.required, model: options.modelName, through: options.through, populate: childrenObj, }; }; const addPipelineForFindOne = (pipelines, engine) => { const is = engine.pipeLineIsHelper || is_1.defaultPipelineIs; let lastIndex = -1; for (let i = pipelines.length - 1; i >= 0; --i) { const pipeline = pipelines[i]; if (is.changingCountOrOrder(pipeline)) { lastIndex = i; break; } } return pipelines .slice(0, lastIndex + 1) .concat([{ limit: 1 }]) .concat(pipelines.slice(lastIndex + 1)); }; const getLastSkipAndLimitIndex = (pipelines) => { let lastIndex = -1; let previousWas = false; for (let i = 0; i < pipelines.length; ++i) { if (typeof pipelines[i].limit === "number" || typeof pipelines[i].skip === "number") { if (!previousWas) lastIndex = i; previousWas = true; } else previousWas = false; } return lastIndex; }; exports.getLastSkipAndLimitIndex = getLastSkipAndLimitIndex; const isEmptyObject = (obj) => { for (const key in obj) { if (obj.hasOwnProperty(key)) { if (obj[key] !== undefined) return false; } } return true; };