diff --git a/etc/notes/CHANGES_5.0.0.md b/etc/notes/CHANGES_5.0.0.md index 30f76f22dc..6175eb8ee3 100644 --- a/etc/notes/CHANGES_5.0.0.md +++ b/etc/notes/CHANGES_5.0.0.md @@ -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; +// 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`. +```typescript +interface Schema { + user: { + name: string + } +} + +declare const collection: Collection; + +// fails to compile, 4 is not assignable to type "string" +const filterPredicate: StrictFilter = { '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 diff --git a/src/index.ts b/src/index.ts index 0d0718b43b..dadb64da5f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -338,6 +338,9 @@ export type { RootFilterOperators, SchemaMember, SetFields, + StrictFilter, + StrictMatchKeysAndValues, + StrictUpdateFilter, UpdateFilter, WithId, WithoutId diff --git a/src/mongo_types.ts b/src/mongo_types.ts index 2408d7a88f..16f5740e95 100644 --- a/src/mongo_types.ts +++ b/src/mongo_types.ts @@ -65,13 +65,9 @@ export type EnhancedOmit = string extends keyof TRecor export type WithoutId = Omit; /** A MongoDB filter can be some portion of the schema or a set of operators @public */ -export type Filter = - | Partial - | ({ - [Property in Join, []>, '.'>]?: Condition< - PropertyType, Property> - >; - } & RootFilterOperators>); +export type Filter = { + [P in keyof WithId]?: Condition[P]>; +} & RootFilterOperators>; /** @public */ export type Condition = AlternativeType | FilterOperators>; @@ -247,19 +243,7 @@ export type OnlyFieldsOfType; /** @public */ -export type MatchKeysAndValues = Readonly< - { - [Property in Join, '.'>]?: PropertyType; - } & { - [Property in `${NestedPathsOfType}.$${`[${string}]` | ''}`]?: ArrayElement< - PropertyType - >; - } & { - [Property in `${NestedPathsOfType[]>}.$${ - | `[${string}]` - | ''}.${string}`]?: any; // Could be further narrowed - } & Document ->; +export type MatchKeysAndValues = Readonly> & Record; /** @public */ export type AddToSetOperators = { @@ -541,3 +525,63 @@ export type NestedPathsOfType = KeysOfAType< }, Type >; + +/** + * @public + * @experimental + */ +export type StrictFilter = + | Partial + | ({ + [Property in Join, []>, '.'>]?: Condition< + PropertyType, Property> + >; + } & RootFilterOperators>); + +/** + * @public + * @experimental + */ +export type StrictUpdateFilter = { + $currentDate?: OnlyFieldsOfType< + TSchema, + Date | Timestamp, + true | { $type: 'date' | 'timestamp' } + >; + $inc?: OnlyFieldsOfType; + $min?: StrictMatchKeysAndValues; + $max?: StrictMatchKeysAndValues; + $mul?: OnlyFieldsOfType; + $rename?: Record; + $set?: StrictMatchKeysAndValues; + $setOnInsert?: StrictMatchKeysAndValues; + $unset?: OnlyFieldsOfType; + $addToSet?: SetFields; + $pop?: OnlyFieldsOfType, 1 | -1>; + $pull?: PullOperator; + $push?: PushOperator; + $pullAll?: PullAllOperator; + $bit?: OnlyFieldsOfType< + TSchema, + NumericType | undefined, + { and: IntegerType } | { or: IntegerType } | { xor: IntegerType } + >; +} & Document; + +/** + * @public + * @experimental + */ +export type StrictMatchKeysAndValues = Readonly< + { + [Property in Join, '.'>]?: PropertyType; + } & { + [Property in `${NestedPathsOfType}.$${`[${string}]` | ''}`]?: ArrayElement< + PropertyType + >; + } & { + [Property in `${NestedPathsOfType[]>}.$${ + | `[${string}]` + | ''}.${string}`]?: any; // Could be further narrowed + } & Document +>; diff --git a/test/types/community/collection/recursive-types.test-d.ts b/test/types/community/collection/recursive-types.test-d.ts index cd1927e0cf..388bfc893a 100644 --- a/test/types/community/collection/recursive-types.test-d.ts +++ b/test/types/community/collection/recursive-types.test-d.ts @@ -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 @@ -15,7 +15,7 @@ interface Book { author: Author; } -expectAssignable>({ +expectAssignable>({ bestBook: { title: 'book title', author: { @@ -40,77 +40,77 @@ expectNotType>({ //////////// Filter // Depth of 1 has type checking -expectNotAssignable>({ +expectNotAssignable>({ 'bestBook.title': 23 }); // Depth of 2 has type checking -expectNotAssignable>({ +expectNotAssignable>({ 'bestBook.author.name': 23 }); // Depth of 3 has type checking -expectNotAssignable>({ +expectNotAssignable>({ 'bestBook.author.bestBook.title': 23 }); // Depth of 4 has type checking -expectNotAssignable>({ +expectNotAssignable>({ 'bestBook.author.bestBook.author.name': 23 }); // Depth of 5 has type checking -expectNotAssignable>({ +expectNotAssignable>({ 'bestBook.author.bestBook.author.bestBook.title': 23 }); // Depth of 6 has type checking -expectNotAssignable>({ +expectNotAssignable>({ 'bestBook.author.bestBook.author.bestBook.author.name': 23 }); // Depth of 7 has type checking -expectNotAssignable>({ +expectNotAssignable>({ 'bestBook.author.bestBook.author.bestBook.author.bestBook.title': 23 }); // Depth of 8 does **not** have type checking -expectAssignable>({ +expectAssignable>({ 'bestBook.author.bestBook.author.bestBook.author.bestBook.author.name': 23 }); //////////// UpdateFilter // Depth of 1 has type checking -expectNotAssignable>({ +expectNotAssignable>({ $set: { 'bestBook.title': 23 } }); // Depth of 2 has type checking -expectNotAssignable>({ +expectAssignable>({ $set: { 'bestBook.author.name': 23 } }); // Depth of 3 has type checking -expectNotAssignable>({ +expectAssignable>({ $set: { 'bestBook.author.bestBook.title': 23 } }); // Depth of 4 has type checking -expectNotAssignable>({ +expectAssignable>({ $set: { 'bestBook.author.bestBook.author.name': 23 } }); // Depth of 5 has type checking -expectNotAssignable>({ +expectAssignable>({ $set: { 'bestBook.author.bestBook.author.bestBook.title': 23 } }); // Depth of 6 has type checking -expectNotAssignable>({ +expectAssignable>({ $set: { 'bestBook.author.bestBook.author.bestBook.author.name': 23 } }); // Depth of 7 has type checking -expectNotAssignable>({ +expectAssignable>({ $set: { 'bestBook.author.bestBook.author.bestBook.author.bestBook.title': 23 } @@ -132,11 +132,6 @@ interface RecursiveButNotReally { } declare const recursiveButNotReallyCollection: Collection; -expectError( - recursiveButNotReallyCollection.find({ - 'a.a': 'asdf' - }) -); recursiveButNotReallyCollection.find({ 'a.a': 2 }); @@ -237,17 +232,6 @@ interface Directory { } declare const recursiveSchemaWithArray: Collection; -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 @@ -297,12 +281,12 @@ type D = { a: A; }; -expectAssignable>({ +expectAssignable>({ 'b.c.d.a.b.c.d.a.b.name': 'a' }); // Beyond the depth supported, there is no type checking -expectAssignable>({ +expectAssignable>({ 'b.c.d.a.b.c.d.a.b.c.name': 3 }); diff --git a/test/types/community/collection/updateX.test-d.ts b/test/types/community/collection/updateX.test-d.ts index 09c8744ce7..9107db45ad 100644 --- a/test/types/community/collection/updateX.test-d.ts +++ b/test/types/community/collection/updateX.test-d.ts @@ -8,6 +8,7 @@ import type { PullOperator, PushOperator, SetFields, + StrictUpdateFilter, UpdateFilter } from '../../../mongodb'; import { @@ -105,7 +106,7 @@ interface TestModel { } const collectionTType = db.collection('test.update'); -function buildUpdateFilter(updateQuery: UpdateFilter): UpdateFilter { +function buildUpdateFilter(updateQuery: UpdateFilter): StrictUpdateFilter { return updateQuery; } @@ -214,13 +215,12 @@ expectAssignable>({ $set: { 'subInterfaceField.nestedObj expectAssignable>({ $set: { 'subInterfaceField.nestedObject': { a: '1', b: '2' } } }); -expectError>({ +expectError>({ $set: { 'subInterfaceField.nestedObject': { a: '1' } } }); -expectError>({ +expectError>({ $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>({ $set: { 'unknown.field': null } }); @@ -231,7 +231,6 @@ expectAssignable>({ $set: { 'numberArray.$[]': 1000.2 } expectAssignable>({ $set: { 'subInterfaceArray.$.field3': 40 } }); expectAssignable>({ $set: { 'subInterfaceArray.$[bla].field3': 40 } }); expectAssignable>({ $set: { 'subInterfaceArray.$[].field3': 1000.2 } }); -expectError(buildUpdateFilter({ $set: { 'numberArray.$': '20' } })); expectAssignable>({ $setOnInsert: { numberField: 1 } }); expectAssignable>({ @@ -243,7 +242,6 @@ expectAssignable>({ $setOnInsert: { longField: Long.from expectAssignable>({ $setOnInsert: { stringField: 'a' } }); expectError(buildUpdateFilter({ $setOnInsert: { stringField: 123 } })); expectAssignable>({ $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>({ $setOnInsert: { 'unknown.field': null } });