popurelate
Version:
Easily populate and filter documents using data from related collections.
546 lines (545 loc) • 24.6 kB
JavaScript
"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;
};