Skip to content

Commit

Permalink
test: Improve docs & tests of const enums (#924)
Browse files Browse the repository at this point in the history
  • Loading branch information
avaly authored Sep 23, 2024
1 parent d8cbb32 commit f30f365
Show file tree
Hide file tree
Showing 7 changed files with 177 additions and 30 deletions.
28 changes: 28 additions & 0 deletions docs/api/schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
14 changes: 8 additions & 6 deletions docs/api/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -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
Expand All @@ -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),
});
```

Expand Down
23 changes: 14 additions & 9 deletions src/__tests__/model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,6 @@ import Types from '../types';
describe('model', () => {
let collection: Collection;

const DEFAULTS = {
bar: 123456,
};
const projection = {
foo: 1,
ham: 1,
Expand All @@ -29,7 +26,9 @@ describe('model', () => {
}),
},
{
defaults: DEFAULTS,
defaults: {
bar: 123456,
},
}
);

Expand All @@ -44,7 +43,9 @@ describe('model', () => {
}),
},
{
defaults: DEFAULTS,
defaults: {
bar: 123456,
},
timestamps: true,
}
);
Expand All @@ -60,7 +61,9 @@ describe('model', () => {
}),
},
{
defaults: DEFAULTS,
defaults: {
bar: 123456,
},
timestamps: {
createdAt: '_createdDate' as const,
updatedAt: '_updatedDate' as const,
Expand All @@ -80,7 +83,9 @@ describe('model', () => {
}),
},
{
defaults: DEFAULTS,
defaults: {
bar: 123456,
},
}
);

Expand Down Expand Up @@ -1605,7 +1610,7 @@ describe('model', () => {
{ upsert: true }
);

expectType<SimpleDocument | null>(result);
expectType<TimestampsDocument | null>(result);

if (result) {
expectType<ObjectId>(result._id);
Expand Down Expand Up @@ -1676,7 +1681,7 @@ describe('model', () => {
{ upsert: true }
);

expectType<SimpleDocument | null>(result);
expectType<TimestampConfigDocument | null>(result);

if (result) {
expectType<ObjectId>(result._id);
Expand Down
102 changes: 95 additions & 7 deletions src/__tests__/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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<ObjectId>(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(
Expand Down Expand Up @@ -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 }),
Expand All @@ -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,
Expand Down Expand Up @@ -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'],
},
Expand Down Expand Up @@ -704,6 +790,7 @@ describe('schema', () => {
'constantRequired',
'dateRequired',
'decimalRequired',
'enumConstRequired',
'enumRequired',
'nullRequired',
'nullableOneOfRequired',
Expand Down Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions src/__tests__/types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down
22 changes: 22 additions & 0 deletions src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
14 changes: 8 additions & 6 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -397,13 +397,15 @@ export default {
decimal: createSimpleType<Decimal128>('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<TValue>}
* @param [options] {GenericOptions}
* @param [options.required] {boolean}
Expand All @@ -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
Expand All @@ -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,
Expand Down

0 comments on commit f30f365

Please sign in to comment.