mingo
Version:
MongoDB query language for in-memory objects
224 lines (223 loc) • 7.23 kB
JavaScript
import { ComputeOptions } from "./core/_internal";
import { Lazy } from "./lazy";
import * as booleanOperators from "./operators/expression/boolean";
import * as comparisonOperators from "./operators/expression/comparison";
import { $addFields } from "./operators/pipeline/addFields";
import { $project } from "./operators/pipeline/project";
import { $replaceRoot } from "./operators/pipeline/replaceRoot";
import { $replaceWith } from "./operators/pipeline/replaceWith";
import { $set } from "./operators/pipeline/set";
import { $sort } from "./operators/pipeline/sort";
import { $unset } from "./operators/pipeline/unset";
import * as queryOperators from "./operators/query";
import * as UPDATE_OPERATORS from "./operators/update";
import {
buildParams,
Trie
} from "./operators/update/_internal";
import { Query } from "./query";
import {
assert,
cloneDeep,
ensureArray,
hashCode,
isArray,
isEqual,
resolve
} from "./util";
const PIPELINE_OPERATORS = {
$addFields,
$set,
$project,
$unset,
$replaceRoot,
$replaceWith
};
function update(obj, modifier, arrayFilters, condition, options) {
const docs = [obj];
const res = updateOne(
docs,
condition || {},
modifier,
{ arrayFilters, cloneMode: options?.cloneMode ?? "copy" },
options?.queryOptions
);
return res.modifiedFields ?? [];
}
function updateMany(documents, condition, modifier, updateConfig = {}, options) {
const { modifiedCount, matchedCount } = updateDocuments(
documents,
condition,
modifier,
updateConfig,
options
);
return { modifiedCount, matchedCount };
}
function updateOne(documents, condition, modifier, updateConfig = {}, options) {
return updateDocuments(documents, condition, modifier, updateConfig, {
...options,
firstOnly: true
});
}
function updateDocuments(documents, condition, modifier, updateConfig = {}, options) {
options ||= {};
const firstOnly = options?.firstOnly ?? false;
const opts = ComputeOptions.init({
...options,
collation: Object.assign({}, options?.collation, updateConfig?.collation)
}).update({
condition,
updateConfig: { cloneMode: "copy", ...updateConfig },
variables: updateConfig.let
});
opts.context.addExpressionOps(booleanOperators).addExpressionOps(comparisonOperators).addQueryOps(queryOperators).addPipelineOps(PIPELINE_OPERATORS);
const filterExists = Object.keys(condition).length > 0;
const matchedDocs = /* @__PURE__ */ new Map();
let docsIter = Lazy(documents);
if (filterExists) {
const query = new Query(condition, opts);
docsIter = docsIter.filter((o, i) => {
if (query.test(o)) {
matchedDocs.set(o, i);
return true;
}
return false;
});
}
let modifiedIndex = -1;
if (firstOnly) {
const indexes = /* @__PURE__ */ new Map();
if (updateConfig.sort) {
if (!filterExists) {
docsIter = docsIter.map((o, i) => {
indexes.set(o, i);
return o;
});
}
docsIter = $sort(docsIter, updateConfig.sort, opts);
}
docsIter = docsIter.take(1);
const firstDoc = docsIter.collect()[0];
modifiedIndex = matchedDocs.get(firstDoc) ?? indexes.get(firstDoc) ?? 0;
}
const foundDocs = docsIter.collect();
if (foundDocs.length === 0) return { matchedCount: 0, modifiedCount: 0 };
if (isArray(modifier)) {
const indexes = firstOnly ? [modifiedIndex] : Array.from(matchedDocs.values());
const hashes = indexes.length ? indexes.map((i) => hashCode(documents[i])) : foundDocs.map((o) => hashCode(o));
const output2 = { matchedCount: hashes.length, modifiedCount: 0 };
const oldFirstDoc = firstOnly ? cloneDeep(documents[indexes[0]]) : void 0;
let updateIter = Lazy(foundDocs);
for (const stage of modifier) {
const [op, expr] = Object.entries(stage)[0];
const pipelineOp = PIPELINE_OPERATORS[op];
assert(pipelineOp, `Unknown pipeline operator: '${op}'.`);
updateIter = pipelineOp(updateIter, expr, opts);
}
const matches = updateIter.collect();
if (indexes.length) {
assert(
indexes.length === matches.length,
"bug: indexes and result size must match."
);
for (let i = 0; i < indexes.length; i++) {
if (hashCode(matches[i]) !== hashes[i]) {
documents[indexes[i]] = matches[i];
output2.modifiedCount++;
}
}
} else {
for (let i = 0; i < documents.length; i++) {
if (hashCode(matches[i]) !== hashes[i]) {
documents[i] = matches[i];
output2.modifiedCount++;
}
}
}
if (firstOnly && output2.modifiedCount) {
const newDoc = documents[indexes[0]];
const modifiedFields2 = getModifiedFields(
modifier,
oldFirstDoc,
newDoc
);
assert(modifiedFields2.length, "bug: failed to retrieve modified fields");
Object.assign(output2, { modifiedFields: modifiedFields2, modifiedIndex });
}
return output2;
}
const unknownOp = Object.keys(modifier).find((op) => !UPDATE_OPERATORS[op]);
assert(!unknownOp, `Unknown update operator: '${unknownOp}'.`);
const arrayFilters = updateConfig?.arrayFilters ?? [];
opts.update({
updateParams: buildParams(
Object.values(modifier),
arrayFilters,
opts
)
});
const matchedCount = foundDocs.length;
const output = { matchedCount, modifiedCount: 0 };
const modifiedFields = [];
for (const doc of foundDocs) {
let modified = false;
for (const [op, expr] of Object.entries(modifier)) {
const mutate = UPDATE_OPERATORS[op];
const fields = mutate(doc, expr, arrayFilters, opts);
if (fields.length) {
modified = true;
if (firstOnly) Array.prototype.push.apply(modifiedFields, fields);
}
}
output.modifiedCount += +modified;
}
if (firstOnly && modifiedFields.length) {
modifiedFields.sort();
Object.assign(output, { modifiedFields, modifiedIndex });
}
return output;
}
function getModifiedFields(pipeline, oldDoc, newDoc) {
const stageFields = [];
for (const stage of pipeline) {
const op = Object.keys(stage)[0];
switch (op) {
case "$addFields":
case "$set":
case "$project":
case "$replaceWith":
stageFields.push(...Object.keys(stage[op]));
break;
case "$unset":
stageFields.push(...ensureArray(stage[op]));
break;
case "$replaceRoot":
stageFields.push(
...Object.keys(stage[op]?.newRoot || [])
);
break;
}
}
const stageFieldsSet = new Set(stageFields.sort());
const conflictDetector = new Trie();
const modifiedFields = [];
for (const key of stageFieldsSet) {
if (conflictDetector.add(key) && !isEqual(resolve(newDoc, key), resolve(oldDoc, key))) {
modifiedFields.push(key);
}
}
for (const key of Object.keys(oldDoc)) {
if (stageFieldsSet.has(key)) continue;
if (!conflictDetector.add(key) || !isEqual(newDoc[key], oldDoc[key])) {
modifiedFields.push(key);
}
}
const topLevelFilter = new Trie();
return modifiedFields.sort().filter((key) => topLevelFilter.add(key));
}
export {
update,
updateMany,
updateOne
};