papr
Version:
MongoDB TypeScript-aware Models
247 lines (246 loc) • 8.36 kB
JavaScript
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;
}