UNPKG

mingo

Version:

MongoDB query language for in-memory objects

224 lines (223 loc) 7.23 kB
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 };