papr
Version:
MongoDB TypeScript-aware Models
174 lines (173 loc) • 6.13 kB
JavaScript
import types from "./types.js";
import { VALIDATION_ACTIONS, VALIDATION_LEVEL, getTimestampProperty } from "./utils.js";
// This removes the artificial `$required` attributes added in the object schemas
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function sanitize(value) {
if (!value || typeof value !== 'object') {
return;
}
if ('$required' in value) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
delete value.$required;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
for (const key of Object.keys(value)) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
sanitize(value[key]);
}
}
/**
* @module intro
* @description
*
* A schema is what defines a Model and the validation performed on the collection associated with the Model.
*/
/**
* Creates a schema for a model and define validation options for it.
*
* Read more about the validation [options](https://docs.mongodb.com/manual/core/schema-validation/index.html#behavior)
* in the MongoDB docs.
*
* Under the hood it is just a wrapper around the [`object`](api/types.md#object) type with some default properties
* (e.g. `_id` and timestamps properties).
*
* While the default `_id` property is added with an `ObjectId` type, its type can be customized into a `string` or a `number`.
*
* The options are exported as a result type (the second value in the return tuple).
*
* The `defaults` option can be a static object, a function that returns an object or an async function that returns an object.
*
* @name schema
*
* @param properties {Record<string, unknown>}
* @param [options] {SchemaOptions}
* @param [options.defaults] {DefaultsOption<TProperties>}
* @param [options.timestamps=false] {TimestampSchemaOptions}
* @param [options.validationAction=VALIDATION_ACTIONS.ERROR] {VALIDATION_ACTIONS}
* @param [options.validationLevel=VALIDATION_LEVEL.STRICT] {VALIDATION_LEVEL}
*
* @returns {Array} The return type is `[TSchema, TOptions]`
*
* @example
* import { schema, types } from 'papr';
*
* const userSchema = schema({
* active: types.boolean(),
* age: types.number(),
* firstName: types.string({ required: true }),
* lastName: types.string({ required: true }),
* });
*
* export type UserDocument = typeof userSchema[0];
* export type UserOptions = typeof userSchema[1];
*
* @example
* // Example with static defaults, timestamps and validation options
*
* import { schema, types, VALIDATION_ACTIONS, VALIDATION_LEVEL } from 'papr';
*
* const orderSchema = schema({
* _id: types.number({ required: true }),
* user: types.objectId({ required: true }),
* product: types.string({ required: true })
* }, {
* defaults: { product: 'test' },
* timestamps: true,
* validationAction: VALIDATION_ACTIONS.WARN,
* validationLevel: VALIDATION_LEVEL.MODERATE
* });
*
* export type OrderDocument = typeof orderSchema[0];
* export type OrderOptions = typeof orderSchema[1];
*
* @example
* // Example with static defaults of const enums
*
* import { schema, types, VALIDATION_ACTIONS, VALIDATION_LEVEL } from 'papr';
*
* const statuses = ['processing', 'shipped'] as const;
* type Status = (typeof statuses)[number];
*
* const orderSchema = schema({
* _id: types.number({ required: true }),
* user: types.objectId({ required: true }),
* status: types.enum(statuses, { required: true })
* }, {
* defaults: {
* // const enums require the full type cast in defaults
* status: 'processing' as Status
* }
* });
*
* export type OrderDocument = typeof orderSchema[0];
* export type OrderOptions = typeof orderSchema[1];
*
* @example
* // Example with dynamic defaults
*
* import { schema, types } from 'papr';
*
* const userSchema = schema({
* active: types.boolean(),
* birthDate: types.date(),
* firstName: types.string({ required: true }),
* lastName: types.string({ required: true }),
* }, {
* defaults: () => ({
* birthDate: new Date();
* })
* });
*
* export type UserDocument = (typeof userSchema)[0];
* export type UserOptions = (typeof userSchema)[1];
*
* @example
* // Example with async dynamic defaults
*
* import { schema, types } from 'papr';
*
* function getDateAsync(): Promise<Date> {
* return Promise.resolve(new Date());
* }
*
* const userSchema = schema({
* active: types.boolean(),
* birthDate: types.date(),
* firstName: types.string({ required: true }),
* lastName: types.string({ required: true }),
* }, {
* defaults: async () => ({
* birthDate: await getDateAsync();
* })
* });
*
* export type UserDocument = (typeof userSchema)[0];
* export type UserOptions = (typeof userSchema)[1];
*/
export function schema(properties, options) {
const { defaults, timestamps = false, validationAction = VALIDATION_ACTIONS.ERROR, validationLevel = VALIDATION_LEVEL.STRICT, } = options || {};
const createdAtProperty = getTimestampProperty('createdAt', timestamps);
const updatedAtProperty = getTimestampProperty('updatedAt', timestamps);
const value = types.object({
_id: types.objectId({ required: true }),
...properties,
...(timestamps && {
[createdAtProperty]: types.date({ required: true }),
[updatedAtProperty]: types.date({ required: true }),
}),
}, { required: true });
sanitize(value);
if (defaults) {
// @ts-expect-error We're defining these defaults now and removing them later in `updateSchema()`
value.$defaults = defaults;
}
if (timestamps) {
// @ts-expect-error We're defining this option now and removing them later in `updateSchema()`
value.$timestamps = timestamps;
}
// @ts-expect-error We're defining this option now and removing it later in `updateSchema()`
value.$validationAction = validationAction;
// @ts-expect-error We're defining this option now and removing it later in `updateSchema()`
value.$validationLevel = validationLevel;
return value;
}