Skip to content

feat(NODE-4738)!: remove dot notation support by default #3520

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Jan 20, 2023
49 changes: 49 additions & 0 deletions etc/notes/CHANGES_5.0.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,55 @@ The following is a detailed collection of the changes in the major v5 release of

## Changes

### Dot Notation Typescript Support Removed By Default

**NOTE** This is a **Typescript compile-time only** change. Dot notation in filters sent to MongoDB will still work the same.

Version 4.3.0 introduced Typescript support for dot notation in filter predicates. For example:

```typescript
interface Schema {
user: {
name: string
}
}

declare const collection: Collection<Schema>;
// compiles pre-v4.3.0, fails in v4.3.0+
collection.find({ 'user.name': 4 })
```

This change caused a number of problems for users, including slow compilation times and compile errors for
valid dot notation queries. While we have tried to mitigate this issue as much as possible
in v4, ultimately we do not believe that this feature is fully production ready for all use cases.

Driver 5.0 removes type checking for dot notation in filter predicates. The preceding example will compile with
driver v5.

#### Dot Notation Helper Types Exported

Although we removed support for type checking on dot notation filters by default, we have preserved the
corresponding types in an experimental capacity.
These helper types can be used for type checking. We export the `StrictUpdateFilter` and the `StrictFilter`
types for type safety in updates and finds.

To use one of the new types, simply create a predicate that uses dot notation and assign it the type of `StrictFilter<your schema>`.
```typescript
interface Schema {
user: {
name: string
}
}

declare const collection: Collection<Schema>;

// fails to compile, 4 is not assignable to type "string"
const filterPredicate: StrictFilter<Schema> = { 'user.name': 4 };
collection.find(filterPredicate);
```

**NOTE** As an experimental feature, these types can change at any time and are not recommended for production settings.

### `Collection.mapReduce()` helper removed

The `mapReduce` helper has been removed from the `Collection` class. The `mapReduce` operation has been
Expand Down
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,9 @@ export type {
RootFilterOperators,
SchemaMember,
SetFields,
StrictFilter,
StrictMatchKeysAndValues,
StrictUpdateFilter,
UpdateFilter,
WithId,
WithoutId
Expand Down
84 changes: 64 additions & 20 deletions src/mongo_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,9 @@ export type EnhancedOmit<TRecordOrUnion, KeyUnion> = string extends keyof TRecor
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> =
| Partial<TSchema>
| ({
[Property in Join<NestedPaths<WithId<TSchema>, []>, '.'>]?: Condition<
PropertyType<WithId<TSchema>, Property>
>;
} & RootFilterOperators<WithId<TSchema>>);
export type Filter<TSchema> = {
[P in keyof WithId<TSchema>]?: Condition<WithId<TSchema>[P]>;
} & RootFilterOperators<WithId<TSchema>>;

/** @public */
export type Condition<T> = AlternativeType<T> | FilterOperators<AlternativeType<T>>;
Expand Down Expand Up @@ -247,19 +243,7 @@ export type OnlyFieldsOfType<TSchema, FieldType = any, AssignableType = FieldTyp
>;

/** @public */
export type MatchKeysAndValues<TSchema> = Readonly<
{
[Property in Join<NestedPaths<TSchema, []>, '.'>]?: PropertyType<TSchema, Property>;
} & {
[Property in `${NestedPathsOfType<TSchema, any[]>}.$${`[${string}]` | ''}`]?: ArrayElement<
PropertyType<TSchema, Property extends `${infer Key}.$${string}` ? Key : never>
>;
} & {
[Property in `${NestedPathsOfType<TSchema, Record<string, any>[]>}.$${
| `[${string}]`
| ''}.${string}`]?: any; // Could be further narrowed
} & Document
>;
export type MatchKeysAndValues<TSchema> = Readonly<Partial<TSchema>> & Record<string, any>;

/** @public */
export type AddToSetOperators<Type> = {
Expand Down Expand Up @@ -541,3 +525,63 @@ export type NestedPathsOfType<TSchema, Type> = KeysOfAType<
},
Type
>;

/**
* @public
* @experimental
*/
export type StrictFilter<TSchema> =
| Partial<TSchema>
| ({
[Property in Join<NestedPaths<WithId<TSchema>, []>, '.'>]?: Condition<
PropertyType<WithId<TSchema>, Property>
>;
} & RootFilterOperators<WithId<TSchema>>);

/**
* @public
* @experimental
*/
export type StrictUpdateFilter<TSchema> = {
$currentDate?: OnlyFieldsOfType<
TSchema,
Date | Timestamp,
true | { $type: 'date' | 'timestamp' }
>;
$inc?: OnlyFieldsOfType<TSchema, NumericType | undefined>;
$min?: StrictMatchKeysAndValues<TSchema>;
$max?: StrictMatchKeysAndValues<TSchema>;
$mul?: OnlyFieldsOfType<TSchema, NumericType | undefined>;
$rename?: Record<string, string>;
$set?: StrictMatchKeysAndValues<TSchema>;
$setOnInsert?: StrictMatchKeysAndValues<TSchema>;
$unset?: OnlyFieldsOfType<TSchema, any, '' | true | 1>;
$addToSet?: SetFields<TSchema>;
$pop?: OnlyFieldsOfType<TSchema, ReadonlyArray<any>, 1 | -1>;
$pull?: PullOperator<TSchema>;
$push?: PushOperator<TSchema>;
$pullAll?: PullAllOperator<TSchema>;
$bit?: OnlyFieldsOfType<
TSchema,
NumericType | undefined,
{ and: IntegerType } | { or: IntegerType } | { xor: IntegerType }
>;
} & Document;

/**
* @public
* @experimental
*/
export type StrictMatchKeysAndValues<TSchema> = Readonly<
{
[Property in Join<NestedPaths<TSchema, []>, '.'>]?: PropertyType<TSchema, Property>;
} & {
[Property in `${NestedPathsOfType<TSchema, any[]>}.$${`[${string}]` | ''}`]?: ArrayElement<
PropertyType<TSchema, Property extends `${infer Key}.$${string}` ? Key : never>
>;
} & {
[Property in `${NestedPathsOfType<TSchema, Record<string, any>[]>}.$${
| `[${string}]`
| ''}.${string}`]?: any; // Could be further narrowed
} & Document
>;
54 changes: 19 additions & 35 deletions test/types/community/collection/recursive-types.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expectAssignable, expectError, expectNotAssignable, expectNotType } from 'tsd';

import type { Collection, Filter, UpdateFilter } from '../../../../src';
import type { Collection, StrictFilter, StrictUpdateFilter, UpdateFilter } from '../../../../src';

/**
* mutually recursive types are not supported and will not get type safety
Expand All @@ -15,7 +15,7 @@ interface Book {
author: Author;
}

expectAssignable<Filter<Author>>({
expectAssignable<StrictFilter<Author>>({
bestBook: {
title: 'book title',
author: {
Expand All @@ -40,77 +40,77 @@ expectNotType<UpdateFilter<Author>>({

//////////// Filter
// Depth of 1 has type checking
expectNotAssignable<Filter<Author>>({
expectNotAssignable<StrictFilter<Author>>({
'bestBook.title': 23
});
// Depth of 2 has type checking
expectNotAssignable<Filter<Author>>({
expectNotAssignable<StrictFilter<Author>>({
'bestBook.author.name': 23
});
// Depth of 3 has type checking
expectNotAssignable<Filter<Author>>({
expectNotAssignable<StrictFilter<Author>>({
'bestBook.author.bestBook.title': 23
});
// Depth of 4 has type checking
expectNotAssignable<Filter<Author>>({
expectNotAssignable<StrictFilter<Author>>({
'bestBook.author.bestBook.author.name': 23
});
// Depth of 5 has type checking
expectNotAssignable<Filter<Author>>({
expectNotAssignable<StrictFilter<Author>>({
'bestBook.author.bestBook.author.bestBook.title': 23
});
// Depth of 6 has type checking
expectNotAssignable<Filter<Author>>({
expectNotAssignable<StrictFilter<Author>>({
'bestBook.author.bestBook.author.bestBook.author.name': 23
});
// Depth of 7 has type checking
expectNotAssignable<Filter<Author>>({
expectNotAssignable<StrictFilter<Author>>({
'bestBook.author.bestBook.author.bestBook.author.bestBook.title': 23
});
// Depth of 8 does **not** have type checking
expectAssignable<Filter<Author>>({
expectAssignable<StrictFilter<Author>>({
'bestBook.author.bestBook.author.bestBook.author.bestBook.author.name': 23
});

//////////// UpdateFilter
// Depth of 1 has type checking
expectNotAssignable<UpdateFilter<Author>>({
expectNotAssignable<StrictUpdateFilter<Author>>({
$set: {
'bestBook.title': 23
}
});
// Depth of 2 has type checking
expectNotAssignable<UpdateFilter<Author>>({
expectAssignable<UpdateFilter<Author>>({
$set: {
'bestBook.author.name': 23
}
});
// Depth of 3 has type checking
expectNotAssignable<UpdateFilter<Author>>({
expectAssignable<UpdateFilter<Author>>({
$set: {
'bestBook.author.bestBook.title': 23
}
});
// Depth of 4 has type checking
expectNotAssignable<UpdateFilter<Author>>({
expectAssignable<UpdateFilter<Author>>({
$set: {
'bestBook.author.bestBook.author.name': 23
}
});
// Depth of 5 has type checking
expectNotAssignable<UpdateFilter<Author>>({
expectAssignable<UpdateFilter<Author>>({
$set: {
'bestBook.author.bestBook.author.bestBook.title': 23
}
});
// Depth of 6 has type checking
expectNotAssignable<UpdateFilter<Author>>({
expectAssignable<UpdateFilter<Author>>({
$set: {
'bestBook.author.bestBook.author.bestBook.author.name': 23
}
});
// Depth of 7 has type checking
expectNotAssignable<UpdateFilter<Author>>({
expectAssignable<UpdateFilter<Author>>({
$set: {
'bestBook.author.bestBook.author.bestBook.author.bestBook.title': 23
}
Expand All @@ -132,11 +132,6 @@ interface RecursiveButNotReally {
}

declare const recursiveButNotReallyCollection: Collection<RecursiveButNotReally>;
expectError(
recursiveButNotReallyCollection.find({
'a.a': 'asdf'
})
);
recursiveButNotReallyCollection.find({
'a.a': 2
});
Expand Down Expand Up @@ -237,17 +232,6 @@ interface Directory {
}

declare const recursiveSchemaWithArray: Collection<MongoStrings>;
expectError(
recursiveSchemaWithArray.findOne({
'branches.0.id': 'hello'
})
);

expectError(
recursiveSchemaWithArray.findOne({
'branches.0.directories.0.id': 'hello'
})
);

// type safety breaks after the first
// level of nested types
Expand Down Expand Up @@ -297,12 +281,12 @@ type D = {
a: A;
};

expectAssignable<Filter<A>>({
expectAssignable<StrictFilter<A>>({
'b.c.d.a.b.c.d.a.b.name': 'a'
});

// Beyond the depth supported, there is no type checking
expectAssignable<Filter<A>>({
expectAssignable<StrictFilter<A>>({
'b.c.d.a.b.c.d.a.b.c.name': 3
});

Expand Down
10 changes: 4 additions & 6 deletions test/types/community/collection/updateX.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
PullOperator,
PushOperator,
SetFields,
StrictUpdateFilter,
UpdateFilter
} from '../../../mongodb';
import {
Expand Down Expand Up @@ -105,7 +106,7 @@ interface TestModel {
}
const collectionTType = db.collection<TestModel>('test.update');

function buildUpdateFilter(updateQuery: UpdateFilter<TestModel>): UpdateFilter<TestModel> {
function buildUpdateFilter(updateQuery: UpdateFilter<TestModel>): StrictUpdateFilter<TestModel> {
return updateQuery;
}

Expand Down Expand Up @@ -214,13 +215,12 @@ expectAssignable<UpdateFilter<TestModel>>({ $set: { 'subInterfaceField.nestedObj
expectAssignable<UpdateFilter<TestModel>>({
$set: { 'subInterfaceField.nestedObject': { a: '1', b: '2' } }
});
expectError<UpdateFilter<TestModel>>({
expectError<StrictUpdateFilter<TestModel>>({
$set: { 'subInterfaceField.nestedObject': { a: '1' } }
});
expectError<UpdateFilter<TestModel>>({
expectError<StrictUpdateFilter<TestModel>>({
$set: { 'subInterfaceField.nestedObject': { a: 1, b: '2' } }
});
expectError(buildUpdateFilter({ $set: { 'subInterfaceField.field2': 2 } }));

// NODE-3875 introduced intersection with Document to the MatchKeysAndValues so this no longer errors
expectAssignable<UpdateFilter<TestModel>>({ $set: { 'unknown.field': null } });
Expand All @@ -231,7 +231,6 @@ expectAssignable<UpdateFilter<TestModel>>({ $set: { 'numberArray.$[]': 1000.2 }
expectAssignable<UpdateFilter<TestModel>>({ $set: { 'subInterfaceArray.$.field3': 40 } });
expectAssignable<UpdateFilter<TestModel>>({ $set: { 'subInterfaceArray.$[bla].field3': 40 } });
expectAssignable<UpdateFilter<TestModel>>({ $set: { 'subInterfaceArray.$[].field3': 1000.2 } });
expectError(buildUpdateFilter({ $set: { 'numberArray.$': '20' } }));

expectAssignable<UpdateFilter<TestModel>>({ $setOnInsert: { numberField: 1 } });
expectAssignable<UpdateFilter<TestModel>>({
Expand All @@ -243,7 +242,6 @@ expectAssignable<UpdateFilter<TestModel>>({ $setOnInsert: { longField: Long.from
expectAssignable<UpdateFilter<TestModel>>({ $setOnInsert: { stringField: 'a' } });
expectError(buildUpdateFilter({ $setOnInsert: { stringField: 123 } }));
expectAssignable<UpdateFilter<TestModel>>({ $setOnInsert: { 'subInterfaceField.field1': '2' } });
expectError(buildUpdateFilter({ $setOnInsert: { 'subInterfaceField.field2': 2 } }));

// NODE-3875 introduced intersection with Document to the MatchKeysAndValues so this no longer errors
expectAssignable<UpdateFilter<TestModel>>({ $setOnInsert: { 'unknown.field': null } });
Expand Down