Skip to content

Commit fbcb04e

Browse files
committed
feat: Support readonly data structures for Filter and Update
* Break many expectations out to individual tests
1 parent d1b2574 commit fbcb04e

File tree

5 files changed

+351
-108
lines changed

5 files changed

+351
-108
lines changed

src/__tests__/model.test.ts

+15
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ describe('model', () => {
2323
bar: Types.number({ required: true }),
2424
foo: Types.string({ required: true }),
2525
ham: Types.date(),
26+
list: Types.array(Types.number()),
2627
nested: Types.object({
2728
direct: Types.string({ required: true }),
2829
other: Types.number(),
@@ -2466,6 +2467,20 @@ describe('model', () => {
24662467
);
24672468
});
24682469

2470+
test('simple schema, readonly array', async () => {
2471+
const filter = { foo: 'foo' } as const;
2472+
const update = { $set: { list: [456] } } as const;
2473+
await simpleModel.updateOne(filter, update);
2474+
2475+
expect(collection.updateOne).toHaveBeenCalledWith(
2476+
{ foo: 'foo' },
2477+
{
2478+
$set: { list: [456] },
2479+
},
2480+
{ ignoreUndefined: true }
2481+
);
2482+
});
2483+
24692484
test.todo('simple schema, uses defaults');
24702485

24712486
test('timestamps schema', async () => {

src/__tests__/mongodbTypes.test.ts

+244-83
Original file line numberDiff line numberDiff line change
@@ -68,44 +68,119 @@ describe('mongodb types', () => {
6868

6969
describe('PaprFilter', () => {
7070
describe('existing top-level keys', () => {
71-
test('valid types', () => {
72-
expect(true).toBeTruthy();
71+
describe('valid types', () => {
72+
test('ObjectId', () => {
73+
const filter = { _id: new ObjectId() } as const;
74+
expectType<PaprFilter<TestDocument>>(filter);
75+
});
7376

74-
expectType<PaprFilter<TestDocument>>({ _id: new ObjectId() });
77+
test('string', () => {
78+
const filter = { foo: 'foo' } as const;
79+
expectType<PaprFilter<TestDocument>>(filter);
80+
});
7581

76-
expectType<PaprFilter<TestDocument>>({ foo: 'foo' });
77-
// string fields can be queried by regexp
78-
expectType<PaprFilter<TestDocument>>({ foo: /foo/ });
82+
test('string queried by regexp', () => {
83+
const filter = { foo: /foo/ } as const;
84+
// string fields can be queried by regexp
85+
expectType<PaprFilter<TestDocument>>(filter);
86+
});
7987

80-
expectType<PaprFilter<TestDocument>>({ bar: 123 });
88+
test('number', () => {
89+
const filter = { bar: 123 } as const;
90+
expectType<PaprFilter<TestDocument>>(filter);
91+
});
8192

82-
expectType<PaprFilter<TestDocument>>({ ham: new Date() });
93+
test('Date', () => {
94+
const filter = { ham: new Date() } as const;
95+
expectType<PaprFilter<TestDocument>>(filter);
96+
});
8397

84-
// array fields can be queried by exact match
85-
expectType<PaprFilter<TestDocument>>({ tags: ['foo'] });
86-
// array fields can be queried by element type
87-
expectType<PaprFilter<TestDocument>>({ tags: 'foo' });
88-
expectType<PaprFilter<TestDocument>>({ tags: /foo/ });
98+
test('array-of-strings queried by exact match', () => {
99+
// array fields can be queried by exact match
100+
const filter = { tags: ['foo'] } as const;
101+
expectType<PaprFilter<TestDocument>>(filter);
102+
});
89103

90-
expectType<PaprFilter<TestDocument>>({
91-
nestedObject: {
92-
deep: { deeper: 'foo' },
93-
direct: true,
94-
},
104+
test('array-of-strings queried by string', () => {
105+
// array fields can be queried by element type
106+
const filter = { tags: 'foo' } as const;
107+
expectType<PaprFilter<TestDocument>>(filter);
108+
});
109+
110+
test('array-of-strings queried by regexp', () => {
111+
const filter = { tags: /foo/ } as const;
112+
expectType<PaprFilter<TestDocument>>(filter);
95113
});
96114

97-
// all BSON types can be used as query values
98-
expectType<PaprFilter<TestDocument>>({ binary: new Binary([], 2) });
99-
expectType<PaprFilter<TestDocument>>({ bsonSymbol: new BSONSymbol('hi') });
100-
expectType<PaprFilter<TestDocument>>({ code: new Code(() => true) });
101-
expectType<PaprFilter<TestDocument>>({ double: new Double(123.45) });
102-
expectType<PaprFilter<TestDocument>>({ dbRef: new DBRef('collection', new ObjectId()) });
103-
expectType<PaprFilter<TestDocument>>({ decimal: new Decimal128('123.45') });
104-
expectType<PaprFilter<TestDocument>>({ int32: new Int32('123') });
105-
expectType<PaprFilter<TestDocument>>({ long: new Long('123', 45) });
106-
expectType<PaprFilter<TestDocument>>({ maxKey: new MaxKey() });
107-
expectType<PaprFilter<TestDocument>>({ minKey: new MinKey() });
108-
expectType<PaprFilter<TestDocument>>({ regexp: /foo/ });
115+
test('nested object', () => {
116+
const filter = {
117+
nestedObject: {
118+
deep: { deeper: 'foo' },
119+
direct: true,
120+
},
121+
} as const;
122+
expectType<PaprFilter<TestDocument>>(filter);
123+
});
124+
125+
describe('BSON types', () => {
126+
// all BSON types can be used as query values
127+
test('binary', () => {
128+
const filter = { binary: new Binary([], 2) } as const;
129+
expectType<PaprFilter<TestDocument>>(filter);
130+
});
131+
132+
test('BSONSymbol', () => {
133+
const filter = { bsonSymbol: new BSONSymbol('hi') } as const;
134+
expectType<PaprFilter<TestDocument>>(filter);
135+
});
136+
137+
test('Code', () => {
138+
const filter = { code: new Code(() => true) } as const;
139+
expectType<PaprFilter<TestDocument>>(filter);
140+
});
141+
142+
test('Double', () => {
143+
const filter = { double: new Double(123.45) } as const;
144+
expectType<PaprFilter<TestDocument>>(filter);
145+
});
146+
147+
test('DBRef', () => {
148+
const filter = {
149+
dbRef: new DBRef('collection', new ObjectId()),
150+
} as const;
151+
expectType<PaprFilter<TestDocument>>(filter);
152+
});
153+
154+
test('Decimal128', () => {
155+
const filter = { decimal: new Decimal128('123.45') } as const;
156+
expectType<PaprFilter<TestDocument>>(filter);
157+
});
158+
159+
test('Int32', () => {
160+
const filter = { int32: new Int32('123') } as const;
161+
expectType<PaprFilter<TestDocument>>(filter);
162+
});
163+
164+
test('Long', () => {
165+
const filter = { long: new Long('123', 45) } as const;
166+
expectType<PaprFilter<TestDocument>>(filter);
167+
});
168+
169+
test('MaxKey', () => {
170+
const filter = { maxKey: new MaxKey() } as const;
171+
expectType<PaprFilter<TestDocument>>(filter);
172+
});
173+
174+
test('MinKey', () => {
175+
const filter = { minKey: new MinKey() } as const;
176+
expectType<PaprFilter<TestDocument>>(filter);
177+
});
178+
179+
test('regexp', () => {
180+
const filter = { regexp: /foo/ } as const;
181+
expectType<PaprFilter<TestDocument>>(filter);
182+
});
183+
});
109184
});
110185

111186
test('invalid types', () => {
@@ -148,68 +223,143 @@ describe('mongodb types', () => {
148223
});
149224

150225
describe('existing nested keys using dot notation', () => {
151-
test('valid types', () => {
152-
// https://www.mongodb.com/docs/manual/tutorial/query-embedded-documents/#query-on-nested-field
153-
expectType<PaprFilter<TestDocument>>({ 'nestedObject.direct': true });
154-
expectType<PaprFilter<TestDocument>>({ 'nestedObject.other': 123 });
155-
expectType<PaprFilter<TestDocument>>({ 'nestedObject.deep.deeper': 'foo' });
156-
expectType<PaprFilter<TestDocument>>({ 'nestedObject.deep.other': 123 });
157-
expectType<PaprFilter<TestDocument>>({
158-
'nestedObject.level2.level3.level4.level5.level6ID': 'foo',
226+
describe('valid types', () => {
227+
describe('nested object', () => {
228+
test('boolean addressed by name', () => {
229+
const filter = { 'nestedObject.direct': true } as const;
230+
// https://www.mongodb.com/docs/manual/tutorial/query-embedded-documents/#query-on-nested-field
231+
expectType<PaprFilter<TestDocument>>(filter);
232+
});
233+
234+
test('number addressed by name', () => {
235+
expectType<PaprFilter<TestDocument>>({ 'nestedObject.other': 123 });
236+
});
237+
238+
test('string addressed by nested name', () => {
239+
expectType<PaprFilter<TestDocument>>({ 'nestedObject.deep.deeper': 'foo' });
240+
});
241+
242+
test('number addressed by nested name', () => {
243+
expectType<PaprFilter<TestDocument>>({ 'nestedObject.deep.other': 123 });
244+
});
245+
246+
test('string addressed by deeply-nested name', () => {
247+
expectType<PaprFilter<TestDocument>>({
248+
'nestedObject.level2.level3.level4.level5.level6ID': 'foo',
249+
});
250+
});
251+
});
252+
253+
describe('generic object', () => {
254+
test('number addressed by arbitrary nested name', () => {
255+
expectType<PaprFilter<TestDocument>>({ 'genericObject.foo.id': 123 });
256+
expectType<PaprFilter<TestDocument>>({ 'genericObject.bar.id': 123 });
257+
});
258+
259+
test('number addressed by nested object with valid property', () => {
260+
expectType<PaprFilter<TestDocument>>({ 'genericObject.foo': { id: 123 } });
261+
});
262+
});
263+
264+
test('string addressed by index + property in array-of-objects', () => {
265+
// https://www.mongodb.com/docs/manual/tutorial/query-array-of-documents/#use-the-array-index-to-query-for-a-field-in-the-embedded-document
266+
expectType<PaprFilter<TestDocument>>({ 'list.0.direct': 'foo' });
159267
});
160268

161-
expectType<PaprFilter<TestDocument>>({ 'genericObject.foo.id': 123 });
162-
expectType<PaprFilter<TestDocument>>({ 'genericObject.bar.id': 123 });
269+
test('number addressed by index + property in array-of-objects', () => {
270+
expectType<PaprFilter<TestDocument>>({ 'list.1.other': 123 });
271+
});
163272

164-
expectType<PaprFilter<TestDocument>>({ 'genericObject.foo': { id: 123 } });
273+
test('number addressed by large index + property in array-of-objects', () => {
274+
// it works with some extreme indexes
275+
expectType<PaprFilter<TestDocument>>({ 'list.4294967295.other': 123 });
276+
});
165277

166-
// https://www.mongodb.com/docs/manual/tutorial/query-array-of-documents/#use-the-array-index-to-query-for-a-field-in-the-embedded-document
167-
expectType<PaprFilter<TestDocument>>({ 'list.0.direct': 'foo' });
168-
expectType<PaprFilter<TestDocument>>({ 'list.1.other': 123 });
169-
// it works with some extreme indexes
170-
expectType<PaprFilter<TestDocument>>({ 'list.4294967295.other': 123 });
171-
expectType<PaprFilter<TestDocument>>({ 'list.9999999999999999999.other': 123 });
278+
test('number addressed by super-large index + property in array-of-objects', () => {
279+
expectType<PaprFilter<TestDocument>>({ 'list.9999999999999999999.other': 123 });
280+
});
172281

173-
expectType<PaprFilter<TestDocument>>({ 'tags.0': 'foo' });
282+
test('string addressed by property in array-of-objects', () => {
283+
// https://www.mongodb.com/docs/manual/tutorial/query-array-of-documents/#specify-a-query-condition-on-a-field-embedded-in-an-array-of-documents
284+
expectType<PaprFilter<TestDocument>>({ 'list.direct': 'foo' });
285+
});
174286

175-
// https://www.mongodb.com/docs/manual/tutorial/query-array-of-documents/#specify-a-query-condition-on-a-field-embedded-in-an-array-of-documents
176-
expectType<PaprFilter<TestDocument>>({ 'list.direct': 'foo' });
177-
expectType<PaprFilter<TestDocument>>({ 'list.other': 123 });
178-
});
287+
test('number addressed by property in array-of-objects', () => {
288+
expectType<PaprFilter<TestDocument>>({ 'list.other': 123 });
289+
});
179290

180-
test('invalid types', () => {
181-
// @ts-expect-error Type mismatch
182-
expectType<PaprFilter<TestDocument>>({ 'tags.0': 123 });
291+
test('string addressed by index in array-of-strings', () => {
292+
expectType<PaprFilter<TestDocument>>({ 'tags.0': 'foo' });
293+
});
294+
});
183295

184-
// @ts-expect-error Type mismatch
185-
expectType<PaprFilter<TestDocument>>({ 'nestedObject.direct': 'foo' });
186-
// @ts-expect-error Type mismatch
187-
expectType<PaprFilter<TestDocument>>({ 'nestedObject.other': 'foo' });
188-
// @ts-expect-error Type mismatch
189-
expectType<PaprFilter<TestDocument>>({ 'nestedObject.deep.deeper': 123 });
190-
// @ts-expect-error Type mismatch
191-
expectType<PaprFilter<TestDocument>>({ 'nestedObject.deep.other': 'foo' });
192-
expectType<PaprFilter<TestDocument>>({
296+
describe('invalid types', () => {
297+
test('number substituted for string at array numeric index', () => {
193298
// @ts-expect-error Type mismatch
194-
'nestedObject.level2.level3.level4.level5.level6': 123,
299+
expectType<PaprFilter<TestDocument>>({ 'tags.0': 123 });
195300
});
196-
expectType<PaprFilter<TestDocument>>({
197-
// @ts-expect-error Nesting level too deep
198-
'nestedObject.level2.level3.level4.level5.level6.level7ID': 'foo',
301+
302+
describe('nested objects', () => {
303+
test('string substituted for object at nested object property', () => {
304+
// @ts-expect-error Type mismatch
305+
expectType<PaprFilter<TestDocument>>({ 'nestedObject.direct': 'foo' });
306+
});
307+
308+
test('string substituted for number at shallow nested object property', () => {
309+
// @ts-expect-error Type mismatch
310+
expectType<PaprFilter<TestDocument>>({ 'nestedObject.other': 'foo' });
311+
});
312+
313+
test('number substituted for string at nested object property', () => {
314+
// @ts-expect-error Type mismatch
315+
expectType<PaprFilter<TestDocument>>({ 'nestedObject.deep.deeper': 123 });
316+
});
317+
318+
test('string substituted for number at deep nested object property', () => {
319+
// @ts-expect-error Type mismatch
320+
expectType<PaprFilter<TestDocument>>({ 'nestedObject.deep.other': 'foo' });
321+
});
322+
323+
test('number substituted for object at deeply nested property (level6)', () => {
324+
const filter = {
325+
'nestedObject.level2.level3.level4.level5.level6': 123,
326+
} as const;
327+
// @ts-expect-error Type mismatch
328+
expectType<PaprFilter<TestDocument>>(filter);
329+
});
330+
331+
test('number substituted for string at deeply nested property (level7) nesting error, not type error', () => {
332+
const filter = {
333+
'nestedObject.level2.level3.level4.level5.level6.level7ID': 123,
334+
} as const;
335+
expectType<PaprFilter<TestDocument>>(filter);
336+
});
199337
});
200338

201-
// @ts-expect-error Type mismatch
202-
expectType<PaprFilter<TestDocument>>({ 'genericObject.foo.id': 'foo' });
203-
// @ts-expect-error Type mismatch
204-
expectType<PaprFilter<TestDocument>>({ 'genericObject.bar.id': true });
339+
describe('generic objects', () => {
340+
test('string substituted for number on generic object field', () => {
341+
// @ts-expect-error Type mismatch
342+
expectType<PaprFilter<TestDocument>>({ 'genericObject.bar.id': '123' });
343+
});
205344

206-
// Support for this type-check is not available yet
207-
// expectType<PaprFilter<TestDocument>>({ 'genericObject.foo': { id: 'foo' } });
345+
test('boolean substituted for number on generic object field', () => {
346+
// @ts-expect-error Type mismatch
347+
expectType<PaprFilter<TestDocument>>({ 'genericObject.bar.id': true });
348+
});
349+
});
208350

209-
// @ts-expect-error Type mismatch
210-
expectType<PaprFilter<TestDocument>>({ 'list.0.direct': 123 });
211-
// @ts-expect-error Type mismatch
212-
expectType<PaprFilter<TestDocument>>({ 'list.1.other': 'foo' });
351+
test('number substituted for required string on array-of-objects', () => {
352+
// Support for this type-check is not available yet
353+
// expectType<PaprFilter<TestDocument>>({ 'genericObject.foo': { id: 'foo' } });
354+
355+
// @ts-expect-error Type mismatch
356+
expectType<PaprFilter<TestDocument>>({ 'list.0.direct': 123 });
357+
});
358+
359+
test('string substituted for optional number on array-of-objects', () => {
360+
// @ts-expect-error Type mismatch
361+
expectType<PaprFilter<TestDocument>>({ 'list.1.other': 'foo' });
362+
});
213363
});
214364
});
215365

@@ -429,11 +579,22 @@ describe('mongodb types', () => {
429579

430580
describe('PaprUpdateFilter', () => {
431581
describe('$currentDate', () => {
432-
test('valid types', () => {
433-
expectType<PaprUpdateFilter<TestDocument>>({ $currentDate: { ham: true } });
434-
expectType<PaprUpdateFilter<TestDocument>>({ $currentDate: { ham: { $type: 'date' } } });
435-
expectType<PaprUpdateFilter<TestDocument>>({
436-
$currentDate: { ham: { $type: 'timestamp' } },
582+
describe('valid types', () => {
583+
test('object', () => {
584+
const filter = { $currentDate: { ham: true } } as const;
585+
expectType<PaprUpdateFilter<TestDocument>>(filter);
586+
});
587+
588+
test('object with $type date', () => {
589+
const filter = { $currentDate: { ham: { $type: 'date' } } } as const;
590+
expectType<PaprUpdateFilter<TestDocument>>(filter);
591+
});
592+
593+
test('object with $type timestamp', () => {
594+
const filter = {
595+
$currentDate: { ham: { $type: 'timestamp' } },
596+
} as const;
597+
expectType<PaprUpdateFilter<TestDocument>>(filter);
437598
});
438599
});
439600

0 commit comments

Comments
 (0)