UNPKG

ts-patch-mongoose

Version:
160 lines (130 loc) 5.26 kB
import jsonpatch from 'fast-json-patch' // Using CJS lodash with .js extensions for ESM compatibility import chunk from 'lodash/chunk.js' import isEmpty from 'lodash/isEmpty.js' import isFunction from 'lodash/isFunction.js' import omit from 'omit-deep' import em from './em' import { HistoryModel } from './model' import type { HydratedDocument, MongooseError, Types } from 'mongoose' import type { Metadata, PatchContext, PatchEvent, PluginOptions, User } from './types' function isPatchHistoryEnabled<T>(opts: PluginOptions<T>, context: PatchContext<T>): boolean { return !opts.patchHistoryDisabled && !context.ignorePatchHistory } export function getJsonOmit<T>(opts: PluginOptions<T>, doc: HydratedDocument<T>): Partial<T> { const object = JSON.parse(JSON.stringify(doc)) as Partial<T> if (opts.omit) { return omit(object, opts.omit) } return object } export function getObjectOmit<T>(opts: PluginOptions<T>, doc: HydratedDocument<T>): Partial<T> { if (opts.omit) { return omit(isFunction(doc?.toObject) ? doc.toObject() : doc, opts.omit) } return doc } export async function getUser<T>(opts: PluginOptions<T>, doc: HydratedDocument<T>): Promise<User | undefined> { if (isFunction(opts.getUser)) { return await opts.getUser(doc) } return undefined } export async function getReason<T>(opts: PluginOptions<T>, doc: HydratedDocument<T>): Promise<string | undefined> { if (isFunction(opts.getReason)) { return await opts.getReason(doc) } return undefined } export async function getMetadata<T>(opts: PluginOptions<T>, doc: HydratedDocument<T>): Promise<Metadata | undefined> { if (isFunction(opts.getMetadata)) { return await opts.getMetadata(doc) } return undefined } export function getValue<T>(item: PromiseSettledResult<T>): T | undefined { return item.status === 'fulfilled' ? item.value : undefined } export async function getData<T>(opts: PluginOptions<T>, doc: HydratedDocument<T>): Promise<[User | undefined, string | undefined, Metadata | undefined]> { return Promise.allSettled([getUser(opts, doc), getReason(opts, doc), getMetadata(opts, doc)]).then(([user, reason, metadata]) => { return [getValue(user), getValue(reason), getValue(metadata)] }) } export function emitEvent<T>(context: PatchContext<T>, event: string | undefined, data: PatchEvent<T>): void { if (event && !context.ignoreEvent) { em.emit(event, data) } } export async function bulkPatch<T>(opts: PluginOptions<T>, context: PatchContext<T>, eventKey: 'eventCreated' | 'eventDeleted', docsKey: 'createdDocs' | 'deletedDocs'): Promise<void> { const history = isPatchHistoryEnabled(opts, context) const event = opts[eventKey] const docs = context[docsKey] const key = eventKey === 'eventCreated' ? 'doc' : 'oldDoc' if (isEmpty(docs) || (!event && !history)) return const chunks = chunk(docs, 1000) for (const chunk of chunks) { const bulk = [] for (const doc of chunk) { emitEvent(context, event, { [key]: doc }) if (history) { const [user, reason, metadata] = await getData(opts, doc) bulk.push({ insertOne: { document: { op: context.op, modelName: context.modelName, collectionName: context.collectionName, collectionId: doc._id as Types.ObjectId, doc: getObjectOmit(opts, doc), user, reason, metadata, version: 0, }, }, }) } } if (history && !isEmpty(bulk)) { await HistoryModel.bulkWrite(bulk, { ordered: false }).catch((error: MongooseError) => { console.error(error.message) }) } } } export async function createPatch<T>(opts: PluginOptions<T>, context: PatchContext<T>): Promise<void> { await bulkPatch(opts, context, 'eventCreated', 'createdDocs') } export async function updatePatch<T>(opts: PluginOptions<T>, context: PatchContext<T>, current: HydratedDocument<T>, original: HydratedDocument<T>): Promise<void> { const history = isPatchHistoryEnabled(opts, context) const currentObject = getJsonOmit(opts, current) const originalObject = getJsonOmit(opts, original) if (isEmpty(originalObject) || isEmpty(currentObject)) return const patch = jsonpatch.compare(originalObject, currentObject, true) if (isEmpty(patch)) return emitEvent(context, opts.eventUpdated, { oldDoc: original, doc: current, patch }) if (history) { let version = 0 const lastHistory = await HistoryModel.findOne({ collectionId: original._id as Types.ObjectId }) .sort('-version') .exec() if (lastHistory && lastHistory.version >= 0) { version = lastHistory.version + 1 } const [user, reason, metadata] = await getData(opts, current) await HistoryModel.create({ op: context.op, modelName: context.modelName, collectionName: context.collectionName, collectionId: original._id as Types.ObjectId, patch, user, reason, metadata, version, }) } } export async function deletePatch<T>(opts: PluginOptions<T>, context: PatchContext<T>): Promise<void> { await bulkPatch(opts, context, 'eventDeleted', 'deletedDocs') }