UNPKG

papr

Version:

MongoDB TypeScript-aware Models

247 lines (246 loc) 8.36 kB
import { ObjectId } from 'mongodb'; // Some of the types are adapted from originals at: https://github.com/mongodb/node-mongodb-native/blob/v5.0.1/src/mongo_types.ts // licensed under Apache License 2.0: https://github.com/mongodb/node-mongodb-native/blob/v5.0.1/LICENSE.md export var VALIDATION_ACTIONS; (function (VALIDATION_ACTIONS) { VALIDATION_ACTIONS["ERROR"] = "error"; VALIDATION_ACTIONS["WARN"] = "warn"; })(VALIDATION_ACTIONS || (VALIDATION_ACTIONS = {})); export var VALIDATION_LEVEL; (function (VALIDATION_LEVEL) { VALIDATION_LEVEL["MODERATE"] = "moderate"; VALIDATION_LEVEL["OFF"] = "off"; VALIDATION_LEVEL["STRICT"] = "strict"; })(VALIDATION_LEVEL || (VALIDATION_LEVEL = {})); export function getIds(ids) { return [...ids].map((id) => new ObjectId(id)); } /** * @module intro * @description * * ## `DocumentForInsert` * * This TypeScript type is useful to define an document representation for an insertion operation, where the `_id` and * other properties which have defaults defined are not required. * * ```ts * import { DocumentForInsert } from 'papr'; * * import type { OrderDocument, OrderOptions } from './schema'; * * const newOrder: DocumentForInsert<OrderDocument, OrderOptions> = { * user: 'John', * }; * * newOrder._id; // ObjectId | undefined * newOrder.user; // string * newOrder.product; // string | undefined * ``` * * ## `ProjectionType` * * This TypeScript type is useful to compute the sub-document resulting from a `find*` operation which used a projection. * * ```ts * import { ProjectionType } from 'papr'; * * const projection = { * firstName: 1 * }; * * type UserProjected = ProjectionType<UserDocument, typeof projection>; * * const user: UserProjected = await User.findOne({}, { projection }); * * user?._id; // value * user?.firstName; // value * user?.lastName; // TypeScript error * user?.age; // TypeScript error * ``` * * When this type is used in conjunction with `as const`, it allows projections with excluding fields. * * ```ts * import { ProjectionType } from 'papr'; * * const projection = { * firstName: 0, * } as const; * * type UserProjected = ProjectionType<UserDocument, typeof projection>; * * const user: UserProjected = await User.findOne({}, { projection }); * * user?._id; // value * user?.firstName; // TypeScript error * user?.lastName; // value * user?.age; // value * ``` * * ## `VALIDATION_ACTIONS` * * ```ts * enum VALIDATION_ACTIONS { * ERROR = 'error', * WARN = 'warn', * } * ``` * * ## `VALIDATION_LEVEL` * * ```ts * enum VALIDATION_LEVEL { * MODERATE = 'moderate', * OFF = 'off', * STRICT = 'strict', * } * ``` */ // Checks the type of the model defaults property and if a function, returns // the result of the function call, otherwise returns the object export async function getDefaultValues(defaults) { if (typeof defaults === 'function') { return await defaults(); } if (typeof defaults === 'object') { return defaults; } return {}; } // Returns either the default timestamp property or the value supplied in timestamp options export function getTimestampProperty(property, options) { if (typeof options === 'object') { return (options[property] ?? property); } return property; } // Creates new update object so the original doesn't get mutated export function timestampUpdateFilter(update, timestamps) { const updatedAtProperty = getTimestampProperty('updatedAt', timestamps); const $currentDate = { ...update.$currentDate, // @ts-expect-error Ignore dynamic string property access ...(!update.$set?.[updatedAtProperty] && !update.$unset?.[updatedAtProperty] && { [updatedAtProperty]: true, }), }; // @ts-expect-error `TSchema` is a `TimestampSchema`, but we can't extend that base type return { ...update, ...(Object.keys($currentDate).length > 0 && { $currentDate }), }; } // Creates new operation objects so the original operations don't get mutated export function timestampBulkWriteOperation(operation, timestamps) { const createdAtProperty = getTimestampProperty('createdAt', timestamps); const updatedAtProperty = getTimestampProperty('updatedAt', timestamps); if ('insertOne' in operation) { return { insertOne: { document: { [createdAtProperty]: new Date(), [updatedAtProperty]: new Date(), ...operation.insertOne.document, }, }, }; } if ('updateOne' in operation) { const { update } = operation.updateOne; // Skip aggregation pipeline updates if (Array.isArray(update)) { return operation; } const $currentDate = { ...update.$currentDate, // @ts-expect-error Ignore dynamic string property access ...(!update.$set?.[updatedAtProperty] && !update.$unset?.[updatedAtProperty] && { [updatedAtProperty]: true, }), }; const $setOnInsert = { ...update.$setOnInsert, // @ts-expect-error Ignore dynamic string property access ...(!update.$set?.[createdAtProperty] && !update.$unset?.[createdAtProperty] && { [createdAtProperty]: new Date(), }), }; return { updateOne: { ...operation.updateOne, // @ts-expect-error `TSchema` is a `TimestampSchema`, but we can't extend that base type update: { ...update, ...(Object.keys($currentDate).length > 0 && { $currentDate }), ...(Object.keys($setOnInsert).length > 0 && { $setOnInsert }), }, }, }; } if ('updateMany' in operation) { const { update } = operation.updateMany; // Skip aggregation pipeline updates if (Array.isArray(update)) { return operation; } const $currentDate = { ...update.$currentDate, // @ts-expect-error Ignore dynamic string property access ...(!update.$set?.[updatedAtProperty] && !update.$unset?.[updatedAtProperty] && { [updatedAtProperty]: true, }), }; const $setOnInsert = { ...update.$setOnInsert, // @ts-expect-error Ignore dynamic string property access ...(!update.$set?.[createdAtProperty] && !update.$unset?.[createdAtProperty] && { [createdAtProperty]: new Date(), }), }; return { updateMany: { ...operation.updateMany, // @ts-expect-error `TSchema` is a `TimestampSchema`, but we can't extend that base type update: { ...update, ...(Object.keys($currentDate).length > 0 && { $currentDate }), ...(Object.keys($setOnInsert).length > 0 && { $setOnInsert }), }, }, }; } if ('replaceOne' in operation) { return { replaceOne: { ...operation.replaceOne, replacement: { [createdAtProperty]: new Date(), [updatedAtProperty]: new Date(), ...operation.replaceOne.replacement, }, }, }; } return operation; } // Clean defaults if properties are present in $set, $push, $inc or $unset // Note: typing the `$setOnInsert` parameter as `NonNullable<PaprUpdateFilter<TSchema>['$setOnInsert']>` // triggers a stack overflow error in `tsc`, so we choose a simple `Record` type here. export function cleanSetOnInsert($setOnInsert, update) { for (const key of Object.keys($setOnInsert)) { if (key in (update.$set || {}) || key in (update.$push || {}) || key in (update.$inc || {}) || key in (update.$unset || {})) { // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete $setOnInsert[key]; } } return $setOnInsert; }