Skip to content

Commit b0e17e8

Browse files
committed
feat: support dot-notation attributes in Filter
1 parent 30f2a2d commit b0e17e8

File tree

2 files changed

+86
-11
lines changed

2 files changed

+86
-11
lines changed

src/mongo_types.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export type WithoutId<TSchema> = Omit<TSchema, '_id'>;
5757

5858
/** A MongoDB filter can be some portion of the schema or a set of operators @public */
5959
export type Filter<TSchema> = {
60-
[P in keyof WithId<TSchema>]?: Condition<WithId<TSchema>[P]>;
60+
[P in Join<NestedPaths<WithId<TSchema>>, '.'>]?: Condition<PropertyType<WithId<TSchema>, P>>;
6161
} & RootFilterOperators<WithId<TSchema>>;
6262

6363
/** @public */
@@ -423,3 +423,48 @@ export class TypedEventEmitter<Events extends EventsDescription> extends EventEm
423423

424424
/** @public */
425425
export class CancellationToken extends TypedEventEmitter<{ cancel(): void }> {}
426+
427+
/**
428+
* Helper types for dot-notation filter attributes
429+
*/
430+
431+
/** @public */
432+
export type Join<T extends unknown[], D extends string> = T extends []
433+
? ''
434+
: T extends [string | number]
435+
? `${T[0]}`
436+
: T extends [string | number, ...infer R]
437+
? `${T[0]}${D}${Join<R, D>}`
438+
: string | number;
439+
440+
/** @public */
441+
export type PropertyType<Type, Property extends string> = string extends Property
442+
? unknown
443+
: Property extends keyof Type
444+
? Type[Property]
445+
: Property extends `${infer Key}.${infer Rest}`
446+
? Key extends `${number}`
447+
? Type extends Array<infer ArrayType>
448+
? PropertyType<ArrayType, Rest>
449+
: Type extends ReadonlyArray<infer ArrayType>
450+
? PropertyType<ArrayType, Rest>
451+
: unknown
452+
: Key extends keyof Type
453+
? PropertyType<Type[Key], Rest>
454+
: unknown
455+
: unknown;
456+
457+
// We dont't support nested circular references
458+
/** @public */
459+
export type NestedPaths<Type> = Type extends string | number | boolean | Date | ObjectId
460+
? []
461+
: Type extends Array<infer ArrayType>
462+
? [number, ...NestedPaths<ArrayType>]
463+
: Type extends ReadonlyArray<infer ArrayType>
464+
? [number, ...NestedPaths<ArrayType>]
465+
: // eslint-disable-next-line @typescript-eslint/ban-types
466+
Type extends object
467+
? {
468+
[Key in Extract<keyof Type, string>]: [Key, ...NestedPaths<Type[Key]>];
469+
}[Extract<keyof Type, string>]
470+
: [];

test/types/community/collection/filterQuery.test-d.ts

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ const db = client.db('test');
1616
* Test the generic Filter using collection.find<T>() method
1717
*/
1818

19+
interface HumanModel {
20+
_id: ObjectId;
21+
name: string;
22+
}
23+
1924
// a collection model for all possible MongoDB BSON types and TypeScript types
2025
interface PetModel {
2126
_id: ObjectId; // ObjectId field
@@ -24,14 +29,28 @@ interface PetModel {
2429
age: number; // number field
2530
type: 'dog' | 'cat' | 'fish'; // union field
2631
isCute: boolean; // boolean field
27-
bestFriend?: PetModel; // object field (Embedded/Nested Documents)
32+
bestFriend?: HumanModel; // object field (Embedded/Nested Documents)
2833
createdAt: Date; // date field
2934
treats: string[]; // array of string
3035
playTimePercent: Decimal128; // bson Decimal128 type
31-
readonly friends?: ReadonlyArray<PetModel>; // readonly array of objects
32-
playmates?: PetModel[]; // writable array of objects
36+
readonly friends?: ReadonlyArray<HumanModel>; // readonly array of objects
37+
playmates?: HumanModel[]; // writable array of objects
38+
// Object with multiple nested levels
39+
meta?: {
40+
updatedAt?: Date;
41+
deep?: {
42+
nested?: {
43+
level?: number;
44+
};
45+
};
46+
};
3347
}
3448

49+
const john = {
50+
_id: new ObjectId('577fa2d90c4cc47e31cf4b6a'),
51+
name: 'John'
52+
};
53+
3554
const spot = {
3655
_id: new ObjectId('577fa2d90c4cc47e31cf4b6f'),
3756
name: 'Spot',
@@ -83,14 +102,29 @@ expectNotType<Filter<PetModel>>({ age: [23, 43] });
83102

84103
/// it should query __nested document__ fields only by exact match
85104
// TODO: we currently cannot enforce field order but field order is important for mongo
86-
await collectionT.find({ bestFriend: spot }).toArray();
105+
await collectionT.find({ bestFriend: john }).toArray();
87106
/// nested documents query should contain all required fields
88-
expectNotType<Filter<PetModel>>({ bestFriend: { family: 'Andersons' } });
107+
expectNotType<Filter<PetModel>>({ bestFriend: { name: 'Andersons' } });
89108
/// it should not accept wrong types for nested document fields
90109
expectNotType<Filter<PetModel>>({ bestFriend: 21 });
91110
expectNotType<Filter<PetModel>>({ bestFriend: 'Andersons' });
92111
expectNotType<Filter<PetModel>>({ bestFriend: [spot] });
93-
expectNotType<Filter<PetModel>>({ bestFriend: [{ family: 'Andersons' }] });
112+
expectNotType<Filter<PetModel>>({ bestFriend: [{ name: 'Andersons' }] });
113+
114+
/// it should query __nested document__ fields using dot-notation
115+
collectionT.find({ 'meta.updatedAt': new Date() });
116+
collectionT.find({ 'meta.deep.nested.level': 123 });
117+
collectionT.find({ 'friends.0.name': 'John' });
118+
collectionT.find({ 'playmates.0.name': 'John' });
119+
/// it should not accept wrong types for nested document fields
120+
expectNotType<Filter<PetModel>>({ 'meta.updatedAt': 123 });
121+
expectNotType<Filter<PetModel>>({ 'meta.updatedAt': true });
122+
expectNotType<Filter<PetModel>>({ 'meta.updatedAt': 'now' });
123+
expectNotType<Filter<PetModel>>({ 'meta.deep.nested.level': '123' });
124+
expectNotType<Filter<PetModel>>({ 'meta.deep.nested.level': true });
125+
expectNotType<Filter<PetModel>>({ 'meta.deep.nested.level': new Date() });
126+
expectNotType<Filter<PetModel>>({ 'friends.0.name': 123 });
127+
expectNotType<Filter<PetModel>>({ 'playmates.0.name': 123 });
94128

95129
/// it should query __array__ fields by exact match
96130
await collectionT.find({ treats: ['kibble', 'bone'] }).toArray();
@@ -232,7 +266,3 @@ await collectionT.find({ playmates: { $elemMatch: { name: 'MrMeow' } } }).toArra
232266
expectNotType<Filter<PetModel>>({ name: { $all: ['world', 'world'] } });
233267
expectNotType<Filter<PetModel>>({ age: { $elemMatch: [1, 2] } });
234268
expectNotType<Filter<PetModel>>({ type: { $size: 2 } });
235-
236-
// dot key case that shows it is assignable even when the referenced key is the wrong type
237-
expectAssignable<Filter<PetModel>>({ 'bestFriend.name': 23 }); // using dot notation permits any type for the key
238-
expectNotType<Filter<PetModel>>({ bestFriend: { name: 23 } });

0 commit comments

Comments
 (0)