Skip to content

Commit 716dbb4

Browse files
fix: Allow explicit optional types on schemas (plexinc#207)
1 parent 2da9019 commit 716dbb4

File tree

2 files changed

+313
-3
lines changed

2 files changed

+313
-3
lines changed

src/__tests__/schema.test.ts

Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,4 +522,310 @@ describe('schema', () => {
522522
}>(value[1]);
523523
expectType<ObjectId>(value[0]?._id);
524524
});
525+
526+
test('explicit optional - simple', () => {
527+
const value = schema({
528+
bar: types.number({ required: true }),
529+
foo: types.boolean({ required: false }),
530+
});
531+
532+
expect(value).toEqual({
533+
$validationAction: 'error',
534+
$validationLevel: 'strict',
535+
additionalProperties: false,
536+
properties: {
537+
__v: {
538+
type: 'number',
539+
},
540+
_id: {
541+
bsonType: 'objectId',
542+
},
543+
bar: {
544+
type: 'number',
545+
},
546+
foo: {
547+
type: 'boolean',
548+
},
549+
},
550+
required: ['_id', 'bar'],
551+
type: 'object',
552+
});
553+
554+
expectType<
555+
[
556+
{
557+
_id: ObjectId;
558+
foo?: boolean;
559+
bar: number;
560+
},
561+
{}
562+
]
563+
>(value);
564+
expectType<ObjectId>(value[0]?._id);
565+
expectType<boolean | undefined>(value[0]?.foo);
566+
expectType<typeof value[0]>({
567+
_id: new ObjectId(),
568+
bar: 123,
569+
foo: true,
570+
});
571+
expectType<typeof value[0]>({
572+
_id: new ObjectId(),
573+
bar: 123,
574+
});
575+
});
576+
577+
test('explicit optional - full', () => {
578+
const value = schema(
579+
{
580+
anyOptional: types.any({ required: false }),
581+
anyRequired: types.any({ required: true }),
582+
arrayOfObjects: types.array(
583+
types.object(
584+
{
585+
foo: types.number(),
586+
},
587+
{ required: true }
588+
)
589+
),
590+
arrayOptional: types.array(types.number({ required: false })),
591+
arrayRequired: types.array(types.number(), { required: true }),
592+
binaryOptional: types.binary({ required: false }),
593+
binaryRequired: types.binary({ required: true }),
594+
booleanOptional: types.boolean({ required: false }),
595+
booleanRequired: types.boolean({ required: true }),
596+
dateOptional: types.date({ required: false }),
597+
dateRequired: types.date({ required: true }),
598+
enumOptional: types.enum([...Object.values(TEST_ENUM), null]),
599+
enumRequired: types.enum(Object.values(TEST_ENUM), { required: true }),
600+
numberOptional: types.number({ required: false }),
601+
numberRequired: types.number({ required: true }),
602+
objectGenericOptional: types.objectGeneric(types.number({ required: false })),
603+
objectGenericRequired: types.objectGeneric(types.number(), 'abc.+', { required: true }),
604+
objectIdOptional: types.objectId({ required: false }),
605+
objectIdRequired: types.objectId({ required: true }),
606+
objectOptional: types.object({
607+
foo: types.number({ required: false }),
608+
}),
609+
objectRequired: types.object(
610+
{
611+
foo: types.number({ required: false }),
612+
},
613+
{ required: true }
614+
),
615+
stringOptional: types.string({ required: false }),
616+
stringRequired: types.string({ required: true }),
617+
},
618+
{
619+
defaults: { stringOptional: 'foo' },
620+
timestamps: true,
621+
validationAction: VALIDATION_ACTIONS.WARN,
622+
validationLevel: VALIDATION_LEVEL.MODERATE,
623+
}
624+
);
625+
626+
expect(value).toEqual({
627+
$defaults: { stringOptional: 'foo' },
628+
$validationAction: 'warn',
629+
$validationLevel: 'moderate',
630+
additionalProperties: false,
631+
properties: {
632+
__v: {
633+
type: 'number',
634+
},
635+
_id: {
636+
bsonType: 'objectId',
637+
},
638+
anyOptional: {
639+
bsonType: [
640+
'array',
641+
'binData',
642+
'bool',
643+
'date',
644+
'null',
645+
'number',
646+
'object',
647+
'objectId',
648+
'string',
649+
],
650+
},
651+
anyRequired: {
652+
bsonType: [
653+
'array',
654+
'binData',
655+
'bool',
656+
'date',
657+
'null',
658+
'number',
659+
'object',
660+
'objectId',
661+
'string',
662+
],
663+
},
664+
arrayOfObjects: {
665+
items: {
666+
additionalProperties: false,
667+
properties: {
668+
foo: {
669+
type: 'number',
670+
},
671+
},
672+
type: 'object',
673+
},
674+
type: 'array',
675+
},
676+
arrayOptional: {
677+
items: {
678+
type: 'number',
679+
},
680+
type: 'array',
681+
},
682+
arrayRequired: {
683+
items: {
684+
type: 'number',
685+
},
686+
type: 'array',
687+
},
688+
binaryOptional: {
689+
bsonType: 'binData',
690+
},
691+
binaryRequired: {
692+
bsonType: 'binData',
693+
},
694+
booleanOptional: {
695+
type: 'boolean',
696+
},
697+
booleanRequired: {
698+
type: 'boolean',
699+
},
700+
createdAt: {
701+
bsonType: 'date',
702+
},
703+
dateOptional: {
704+
bsonType: 'date',
705+
},
706+
dateRequired: {
707+
bsonType: 'date',
708+
},
709+
enumOptional: {
710+
enum: ['foo', 'bar', null],
711+
},
712+
enumRequired: {
713+
enum: ['foo', 'bar'],
714+
},
715+
numberOptional: {
716+
type: 'number',
717+
},
718+
numberRequired: {
719+
type: 'number',
720+
},
721+
objectGenericOptional: {
722+
additionalProperties: false,
723+
patternProperties: {
724+
'.+': {
725+
type: 'number',
726+
},
727+
},
728+
type: 'object',
729+
},
730+
objectGenericRequired: {
731+
additionalProperties: false,
732+
patternProperties: {
733+
'abc.+': {
734+
type: 'number',
735+
},
736+
},
737+
type: 'object',
738+
},
739+
740+
objectIdOptional: {
741+
bsonType: 'objectId',
742+
},
743+
objectIdRequired: {
744+
bsonType: 'objectId',
745+
},
746+
objectOptional: {
747+
additionalProperties: false,
748+
properties: {
749+
foo: {
750+
type: 'number',
751+
},
752+
},
753+
type: 'object',
754+
},
755+
objectRequired: {
756+
additionalProperties: false,
757+
properties: {
758+
foo: {
759+
type: 'number',
760+
},
761+
},
762+
type: 'object',
763+
},
764+
stringOptional: {
765+
type: 'string',
766+
},
767+
stringRequired: {
768+
type: 'string',
769+
},
770+
updatedAt: {
771+
bsonType: 'date',
772+
},
773+
},
774+
required: [
775+
'_id',
776+
'anyRequired',
777+
'arrayRequired',
778+
'binaryRequired',
779+
'booleanRequired',
780+
'dateRequired',
781+
'enumRequired',
782+
'numberRequired',
783+
'objectGenericRequired',
784+
'objectIdRequired',
785+
'objectRequired',
786+
'stringRequired',
787+
'createdAt',
788+
'updatedAt',
789+
],
790+
type: 'object',
791+
});
792+
793+
/* eslint-disable */
794+
expectType<{
795+
_id: ObjectId;
796+
anyOptional?: any;
797+
// This `any` can not be required in TS
798+
anyRequired?: any;
799+
arrayOptional?: (number | undefined)[];
800+
arrayRequired: (number | undefined)[];
801+
arrayOfObjects?: {
802+
foo?: number;
803+
}[];
804+
binaryOptional?: Binary;
805+
binaryRequired: Binary;
806+
booleanOptional?: boolean;
807+
booleanRequired: boolean;
808+
dateOptional?: Date;
809+
dateRequired: Date;
810+
enumOptional?: TEST_ENUM | null;
811+
enumRequired: TEST_ENUM;
812+
numberOptional?: number;
813+
numberRequired: number;
814+
objectGenericOptional?: { [key: string]: number | undefined };
815+
objectGenericRequired: { [key: string]: number | undefined };
816+
objectIdOptional?: ObjectId;
817+
objectIdRequired: ObjectId;
818+
objectOptional?: { foo?: number };
819+
objectRequired: { foo?: number };
820+
stringOptional?: string;
821+
stringRequired: string;
822+
createdAt: Date;
823+
updatedAt: Date;
824+
}>(value[0]);
825+
/* eslint-enable */
826+
expectType<{
827+
stringOptional: string;
828+
}>(value[1]);
829+
expectType<ObjectId>(value[0]?._id);
830+
});
525831
});

src/types.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,11 @@ type BSONType =
4949
| 'objectId'
5050
| 'string';
5151

52-
type GetType<Type, Options> = Options extends RequiredOptions ? Type : Type | undefined;
52+
type GetType<Type, Options> = Options extends RequiredOptions
53+
? Options['required'] extends true
54+
? Type
55+
: Type | undefined
56+
: Type | undefined;
5357

5458
type RequiredProperties<Properties> = {
5559
[Prop in keyof Properties]: undefined extends Properties[Prop] ? never : Prop;
@@ -133,7 +137,7 @@ function enumType<Enum, Options extends GenericOptions>(
133137
options?: Options
134138
): GetType<Enum, Options> {
135139
return {
136-
...(options && 'required' in options ? { $required: true } : {}),
140+
...(options?.required ? { $required: true } : {}),
137141
enum: values,
138142
} as unknown as GetType<Enum, Options>;
139143
}
@@ -191,7 +195,7 @@ export function objectGeneric<Property, Options extends ObjectOptions>(
191195
function createSimpleType<Type>(type: BSONType) {
192196
return <Options extends GenericOptions>(options?: Options) => {
193197
return {
194-
...(options && 'required' in options ? { $required: true } : {}),
198+
...(options?.required ? { $required: true } : {}),
195199
...(type === 'date' || type === 'objectId' || type === 'binData'
196200
? { bsonType: type }
197201
: { type }),

0 commit comments

Comments
 (0)