Skip to content
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

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

Merged
merged 12 commits into from
Jan 20, 2023
47 changes: 47 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,53 @@ 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 still work the same.
baileympearson marked this conversation as resolved.
Show resolved Hide resolved

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 many problems for users, including slow compilation times and compile errors for
baileympearson marked this conversation as resolved.
Show resolved Hide resolved
valid dot notation queries.
baileympearson marked this conversation as resolved.
Show resolved Hide resolved

Driver 5.0 removes type checking on filter predicates. The preceding example will compile with
dariakp marked this conversation as resolved.
Show resolved Hide resolved
baileympearson marked this conversation as resolved.
Show resolved Hide resolved
driver v5.

#### Dot Notation Helper Types Exported

Although we removed support for type checking on dot notation filters by default, we now export these types.
baileympearson marked this conversation as resolved.
Show resolved Hide resolved
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** These types are also now marked experimental and can be changed at any time.
baileympearson marked this conversation as resolved.
Show resolved Hide resolved

### `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