From f30f365321a33be84fa7963b14d1bf0bc87ece22 Mon Sep 17 00:00:00 2001 From: Valentin Agachi Date: Mon, 23 Sep 2024 10:01:05 +0200 Subject: [PATCH] test: Improve docs & tests of const enums (#924) --- docs/api/schema.md | 28 ++++++++++ docs/api/types.md | 14 ++--- src/__tests__/model.test.ts | 23 ++++---- src/__tests__/schema.test.ts | 102 ++++++++++++++++++++++++++++++++--- src/__tests__/types.test.ts | 4 +- src/schema.ts | 22 ++++++++ src/types.ts | 14 ++--- 7 files changed, 177 insertions(+), 30 deletions(-) diff --git a/docs/api/schema.md b/docs/api/schema.md index a4d4c0c6..c9b8f5e5 100644 --- a/docs/api/schema.md +++ b/docs/api/schema.md @@ -78,6 +78,34 @@ export type OrderOptions = (typeof orderSchema)[1]; **Example:** +```ts +// 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:** + ```ts // Example with dynamic defaults diff --git a/docs/api/types.md b/docs/api/types.md index 2c37f9fe..97f05b50 100644 --- a/docs/api/types.md +++ b/docs/api/types.md @@ -182,13 +182,15 @@ schema({ ## `enum` -With `enum` you can create an enum type either: +With `enum` you can create an enum type based on either: -- based on a TypeScript `enum` structure -- based on an array of `const` +- a TypeScript `enum` structure +- a readonly/const array (`as const`) Enum types may contain `null` as well. +Const enums require a full type cast when used in the schema `defaults`. + **Parameters:** | Name | Type | Attribute | @@ -207,7 +209,7 @@ enum SampleEnum { bar = 'bar', } -const SampleArray = ['foo' as const, 'bar' as const]; +const SampleConstArray = ['foo', 'bar'] as const; schema({ // type: SampleEnum @@ -217,9 +219,9 @@ schema({ // type: SampleEnum | null | undefined optionalEnumWithNull: types.enum([...Object.values(SampleEnum), null]), // type: 'foo' | 'bar' - requiredEnumAsConstArray: types.enum(SampleArray, { required: true }), + requiredEnumAsConstArray: types.enum(SampleConstArray, { required: true }), // type: 'foo' | 'bar' | undefined - optionalEnumAsConstArray: types.enum(SampleArray), + optionalEnumAsConstArray: types.enum(SampleConstArray), }); ``` diff --git a/src/__tests__/model.test.ts b/src/__tests__/model.test.ts index a278c373..f5146a8e 100644 --- a/src/__tests__/model.test.ts +++ b/src/__tests__/model.test.ts @@ -10,9 +10,6 @@ import Types from '../types'; describe('model', () => { let collection: Collection; - const DEFAULTS = { - bar: 123456, - }; const projection = { foo: 1, ham: 1, @@ -29,7 +26,9 @@ describe('model', () => { }), }, { - defaults: DEFAULTS, + defaults: { + bar: 123456, + }, } ); @@ -44,7 +43,9 @@ describe('model', () => { }), }, { - defaults: DEFAULTS, + defaults: { + bar: 123456, + }, timestamps: true, } ); @@ -60,7 +61,9 @@ describe('model', () => { }), }, { - defaults: DEFAULTS, + defaults: { + bar: 123456, + }, timestamps: { createdAt: '_createdDate' as const, updatedAt: '_updatedDate' as const, @@ -80,7 +83,9 @@ describe('model', () => { }), }, { - defaults: DEFAULTS, + defaults: { + bar: 123456, + }, } ); @@ -1605,7 +1610,7 @@ describe('model', () => { { upsert: true } ); - expectType(result); + expectType(result); if (result) { expectType(result._id); @@ -1676,7 +1681,7 @@ describe('model', () => { { upsert: true } ); - expectType(result); + expectType(result); if (result) { expectType(result._id); diff --git a/src/__tests__/schema.test.ts b/src/__tests__/schema.test.ts index 3250e53d..bab6e8c3 100644 --- a/src/__tests__/schema.test.ts +++ b/src/__tests__/schema.test.ts @@ -9,7 +9,7 @@ enum TEST_ENUM { FOO = 'foo', BAR = 'bar', } -const READONLY_CONST_VALUES = ['qux', 'baz'] as const; +const CONST_ENUM = ['ham', 'baz'] as const; describe('schema', () => { test('simple', () => { @@ -117,6 +117,86 @@ describe('schema', () => { }); }); + test('with enums & defaults', () => { + const value = schema( + { + enumConstOptional: types.enum(CONST_ENUM), + enumConstRequired: types.enum(CONST_ENUM, { required: true }), + enumOptional: types.enum(Object.values(TEST_ENUM)), + enumRequired: types.enum(Object.values(TEST_ENUM), { required: true }), + }, + { + defaults: { + enumConstOptional: 'ham' as (typeof CONST_ENUM)[number], + enumConstRequired: 'baz' as (typeof CONST_ENUM)[number], + enumOptional: TEST_ENUM.FOO, + enumRequired: TEST_ENUM.BAR, + }, + } + ); + + expect(value).toEqual({ + $defaults: { + enumConstOptional: 'ham', + enumConstRequired: 'baz', + enumOptional: 'foo', + enumRequired: 'bar', + }, + $validationAction: 'error', + $validationLevel: 'strict', + additionalProperties: false, + properties: { + _id: { + bsonType: 'objectId', + }, + enumConstOptional: { + enum: ['ham', 'baz'], + }, + enumConstRequired: { + enum: ['ham', 'baz'], + }, + enumOptional: { + enum: ['foo', 'bar'], + }, + enumRequired: { + enum: ['foo', 'bar'], + }, + }, + required: ['_id', 'enumConstRequired', 'enumRequired'], + type: 'object', + }); + + expectType< + [ + { + _id: ObjectId; + enumConstOptional?: 'baz' | 'ham'; + }, + { + defaults: { + enumConstOptional?: 'baz' | 'ham'; + enumOptional?: TEST_ENUM; + }; + }, + ] + >(value); + expectType(value[0]?._id); + expectType<'baz' | 'ham' | undefined>(value[0]?.enumConstOptional); + expectType<'baz' | 'ham'>(value[0]?.enumConstRequired); + expectType<(typeof value)[0]>({ + _id: new ObjectId(), + enumConstOptional: 'ham', + enumConstRequired: 'baz', + enumOptional: TEST_ENUM.FOO, + enumRequired: TEST_ENUM.BAR, + }); + expectType<(typeof value)[0]>({ + _id: new ObjectId(), + enumConstRequired: 'baz', + enumRequired: TEST_ENUM.BAR, + }); + }); + describe('with timestamps', () => { test('enabled', () => { const value = schema( @@ -465,8 +545,9 @@ describe('schema', () => { dateRequired: types.date({ required: true }), decimalOptional: types.decimal(), decimalRequired: types.decimal({ required: true }), + enumConstOptional: types.enum(CONST_ENUM), + enumConstRequired: types.enum(CONST_ENUM, { required: true }), enumOptional: types.enum([...Object.values(TEST_ENUM), null]), - enumReadonly: types.enum(READONLY_CONST_VALUES), enumRequired: types.enum(Object.values(TEST_ENUM), { required: true }), nullOptional: types.null(), nullRequired: types.null({ required: true }), @@ -493,7 +574,9 @@ describe('schema', () => { stringRequired: types.string({ required: true }), }, { - defaults: { stringOptional: 'foo' }, + defaults: { + stringOptional: 'foo', + }, timestamps: true, validationAction: VALIDATION_ACTIONS.WARN, validationLevel: VALIDATION_LEVEL.MODERATE, @@ -595,12 +678,15 @@ describe('schema', () => { decimalRequired: { bsonType: 'decimal', }, + enumConstOptional: { + enum: ['ham', 'baz'], + }, + enumConstRequired: { + enum: ['ham', 'baz'], + }, enumOptional: { enum: ['foo', 'bar', null], }, - enumReadonly: { - enum: ['qux', 'baz'], - }, enumRequired: { enum: ['foo', 'bar'], }, @@ -704,6 +790,7 @@ describe('schema', () => { 'constantRequired', 'dateRequired', 'decimalRequired', + 'enumConstRequired', 'enumRequired', 'nullRequired', 'nullableOneOfRequired', @@ -740,8 +827,9 @@ describe('schema', () => { dateRequired: Date; decimalOptional?: Decimal128; decimalRequired: Decimal128; + enumConstOptional?: 'ham' | 'baz'; + enumConstRequired: 'ham' | 'baz'; enumOptional?: TEST_ENUM | null; - enumReadonly?: 'qux' | 'baz'; enumRequired: TEST_ENUM; nullOptional?: null; nullRequired: null; diff --git a/src/__tests__/types.test.ts b/src/__tests__/types.test.ts index 06135b1f..334af9b5 100644 --- a/src/__tests__/types.test.ts +++ b/src/__tests__/types.test.ts @@ -200,8 +200,8 @@ describe('types', () => { types.enum(Object.values(TEST_ENUM), { maxLength: 1 }); }); - test('array of const', () => { - const value = types.enum(['a' as const, 'b' as const]); + test('const array', () => { + const value = types.enum(['a', 'b'] as const); expect(value).toEqual({ enum: ['a', 'b'], diff --git a/src/schema.ts b/src/schema.ts index f98c24af..00127ddc 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -121,6 +121,28 @@ function sanitize(value: any): void { * 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'; diff --git a/src/types.ts b/src/types.ts index 56df1c91..7a98543a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -397,13 +397,15 @@ export default { decimal: createSimpleType('decimal'), /** - * With `enum` you can create an enum type either: + * With `enum` you can create an enum type based on either: * - * - based on a TypeScript `enum` structure - * - based on an array of `const` + * - a TypeScript `enum` structure + * - a readonly/const array (`as const`) * * Enum types may contain `null` as well. * + * Const enums require a full type cast when used in the schema `defaults`. + * * @param values {Array} * @param [options] {GenericOptions} * @param [options.required] {boolean} @@ -416,7 +418,7 @@ export default { * bar = 'bar' * } * - * const SampleArray = ['foo' as const, 'bar' as const]; + * const SampleConstArray = ['foo', 'bar'] as const; * * schema({ * // type: SampleEnum @@ -426,9 +428,9 @@ export default { * // type: SampleEnum | null | undefined * optionalEnumWithNull: types.enum([...Object.values(SampleEnum), null]), * // type: 'foo' | 'bar' - * requiredEnumAsConstArray: types.enum(SampleArray, { required: true }), + * requiredEnumAsConstArray: types.enum(SampleConstArray, { required: true }), * // type: 'foo' | 'bar' | undefined - * optionalEnumAsConstArray: types.enum(SampleArray), + * optionalEnumAsConstArray: types.enum(SampleConstArray), * }); */ enum: enumType,