Skip to content

Commit f0a3e4f

Browse files
committed
feat: support dot-notation attributes in Filter
1 parent fa2f8d0 commit f0a3e4f

File tree

3 files changed

+102
-17
lines changed

3 files changed

+102
-17
lines changed

src/index.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,9 @@ export type {
430430
KeysOfAType,
431431
KeysOfOtherType,
432432
IsAny,
433-
OneOrMore
433+
OneOrMore,
434+
Join,
435+
PropertyType,
436+
NestedPaths
434437
} from './mongo_types';
435438
export type { serialize, deserialize } from './bson';

src/mongo_types.ts

+58-6
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,9 @@ export type WithoutId<TSchema> = Omit<TSchema, '_id'>;
5656

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

6263
/** @public */
6364
export type Condition<T> = AlternativeType<T> | FilterOperators<AlternativeType<T>>;
@@ -256,7 +257,8 @@ export type SetFields<TSchema> = ({
256257
readonly [key in KeysOfAType<TSchema, ReadonlyArray<any> | undefined>]?:
257258
| OptionalId<Flatten<TSchema[key]>>
258259
| AddToSetOperators<Array<OptionalId<Flatten<TSchema[key]>>>>;
259-
} & NotAcceptedFields<TSchema, ReadonlyArray<any> | undefined>) & {
260+
} &
261+
NotAcceptedFields<TSchema, ReadonlyArray<any> | undefined>) & {
260262
readonly [key: string]: AddToSetOperators<any> | any;
261263
};
262264

@@ -265,7 +267,8 @@ export type PushOperator<TSchema> = ({
265267
readonly [key in KeysOfAType<TSchema, ReadonlyArray<any>>]?:
266268
| Flatten<TSchema[key]>
267269
| ArrayOperator<Array<Flatten<TSchema[key]>>>;
268-
} & NotAcceptedFields<TSchema, ReadonlyArray<any>>) & {
270+
} &
271+
NotAcceptedFields<TSchema, ReadonlyArray<any>>) & {
269272
readonly [key: string]: ArrayOperator<any> | any;
270273
};
271274

@@ -274,14 +277,16 @@ export type PullOperator<TSchema> = ({
274277
readonly [key in KeysOfAType<TSchema, ReadonlyArray<any>>]?:
275278
| Partial<Flatten<TSchema[key]>>
276279
| FilterOperations<Flatten<TSchema[key]>>;
277-
} & NotAcceptedFields<TSchema, ReadonlyArray<any>>) & {
280+
} &
281+
NotAcceptedFields<TSchema, ReadonlyArray<any>>) & {
278282
readonly [key: string]: FilterOperators<any> | any;
279283
};
280284

281285
/** @public */
282286
export type PullAllOperator<TSchema> = ({
283287
readonly [key in KeysOfAType<TSchema, ReadonlyArray<any>>]?: TSchema[key];
284-
} & NotAcceptedFields<TSchema, ReadonlyArray<any>>) & {
288+
} &
289+
NotAcceptedFields<TSchema, ReadonlyArray<any>>) & {
285290
readonly [key: string]: ReadonlyArray<any>;
286291
};
287292

@@ -422,3 +427,50 @@ export class TypedEventEmitter<Events extends EventsDescription> extends EventEm
422427

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

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

+40-10
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ const db = client.db('test');
1515
* Test the generic Filter using collection.find<T>() method
1616
*/
1717

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

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

7998
/// it should query __nested document__ fields only by exact match
8099
// TODO: we currently cannot enforce field order but field order is important for mongo
81-
await collectionT.find({ bestFriend: spot }).toArray();
100+
await collectionT.find({ bestFriend: john }).toArray();
82101
/// nested documents query should contain all required fields
83-
expectNotType<Filter<PetModel>>({ bestFriend: { family: 'Andersons' } });
102+
expectNotType<Filter<PetModel>>({ bestFriend: { name: 'Andersons' } });
84103
/// it should not accept wrong types for nested document fields
85104
expectNotType<Filter<PetModel>>({ bestFriend: 21 });
86105
expectNotType<Filter<PetModel>>({ bestFriend: 'Andersons' });
87106
expectNotType<Filter<PetModel>>({ bestFriend: [spot] });
88-
expectNotType<Filter<PetModel>>({ bestFriend: [{ family: 'Andersons' }] });
107+
expectNotType<Filter<PetModel>>({ bestFriend: [{ name: 'Andersons' }] });
108+
109+
/// it should query __nested document__ fields using dot-notation
110+
collectionT.find({ 'meta.updatedAt': new Date() });
111+
collectionT.find({ 'meta.deep.nested.level': 123 });
112+
collectionT.find({ 'friends.0.name': 'John' });
113+
collectionT.find({ 'playmates.0.name': 'John' });
114+
/// it should not accept wrong types for nested document fields
115+
expectNotType<Filter<PetModel>>({ 'meta.updatedAt': 123 });
116+
expectNotType<Filter<PetModel>>({ 'meta.updatedAt': true });
117+
expectNotType<Filter<PetModel>>({ 'meta.updatedAt': 'now' });
118+
expectNotType<Filter<PetModel>>({ 'meta.deep.nested.level': '123' });
119+
expectNotType<Filter<PetModel>>({ 'meta.deep.nested.level': true });
120+
expectNotType<Filter<PetModel>>({ 'meta.deep.nested.level': new Date() });
121+
expectNotType<Filter<PetModel>>({ 'friends.0.name': 123 });
122+
expectNotType<Filter<PetModel>>({ 'playmates.0.name': 123 });
89123

90124
/// it should query __array__ fields by exact match
91125
await collectionT.find({ treats: ['kibble', 'bone'] }).toArray();
@@ -227,7 +261,3 @@ await collectionT.find({ playmates: { $elemMatch: { name: 'MrMeow' } } }).toArra
227261
expectNotType<Filter<PetModel>>({ name: { $all: ['world', 'world'] } });
228262
expectNotType<Filter<PetModel>>({ age: { $elemMatch: [1, 2] } });
229263
expectNotType<Filter<PetModel>>({ type: { $size: 2 } });
230-
231-
// dot key case that shows it is assignable even when the referenced key is the wrong type
232-
expectAssignable<Filter<PetModel>>({ 'bestFriend.name': 23 }); // using dot notation permits any type for the key
233-
expectNotType<Filter<PetModel>>({ bestFriend: { name: 23 } });

0 commit comments

Comments
 (0)