diff --git a/src/index.ts b/src/index.ts index 4a80f643bdd..a50cccd608d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -430,6 +430,9 @@ export type { KeysOfAType, KeysOfOtherType, IsAny, - OneOrMore + OneOrMore, + Join, + PropertyType, + NestedPaths } from './mongo_types'; export type { serialize, deserialize } from './bson'; diff --git a/src/mongo_types.ts b/src/mongo_types.ts index d4107e1418b..4221ed7e8dc 100644 --- a/src/mongo_types.ts +++ b/src/mongo_types.ts @@ -54,7 +54,7 @@ export type WithoutId = Omit; /** A MongoDB filter can be some portion of the schema or a set of operators @public */ export type Filter = { - [P in keyof TSchema]?: Condition; + [P in Join, '.'>]?: Condition>; } & RootFilterOperators; @@ -425,3 +425,45 @@ export class TypedEventEmitter extends EventEm /** @public */ export class CancellationToken extends TypedEventEmitter<{ cancel(): void }> {} + +/** + * Helper types for dot-notation filter attributes + */ + +/** @public */ +export type Join = T extends [] + ? '' + : T extends [string | number | boolean | bigint] + ? `${T[0]}` + : T extends [string | number | boolean | bigint, ...infer R] + ? `${T[0]}${D}${Join}` + : string; + +/** @public */ +export type PropertyType = string extends P + ? unknown + : P extends keyof T + ? T[P] + : P extends `${infer K}.${infer R}` + ? K extends keyof T + ? PropertyType + : unknown + : unknown; + +// We dont't support nested circular references +/** @public */ +export type NestedPaths = T extends + | string + | number + | boolean + | Date + | ObjectId + | Array + | ReadonlyArray + ? [] + : // eslint-disable-next-line @typescript-eslint/ban-types + T extends object + ? { + [K in Extract]: [K, ...NestedPaths]; + }[Extract] + : []; diff --git a/test/types/community/collection/filterQuery.test-d.ts b/test/types/community/collection/filterQuery.test-d.ts index 7902002a909..2536eeaa766 100644 --- a/test/types/community/collection/filterQuery.test-d.ts +++ b/test/types/community/collection/filterQuery.test-d.ts @@ -15,6 +15,11 @@ const db = client.db('test'); * Test the generic Filter using collection.find() method */ +interface HumanModel { + _id: ObjectId; + name: string; +} + // a collection model for all possible MongoDB BSON types and TypeScript types interface PetModel { _id: ObjectId; // ObjectId field @@ -23,14 +28,28 @@ interface PetModel { age: number; // number field type: 'dog' | 'cat' | 'fish'; // union field isCute: boolean; // boolean field - bestFriend?: PetModel; // object field (Embedded/Nested Documents) + bestFriend?: HumanModel; // object field (Embedded/Nested Documents) createdAt: Date; // date field treats: string[]; // array of string playTimePercent: Decimal128; // bson Decimal128 type - readonly friends?: ReadonlyArray; // readonly array of objects - playmates?: PetModel[]; // writable array of objects + readonly friends?: ReadonlyArray; // readonly array of objects + playmates?: HumanModel[]; // writable array of objects + // Object with multiple nested levels + meta?: { + updatedAt?: Date; + deep?: { + nested?: { + level?: number; + }; + }; + }; } +const john = { + _id: new ObjectId('577fa2d90c4cc47e31cf4b6a'), + name: 'John' +}; + const spot = { _id: new ObjectId('577fa2d90c4cc47e31cf4b6f'), name: 'Spot', @@ -78,14 +97,25 @@ expectNotType>({ age: [23, 43] }); /// it should query __nested document__ fields only by exact match // TODO: we currently cannot enforce field order but field order is important for mongo -await collectionT.find({ bestFriend: spot }).toArray(); +await collectionT.find({ bestFriend: john }).toArray(); /// nested documents query should contain all required fields -expectNotType>({ bestFriend: { family: 'Andersons' } }); +expectNotType>({ bestFriend: { name: 'Andersons' } }); /// it should not accept wrong types for nested document fields expectNotType>({ bestFriend: 21 }); expectNotType>({ bestFriend: 'Andersons' }); expectNotType>({ bestFriend: [spot] }); -expectNotType>({ bestFriend: [{ family: 'Andersons' }] }); +expectNotType>({ bestFriend: [{ name: 'Andersons' }] }); + +/// it should query __nested document__ fields using dot-notation +collectionT.find({ 'meta.updatedAt': new Date() }); +collectionT.find({ 'meta.deep.nested.level': 123 }); +/// it should not accept wrong types for nested document fields +expectNotType>({ 'meta.updatedAt': 123 }); +expectNotType>({ 'meta.updatedAt': true }); +expectNotType>({ 'meta.updatedAt': 'now' }); +expectNotType>({ 'meta.deep.nested.level': '123' }); +expectNotType>({ 'meta.deep.nested.level': true }); +expectNotType>({ 'meta.deep.nested.level': new Date() }); /// it should query __array__ fields by exact match await collectionT.find({ treats: ['kibble', 'bone'] }).toArray(); @@ -227,7 +257,3 @@ await collectionT.find({ playmates: { $elemMatch: { name: 'MrMeow' } } }).toArra expectNotType>({ name: { $all: ['world', 'world'] } }); expectNotType>({ age: { $elemMatch: [1, 2] } }); expectNotType>({ type: { $size: 2 } }); - -// dot key case that shows it is assignable even when the referenced key is the wrong type -expectAssignable>({ 'bestFriend.name': 23 }); // using dot notation permits any type for the key -expectNotType>({ bestFriend: { name: 23 } });