Skip to content

Commit

Permalink
feat: support dot-notation attributes in Filter
Browse files Browse the repository at this point in the history
  • Loading branch information
avaly committed Nov 9, 2021
1 parent e1234a7 commit 35f962b
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 12 deletions.
5 changes: 4 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,9 @@ export type {
KeysOfAType,
KeysOfOtherType,
IsAny,
OneOrMore
OneOrMore,
Join,
PropertyType,
NestedPaths
} from './mongo_types';
export type { serialize, deserialize } from './bson';
47 changes: 46 additions & 1 deletion src/mongo_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export type WithoutId<TSchema> = Omit<TSchema, '_id'>;

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

/** @public */
Expand Down Expand Up @@ -422,3 +422,48 @@ export class TypedEventEmitter<Events extends EventsDescription> extends EventEm

/** @public */
export class CancellationToken extends TypedEventEmitter<{ cancel(): void }> {}

/**
* Helper types for dot-notation filter attributes
*/

/** @public */
export type Join<T extends unknown[], D extends string> = T extends []
? ''
: T extends [string | number]
? `${T[0]}`
: T extends [string | number, ...infer R]
? `${T[0]}${D}${Join<R, D>}`
: string | number;

/** @public */
export type PropertyType<Type, Property extends string> = string extends Property
? unknown
: Property extends keyof Type
? Type[Property]
: Property extends `${infer Key}.${infer Rest}`
? Key extends `${number}`
? Type extends Array<infer ArrayType>
? PropertyType<ArrayType, Rest>
: Type extends ReadonlyArray<infer ArrayType>
? PropertyType<ArrayType, Rest>
: unknown
: Key extends keyof Type
? PropertyType<Type[Key], Rest>
: unknown
: unknown;

// We dont't support nested circular references
/** @public */
export type NestedPaths<Type> = Type extends string | number | boolean | Date | ObjectId
? []
: Type extends Array<infer ArrayType>
? [number, ...NestedPaths<ArrayType>]
: Type extends ReadonlyArray<infer ArrayType>
? [number, ...NestedPaths<ArrayType>]
: // eslint-disable-next-line @typescript-eslint/ban-types
Type extends object
? {
[Key in Extract<keyof Type, string>]: [Key, ...NestedPaths<Type[Key]>];
}[Extract<keyof Type, string>]
: [];
50 changes: 40 additions & 10 deletions test/types/community/collection/filterQuery.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ const db = client.db('test');
* Test the generic Filter using collection.find<T>() 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
Expand All @@ -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<PetModel>; // readonly array of objects
playmates?: PetModel[]; // writable array of objects
readonly friends?: ReadonlyArray<HumanModel>; // 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',
Expand Down Expand Up @@ -78,14 +97,29 @@ expectNotType<Filter<PetModel>>({ 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<Filter<PetModel>>({ bestFriend: { family: 'Andersons' } });
expectNotType<Filter<PetModel>>({ bestFriend: { name: 'Andersons' } });
/// it should not accept wrong types for nested document fields
expectNotType<Filter<PetModel>>({ bestFriend: 21 });
expectNotType<Filter<PetModel>>({ bestFriend: 'Andersons' });
expectNotType<Filter<PetModel>>({ bestFriend: [spot] });
expectNotType<Filter<PetModel>>({ bestFriend: [{ family: 'Andersons' }] });
expectNotType<Filter<PetModel>>({ 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 });
collectionT.find({ 'friends.0.name': 'John' });
collectionT.find({ 'playmates.0.name': 'John' });
/// it should not accept wrong types for nested document fields
expectNotType<Filter<PetModel>>({ 'meta.updatedAt': 123 });
expectNotType<Filter<PetModel>>({ 'meta.updatedAt': true });
expectNotType<Filter<PetModel>>({ 'meta.updatedAt': 'now' });
expectNotType<Filter<PetModel>>({ 'meta.deep.nested.level': '123' });
expectNotType<Filter<PetModel>>({ 'meta.deep.nested.level': true });
expectNotType<Filter<PetModel>>({ 'meta.deep.nested.level': new Date() });
expectNotType<Filter<PetModel>>({ 'friends.0.name': 123 });
expectNotType<Filter<PetModel>>({ 'playmates.0.name': 123 });

/// it should query __array__ fields by exact match
await collectionT.find({ treats: ['kibble', 'bone'] }).toArray();
Expand Down Expand Up @@ -227,7 +261,3 @@ await collectionT.find({ playmates: { $elemMatch: { name: 'MrMeow' } } }).toArra
expectNotType<Filter<PetModel>>({ name: { $all: ['world', 'world'] } });
expectNotType<Filter<PetModel>>({ age: { $elemMatch: [1, 2] } });
expectNotType<Filter<PetModel>>({ type: { $size: 2 } });

// dot key case that shows it is assignable even when the referenced key is the wrong type
expectAssignable<Filter<PetModel>>({ 'bestFriend.name': 23 }); // using dot notation permits any type for the key
expectNotType<Filter<PetModel>>({ bestFriend: { name: 23 } });

0 comments on commit 35f962b

Please sign in to comment.