Skip to content

Commit e78f1ca

Browse files
authored
feat: null type (plexinc#337)
1 parent 686f2eb commit e78f1ca

File tree

4 files changed

+134
-0
lines changed

4 files changed

+134
-0
lines changed

docs/api/types.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,38 @@ schema({
223223
});
224224
```
225225

226+
## `null`
227+
228+
Creates a `null` type. Use discouraged. Typically used in conjunction with
229+
another type when applying a schema to a collection that already contains
230+
`null` values in a field.
231+
232+
Usage of `null` as a value in Mongo is discouraged, as it makes some
233+
common query patterns ambiguous: `find({ myField: null })` will match
234+
documents that have the `myField` value set to the literal `null` _or_
235+
that match `{ myField: { $exists: false } }`.
236+
237+
To match documents with a literal `null` value you must query with
238+
`{ myField: { $type: 10 } }` (where `10` is the [BSON null type
239+
constant](https://www.mongodb.com/docs/manual/reference/bson-types/))
240+
241+
**Parameters:**
242+
243+
| Name | Type | Attribute |
244+
| ------------------ | ---------------- | --------- |
245+
| `options` | `GenericOptions` | optional |
246+
| `options.required` | `boolean` | optional |
247+
248+
**Example:**
249+
250+
```ts
251+
import { schema, types } from 'papr';
252+
253+
schema({
254+
nullableNumber: types.oneOf([types.number(), types.null()]),
255+
});
256+
```
257+
226258
## `number`
227259

228260
Creates a number type.

src/__tests__/schema.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,10 @@ describe('schema', () => {
466466
decimalRequired: types.decimal({ required: true }),
467467
enumOptional: types.enum([...Object.values(TEST_ENUM), null]),
468468
enumRequired: types.enum(Object.values(TEST_ENUM), { required: true }),
469+
nullOptional: types.null(),
470+
nullRequired: types.null({ required: true }),
471+
nullableOneOfOptional: types.oneOf([types.number(), types.null()]),
472+
nullableOneOfRequired: types.oneOf([types.number(), types.null()], { required: true }),
469473
numberOptional: types.number(),
470474
numberRequired: types.number({ required: true }),
471475
objectGenericOptional: types.objectGeneric(types.number()),
@@ -595,6 +599,18 @@ describe('schema', () => {
595599
enumRequired: {
596600
enum: ['foo', 'bar'],
597601
},
602+
nullOptional: {
603+
type: 'null',
604+
},
605+
nullRequired: {
606+
type: 'null',
607+
},
608+
nullableOneOfOptional: {
609+
oneOf: [{ type: 'number' }, { type: 'null' }],
610+
},
611+
nullableOneOfRequired: {
612+
oneOf: [{ type: 'number' }, { type: 'null' }],
613+
},
598614
numberOptional: {
599615
type: 'number',
600616
},
@@ -684,6 +700,8 @@ describe('schema', () => {
684700
'dateRequired',
685701
'decimalRequired',
686702
'enumRequired',
703+
'nullRequired',
704+
'nullableOneOfRequired',
687705
'numberRequired',
688706
'objectGenericRequired',
689707
'objectIdRequired',
@@ -719,6 +737,10 @@ describe('schema', () => {
719737
decimalRequired: Decimal128;
720738
enumOptional?: TEST_ENUM | null;
721739
enumRequired: TEST_ENUM;
740+
nullOptional?: null;
741+
nullRequired: null;
742+
nullableOneOfOptional?: null | number;
743+
nullableOneOfRequired?: null | number;
722744
numberOptional?: number;
723745
numberRequired: number;
724746
objectGenericOptional?: { [key: string]: number | undefined };
@@ -815,6 +837,10 @@ describe('schema', () => {
815837
dateRequired: types.date({ required: true }),
816838
enumOptional: types.enum([...Object.values(TEST_ENUM), null]),
817839
enumRequired: types.enum(Object.values(TEST_ENUM), { required: true }),
840+
nullOptional: types.null({ required: false }),
841+
nullRequired: types.null({ required: true }),
842+
nullableOneOfOptional: types.oneOf([types.number(), types.null()], { required: false }),
843+
nullableOneOfRequired: types.oneOf([types.number(), types.null()], { required: true }),
818844
numberOptional: types.number({ required: false }),
819845
numberRequired: types.number({ required: true }),
820846
objectGenericOptional: types.objectGeneric(types.number({ required: false })),
@@ -938,6 +964,18 @@ describe('schema', () => {
938964
enumRequired: {
939965
enum: ['foo', 'bar'],
940966
},
967+
nullOptional: {
968+
type: 'null',
969+
},
970+
nullRequired: {
971+
type: 'null',
972+
},
973+
nullableOneOfOptional: {
974+
oneOf: [{ type: 'number' }, { type: 'null' }],
975+
},
976+
nullableOneOfRequired: {
977+
oneOf: [{ type: 'number' }, { type: 'null' }],
978+
},
941979
numberOptional: {
942980
type: 'number',
943981
},
@@ -1026,6 +1064,8 @@ describe('schema', () => {
10261064
'constantRequired',
10271065
'dateRequired',
10281066
'enumRequired',
1067+
'nullRequired',
1068+
'nullableOneOfRequired',
10291069
'numberRequired',
10301070
'objectGenericRequired',
10311071
'objectIdRequired',
@@ -1059,6 +1099,10 @@ describe('schema', () => {
10591099
dateRequired: Date;
10601100
enumOptional?: TEST_ENUM | null;
10611101
enumRequired: TEST_ENUM;
1102+
nullOptional?: null;
1103+
nullRequired: null;
1104+
nullableOneOfOptional?: null | number;
1105+
nullableOneOfRequired: null | number;
10621106
numberOptional?: number;
10631107
numberRequired: number;
10641108
objectGenericOptional?: { [key: string]: number | undefined };

src/__tests__/types.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,37 @@ describe('types', () => {
101101
});
102102
});
103103

104+
describe('null', () => {
105+
test('default', () => {
106+
const value = types.null();
107+
108+
expect(value).toEqual({
109+
type: 'null',
110+
});
111+
expectType<null | undefined>(value);
112+
expectType<typeof value>(undefined);
113+
});
114+
115+
test('required', () => {
116+
const value = types.null({ required: true });
117+
118+
expect(value).toEqual({
119+
$required: true,
120+
type: 'null',
121+
});
122+
expectType<null>(value);
123+
// @ts-expect-error `value` should not be undefined
124+
expectType<typeof value>(undefined);
125+
});
126+
127+
test('options', () => {
128+
types.null({ required: true });
129+
130+
// @ts-expect-error invalid option
131+
types.number({ maxLength: 1 });
132+
});
133+
});
134+
104135
describe('number', () => {
105136
test('default', () => {
106137
const value = types.number();

src/types.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ type BSONType =
4747
| 'boolean'
4848
| 'date'
4949
| 'decimal'
50+
| 'null'
5051
| 'number'
5152
| 'object'
5253
| 'objectId'
@@ -432,6 +433,32 @@ export default {
432433
*/
433434
enum: enumType,
434435

436+
/**
437+
* Creates a `null` type. Use discouraged. Typically used in conjunction with
438+
* another type when applying a schema to a collection that already contains
439+
* `null` values in a field.
440+
*
441+
* Usage of `null` as a value in Mongo is discouraged, as it makes some
442+
* common query patterns ambiguous: `find({ myField: null })` will match
443+
* documents that have the `myField` value set to the literal `null` _or_
444+
* that match `{ myField: { $exists: false } }`.
445+
*
446+
* To match documents with a literal `null` value you must query with
447+
* `{ myField: { $type: 10 } }` (where `10` is the [BSON null type
448+
* constant](https://www.mongodb.com/docs/manual/reference/bson-types/))
449+
*
450+
* @param [options] {GenericOptions}
451+
* @param [options.required] {boolean}
452+
*
453+
* @example
454+
* import { schema, types } from 'papr';
455+
*
456+
* schema({
457+
* nullableNumber: types.oneOf([ types.number(), types.null() ]),
458+
* });
459+
*/
460+
null: createSimpleType<null>('null'),
461+
435462
/**
436463
* Creates a number type.
437464
*

0 commit comments

Comments
 (0)