Skip to content

Commit 26145df

Browse files
baileympearsondurrannbbeekendariakp
authored
feat(NODE-4738)!: remove dot notation support by default (#3520)
Co-authored-by: Durran Jordan <[email protected]> Co-authored-by: Neal Beeken <[email protected]> Co-authored-by: Daria Pardue <[email protected]>
1 parent 1470115 commit 26145df

File tree

5 files changed

+139
-61
lines changed

5 files changed

+139
-61
lines changed

etc/notes/CHANGES_5.0.0.md

+49
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,55 @@ The following is a detailed collection of the changes in the major v5 release of
1616

1717
## Changes
1818

19+
### Dot Notation Typescript Support Removed By Default
20+
21+
**NOTE** This is a **Typescript compile-time only** change. Dot notation in filters sent to MongoDB will still work the same.
22+
23+
Version 4.3.0 introduced Typescript support for dot notation in filter predicates. For example:
24+
25+
```typescript
26+
interface Schema {
27+
user: {
28+
name: string
29+
}
30+
}
31+
32+
declare const collection: Collection<Schema>;
33+
// compiles pre-v4.3.0, fails in v4.3.0+
34+
collection.find({ 'user.name': 4 })
35+
```
36+
37+
This change caused a number of problems for users, including slow compilation times and compile errors for
38+
valid dot notation queries. While we have tried to mitigate this issue as much as possible
39+
in v4, ultimately we do not believe that this feature is fully production ready for all use cases.
40+
41+
Driver 5.0 removes type checking for dot notation in filter predicates. The preceding example will compile with
42+
driver v5.
43+
44+
#### Dot Notation Helper Types Exported
45+
46+
Although we removed support for type checking on dot notation filters by default, we have preserved the
47+
corresponding types in an experimental capacity.
48+
These helper types can be used for type checking. We export the `StrictUpdateFilter` and the `StrictFilter`
49+
types for type safety in updates and finds.
50+
51+
To use one of the new types, simply create a predicate that uses dot notation and assign it the type of `StrictFilter<your schema>`.
52+
```typescript
53+
interface Schema {
54+
user: {
55+
name: string
56+
}
57+
}
58+
59+
declare const collection: Collection<Schema>;
60+
61+
// fails to compile, 4 is not assignable to type "string"
62+
const filterPredicate: StrictFilter<Schema> = { 'user.name': 4 };
63+
collection.find(filterPredicate);
64+
```
65+
66+
**NOTE** As an experimental feature, these types can change at any time and are not recommended for production settings.
67+
1968
### `Collection.mapReduce()` helper removed
2069

2170
The `mapReduce` helper has been removed from the `Collection` class. The `mapReduce` operation has been

src/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,9 @@ export type {
338338
RootFilterOperators,
339339
SchemaMember,
340340
SetFields,
341+
StrictFilter,
342+
StrictMatchKeysAndValues,
343+
StrictUpdateFilter,
341344
UpdateFilter,
342345
WithId,
343346
WithoutId

src/mongo_types.ts

+64-20
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,9 @@ export type EnhancedOmit<TRecordOrUnion, KeyUnion> = string extends keyof TRecor
6565
export type WithoutId<TSchema> = Omit<TSchema, '_id'>;
6666

6767
/** A MongoDB filter can be some portion of the schema or a set of operators @public */
68-
export type Filter<TSchema> =
69-
| Partial<TSchema>
70-
| ({
71-
[Property in Join<NestedPaths<WithId<TSchema>, []>, '.'>]?: Condition<
72-
PropertyType<WithId<TSchema>, Property>
73-
>;
74-
} & RootFilterOperators<WithId<TSchema>>);
68+
export type Filter<TSchema> = {
69+
[P in keyof WithId<TSchema>]?: Condition<WithId<TSchema>[P]>;
70+
} & RootFilterOperators<WithId<TSchema>>;
7571

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

249245
/** @public */
250-
export type MatchKeysAndValues<TSchema> = Readonly<
251-
{
252-
[Property in Join<NestedPaths<TSchema, []>, '.'>]?: PropertyType<TSchema, Property>;
253-
} & {
254-
[Property in `${NestedPathsOfType<TSchema, any[]>}.$${`[${string}]` | ''}`]?: ArrayElement<
255-
PropertyType<TSchema, Property extends `${infer Key}.$${string}` ? Key : never>
256-
>;
257-
} & {
258-
[Property in `${NestedPathsOfType<TSchema, Record<string, any>[]>}.$${
259-
| `[${string}]`
260-
| ''}.${string}`]?: any; // Could be further narrowed
261-
} & Document
262-
>;
246+
export type MatchKeysAndValues<TSchema> = Readonly<Partial<TSchema>> & Record<string, any>;
263247

264248
/** @public */
265249
export type AddToSetOperators<Type> = {
@@ -541,3 +525,63 @@ export type NestedPathsOfType<TSchema, Type> = KeysOfAType<
541525
},
542526
Type
543527
>;
528+
529+
/**
530+
* @public
531+
* @experimental
532+
*/
533+
export type StrictFilter<TSchema> =
534+
| Partial<TSchema>
535+
| ({
536+
[Property in Join<NestedPaths<WithId<TSchema>, []>, '.'>]?: Condition<
537+
PropertyType<WithId<TSchema>, Property>
538+
>;
539+
} & RootFilterOperators<WithId<TSchema>>);
540+
541+
/**
542+
* @public
543+
* @experimental
544+
*/
545+
export type StrictUpdateFilter<TSchema> = {
546+
$currentDate?: OnlyFieldsOfType<
547+
TSchema,
548+
Date | Timestamp,
549+
true | { $type: 'date' | 'timestamp' }
550+
>;
551+
$inc?: OnlyFieldsOfType<TSchema, NumericType | undefined>;
552+
$min?: StrictMatchKeysAndValues<TSchema>;
553+
$max?: StrictMatchKeysAndValues<TSchema>;
554+
$mul?: OnlyFieldsOfType<TSchema, NumericType | undefined>;
555+
$rename?: Record<string, string>;
556+
$set?: StrictMatchKeysAndValues<TSchema>;
557+
$setOnInsert?: StrictMatchKeysAndValues<TSchema>;
558+
$unset?: OnlyFieldsOfType<TSchema, any, '' | true | 1>;
559+
$addToSet?: SetFields<TSchema>;
560+
$pop?: OnlyFieldsOfType<TSchema, ReadonlyArray<any>, 1 | -1>;
561+
$pull?: PullOperator<TSchema>;
562+
$push?: PushOperator<TSchema>;
563+
$pullAll?: PullAllOperator<TSchema>;
564+
$bit?: OnlyFieldsOfType<
565+
TSchema,
566+
NumericType | undefined,
567+
{ and: IntegerType } | { or: IntegerType } | { xor: IntegerType }
568+
>;
569+
} & Document;
570+
571+
/**
572+
* @public
573+
* @experimental
574+
*/
575+
export type StrictMatchKeysAndValues<TSchema> = Readonly<
576+
{
577+
[Property in Join<NestedPaths<TSchema, []>, '.'>]?: PropertyType<TSchema, Property>;
578+
} & {
579+
[Property in `${NestedPathsOfType<TSchema, any[]>}.$${`[${string}]` | ''}`]?: ArrayElement<
580+
PropertyType<TSchema, Property extends `${infer Key}.$${string}` ? Key : never>
581+
>;
582+
} & {
583+
[Property in `${NestedPathsOfType<TSchema, Record<string, any>[]>}.$${
584+
| `[${string}]`
585+
| ''}.${string}`]?: any; // Could be further narrowed
586+
} & Document
587+
>;

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

+19-35
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { expectAssignable, expectError, expectNotAssignable, expectNotType } from 'tsd';
22

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

55
/**
66
* mutually recursive types are not supported and will not get type safety
@@ -15,7 +15,7 @@ interface Book {
1515
author: Author;
1616
}
1717

18-
expectAssignable<Filter<Author>>({
18+
expectAssignable<StrictFilter<Author>>({
1919
bestBook: {
2020
title: 'book title',
2121
author: {
@@ -40,77 +40,77 @@ expectNotType<UpdateFilter<Author>>({
4040

4141
//////////// Filter
4242
// Depth of 1 has type checking
43-
expectNotAssignable<Filter<Author>>({
43+
expectNotAssignable<StrictFilter<Author>>({
4444
'bestBook.title': 23
4545
});
4646
// Depth of 2 has type checking
47-
expectNotAssignable<Filter<Author>>({
47+
expectNotAssignable<StrictFilter<Author>>({
4848
'bestBook.author.name': 23
4949
});
5050
// Depth of 3 has type checking
51-
expectNotAssignable<Filter<Author>>({
51+
expectNotAssignable<StrictFilter<Author>>({
5252
'bestBook.author.bestBook.title': 23
5353
});
5454
// Depth of 4 has type checking
55-
expectNotAssignable<Filter<Author>>({
55+
expectNotAssignable<StrictFilter<Author>>({
5656
'bestBook.author.bestBook.author.name': 23
5757
});
5858
// Depth of 5 has type checking
59-
expectNotAssignable<Filter<Author>>({
59+
expectNotAssignable<StrictFilter<Author>>({
6060
'bestBook.author.bestBook.author.bestBook.title': 23
6161
});
6262
// Depth of 6 has type checking
63-
expectNotAssignable<Filter<Author>>({
63+
expectNotAssignable<StrictFilter<Author>>({
6464
'bestBook.author.bestBook.author.bestBook.author.name': 23
6565
});
6666
// Depth of 7 has type checking
67-
expectNotAssignable<Filter<Author>>({
67+
expectNotAssignable<StrictFilter<Author>>({
6868
'bestBook.author.bestBook.author.bestBook.author.bestBook.title': 23
6969
});
7070
// Depth of 8 does **not** have type checking
71-
expectAssignable<Filter<Author>>({
71+
expectAssignable<StrictFilter<Author>>({
7272
'bestBook.author.bestBook.author.bestBook.author.bestBook.author.name': 23
7373
});
7474

7575
//////////// UpdateFilter
7676
// Depth of 1 has type checking
77-
expectNotAssignable<UpdateFilter<Author>>({
77+
expectNotAssignable<StrictUpdateFilter<Author>>({
7878
$set: {
7979
'bestBook.title': 23
8080
}
8181
});
8282
// Depth of 2 has type checking
83-
expectNotAssignable<UpdateFilter<Author>>({
83+
expectAssignable<UpdateFilter<Author>>({
8484
$set: {
8585
'bestBook.author.name': 23
8686
}
8787
});
8888
// Depth of 3 has type checking
89-
expectNotAssignable<UpdateFilter<Author>>({
89+
expectAssignable<UpdateFilter<Author>>({
9090
$set: {
9191
'bestBook.author.bestBook.title': 23
9292
}
9393
});
9494
// Depth of 4 has type checking
95-
expectNotAssignable<UpdateFilter<Author>>({
95+
expectAssignable<UpdateFilter<Author>>({
9696
$set: {
9797
'bestBook.author.bestBook.author.name': 23
9898
}
9999
});
100100
// Depth of 5 has type checking
101-
expectNotAssignable<UpdateFilter<Author>>({
101+
expectAssignable<UpdateFilter<Author>>({
102102
$set: {
103103
'bestBook.author.bestBook.author.bestBook.title': 23
104104
}
105105
});
106106
// Depth of 6 has type checking
107-
expectNotAssignable<UpdateFilter<Author>>({
107+
expectAssignable<UpdateFilter<Author>>({
108108
$set: {
109109
'bestBook.author.bestBook.author.bestBook.author.name': 23
110110
}
111111
});
112112
// Depth of 7 has type checking
113-
expectNotAssignable<UpdateFilter<Author>>({
113+
expectAssignable<UpdateFilter<Author>>({
114114
$set: {
115115
'bestBook.author.bestBook.author.bestBook.author.bestBook.title': 23
116116
}
@@ -132,11 +132,6 @@ interface RecursiveButNotReally {
132132
}
133133

134134
declare const recursiveButNotReallyCollection: Collection<RecursiveButNotReally>;
135-
expectError(
136-
recursiveButNotReallyCollection.find({
137-
'a.a': 'asdf'
138-
})
139-
);
140135
recursiveButNotReallyCollection.find({
141136
'a.a': 2
142137
});
@@ -237,17 +232,6 @@ interface Directory {
237232
}
238233

239234
declare const recursiveSchemaWithArray: Collection<MongoStrings>;
240-
expectError(
241-
recursiveSchemaWithArray.findOne({
242-
'branches.0.id': 'hello'
243-
})
244-
);
245-
246-
expectError(
247-
recursiveSchemaWithArray.findOne({
248-
'branches.0.directories.0.id': 'hello'
249-
})
250-
);
251235

252236
// type safety breaks after the first
253237
// level of nested types
@@ -297,12 +281,12 @@ type D = {
297281
a: A;
298282
};
299283

300-
expectAssignable<Filter<A>>({
284+
expectAssignable<StrictFilter<A>>({
301285
'b.c.d.a.b.c.d.a.b.name': 'a'
302286
});
303287

304288
// Beyond the depth supported, there is no type checking
305-
expectAssignable<Filter<A>>({
289+
expectAssignable<StrictFilter<A>>({
306290
'b.c.d.a.b.c.d.a.b.c.name': 3
307291
});
308292

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

+4-6
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type {
88
PullOperator,
99
PushOperator,
1010
SetFields,
11+
StrictUpdateFilter,
1112
UpdateFilter
1213
} from '../../../mongodb';
1314
import {
@@ -105,7 +106,7 @@ interface TestModel {
105106
}
106107
const collectionTType = db.collection<TestModel>('test.update');
107108

108-
function buildUpdateFilter(updateQuery: UpdateFilter<TestModel>): UpdateFilter<TestModel> {
109+
function buildUpdateFilter(updateQuery: UpdateFilter<TestModel>): StrictUpdateFilter<TestModel> {
109110
return updateQuery;
110111
}
111112

@@ -214,13 +215,12 @@ expectAssignable<UpdateFilter<TestModel>>({ $set: { 'subInterfaceField.nestedObj
214215
expectAssignable<UpdateFilter<TestModel>>({
215216
$set: { 'subInterfaceField.nestedObject': { a: '1', b: '2' } }
216217
});
217-
expectError<UpdateFilter<TestModel>>({
218+
expectError<StrictUpdateFilter<TestModel>>({
218219
$set: { 'subInterfaceField.nestedObject': { a: '1' } }
219220
});
220-
expectError<UpdateFilter<TestModel>>({
221+
expectError<StrictUpdateFilter<TestModel>>({
221222
$set: { 'subInterfaceField.nestedObject': { a: 1, b: '2' } }
222223
});
223-
expectError(buildUpdateFilter({ $set: { 'subInterfaceField.field2': 2 } }));
224224

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

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

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

0 commit comments

Comments
 (0)