From 1a9a44cf91847258cf33454d6d3c71c3ba002507 Mon Sep 17 00:00:00 2001 From: Eliott C Date: Fri, 17 Jun 2022 23:07:31 +0200 Subject: [PATCH] feat(NODE-4267): support nested fields in type completion for UpdateFilter (#3259) Co-authored-by: Julien Chaumond --- src/index.ts | 2 + src/mongo_types.ts | 29 ++++++++- .../community/collection/bulkWrite.test-d.ts | 4 +- .../community/collection/updateX.test-d.ts | 60 +++++++++++++------ 4 files changed, 75 insertions(+), 20 deletions(-) diff --git a/src/index.ts b/src/index.ts index 1e78dc2a67..18e1e4d995 100644 --- a/src/index.ts +++ b/src/index.ts @@ -305,6 +305,7 @@ export type { AcceptedFields, AddToSetOperators, AlternativeType, + ArrayElement, ArrayOperator, BitwiseFilter, BSONTypeAlias, @@ -322,6 +323,7 @@ export type { KeysOfOtherType, MatchKeysAndValues, NestedPaths, + NestedPathsOfType, NonObjectIdLikeDocument, NotAcceptedFields, NumericType, diff --git a/src/mongo_types.ts b/src/mongo_types.ts index fbbb2a475c..5b7e0729e5 100644 --- a/src/mongo_types.ts +++ b/src/mongo_types.ts @@ -214,6 +214,9 @@ export type IsAny = true extends false & Type /** @public */ export type Flatten = Type extends ReadonlyArray ? Item : Type; +/** @public */ +export type ArrayElement = Type extends ReadonlyArray ? Item : never; + /** @public */ export type SchemaMember = { [P in keyof T]?: V } | { [key: string]: V }; @@ -258,7 +261,19 @@ export type OnlyFieldsOfType; /** @public */ -export type MatchKeysAndValues = Readonly> & Record; +export type MatchKeysAndValues = Readonly< + { + [Property in Join, '.'>]?: PropertyType; + } & { + [Property in `${NestedPathsOfType}.$${`[${string}]` | ''}`]?: ArrayElement< + PropertyType + >; + } & { + [Property in `${NestedPathsOfType[]>}.$${ + | `[${string}]` + | ''}.${string}`]?: any; // Could be further narrowed + } +>; /** @public */ export type AddToSetOperators = { @@ -520,3 +535,15 @@ export type NestedPaths = Type extends [Key, ...NestedPaths]; }[Extract] : []; + +/** + * @public + * returns keys (strings) for every path into a schema with a value of type + * https://docs.mongodb.com/manual/tutorial/query-embedded-documents/ + */ +export type NestedPathsOfType = KeysOfAType< + { + [Property in Join, '.'>]: PropertyType; + }, + Type +>; diff --git a/test/types/community/collection/bulkWrite.test-d.ts b/test/types/community/collection/bulkWrite.test-d.ts index b35c1e4d05..271b3b71a7 100644 --- a/test/types/community/collection/bulkWrite.test-d.ts +++ b/test/types/community/collection/bulkWrite.test-d.ts @@ -74,7 +74,7 @@ collectionType.bulkWrite([ update: { $set: { numberField: 123, - 'dot.notation': true + 'subInterfaceField.field1': 'true' } } } @@ -123,7 +123,7 @@ collectionType.bulkWrite([ update: { $set: { numberField: 123, - 'dot.notation': true + 'subInterfaceField.field2': 'true' } } } diff --git a/test/types/community/collection/updateX.test-d.ts b/test/types/community/collection/updateX.test-d.ts index ae562e66c6..6d269bc320 100644 --- a/test/types/community/collection/updateX.test-d.ts +++ b/test/types/community/collection/updateX.test-d.ts @@ -22,7 +22,10 @@ import type { } from '../../../../src/mongo_types'; // MatchKeysAndValues - for basic mapping keys to their values, restricts that key types must be the same but optional, and permit dot array notation -expectAssignable>({ a: 2, 'dot.notation': true }); +expectAssignable>({ + a: 2, + 'c.d': true +}); expectNotType>({ b: 2 }); // AddToSetOperators @@ -70,6 +73,7 @@ interface SubTestModel { _id: ObjectId; field1: string; field2?: string; + field3?: number; } type FruitTypes = 'apple' | 'pear'; @@ -78,6 +82,7 @@ type FruitTypes = 'apple' | 'pear'; interface TestModel { stringField: string; numberField: number; + numberArray: number[]; decimal128Field: Decimal128; doubleField: Double; int32Field: Int32; @@ -148,10 +153,13 @@ expectAssignable>({ $min: { doubleField: new Double(1.23 expectAssignable>({ $min: { int32Field: new Int32(10) } }); expectAssignable>({ $min: { longField: Long.fromString('999') } }); expectAssignable>({ $min: { stringField: 'a' } }); -expectAssignable>({ $min: { 'dot.notation': 2 } }); -expectAssignable>({ $min: { 'subInterfaceArray.$': 'string' } }); -expectAssignable>({ $min: { 'subInterfaceArray.$[bla]': 40 } }); -expectAssignable>({ $min: { 'subInterfaceArray.$[]': 1000.2 } }); +expectAssignable>({ $min: { 'subInterfaceField.field1': '2' } }); +expectAssignable>({ $min: { 'numberArray.$': 40 } }); +expectAssignable>({ $min: { 'numberArray.$[bla]': 40 } }); +expectAssignable>({ $min: { 'numberArray.$[]': 1000.2 } }); +expectAssignable>({ $min: { 'subInterfaceArray.$.field3': 40 } }); +expectAssignable>({ $min: { 'subInterfaceArray.$[bla].field3': 40 } }); +expectAssignable>({ $min: { 'subInterfaceArray.$[].field3': 1000.2 } }); expectNotType>({ $min: { numberField: 'a' } }); // Matches the type of the keys @@ -163,10 +171,13 @@ expectAssignable>({ $max: { doubleField: new Double(1.23 expectAssignable>({ $max: { int32Field: new Int32(10) } }); expectAssignable>({ $max: { longField: Long.fromString('999') } }); expectAssignable>({ $max: { stringField: 'a' } }); -expectAssignable>({ $max: { 'dot.notation': 2 } }); -expectAssignable>({ $max: { 'subInterfaceArray.$': -10 } }); -expectAssignable>({ $max: { 'subInterfaceArray.$[bla]': 40 } }); -expectAssignable>({ $max: { 'subInterfaceArray.$[]': 1000.2 } }); +expectAssignable>({ $max: { 'subInterfaceField.field1': '2' } }); +expectAssignable>({ $max: { 'numberArray.$': 40 } }); +expectAssignable>({ $max: { 'numberArray.$[bla]': 40 } }); +expectAssignable>({ $max: { 'numberArray.$[]': 1000.2 } }); +expectAssignable>({ $max: { 'subInterfaceArray.$.field3': 40 } }); +expectAssignable>({ $max: { 'subInterfaceArray.$[bla].field3': 40 } }); +expectAssignable>({ $max: { 'subInterfaceArray.$[].field3': 1000.2 } }); expectNotType>({ $min: { numberField: 'a' } }); // Matches the type of the keys @@ -192,10 +203,16 @@ expectAssignable>({ $set: { int32Field: new Int32(10) } expectAssignable>({ $set: { longField: Long.fromString('999') } }); expectAssignable>({ $set: { stringField: 'a' } }); expectError(buildUpdateFilter({ $set: { stringField: 123 } })); -expectAssignable>({ $set: { 'dot.notation': 2 } }); -expectAssignable>({ $set: { 'subInterfaceArray.$': -10 } }); -expectAssignable>({ $set: { 'subInterfaceArray.$[bla]': 40 } }); -expectAssignable>({ $set: { 'subInterfaceArray.$[]': 1000.2 } }); +expectAssignable>({ $set: { 'subInterfaceField.field2': '2' } }); +expectError(buildUpdateFilter({ $set: { 'subInterfaceField.field2': 2 } })); +expectError(buildUpdateFilter({ $set: { 'unknown.field': null } })); +expectAssignable>({ $set: { 'numberArray.$': 40 } }); +expectAssignable>({ $set: { 'numberArray.$[bla]': 40 } }); +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>({ @@ -206,10 +223,19 @@ expectAssignable>({ $setOnInsert: { int32Field: new Int3 expectAssignable>({ $setOnInsert: { longField: Long.fromString('999') } }); expectAssignable>({ $setOnInsert: { stringField: 'a' } }); expectError(buildUpdateFilter({ $setOnInsert: { stringField: 123 } })); -expectAssignable>({ $setOnInsert: { 'dot.notation': 2 } }); -expectAssignable>({ $setOnInsert: { 'subInterfaceArray.$': -10 } }); -expectAssignable>({ $setOnInsert: { 'subInterfaceArray.$[bla]': 40 } }); -expectAssignable>({ $setOnInsert: { 'subInterfaceArray.$[]': 1000.2 } }); +expectAssignable>({ $setOnInsert: { 'subInterfaceField.field1': '2' } }); +expectError(buildUpdateFilter({ $setOnInsert: { 'subInterfaceField.field2': 2 } })); +expectError(buildUpdateFilter({ $setOnInsert: { 'unknown.field': null } })); +expectAssignable>({ $setOnInsert: { 'numberArray.$': 40 } }); +expectAssignable>({ $setOnInsert: { 'numberArray.$[bla]': 40 } }); +expectAssignable>({ $setOnInsert: { 'numberArray.$[]': 1000.2 } }); +expectAssignable>({ $setOnInsert: { 'subInterfaceArray.$.field3': 40 } }); +expectAssignable>({ + $setOnInsert: { 'subInterfaceArray.$[bla].field3': 40 } +}); +expectAssignable>({ + $ssetOnInsert: { 'subInterfaceArray.$[].field3': 1000.2 } +}); expectAssignable>({ $unset: { numberField: '' } }); expectAssignable>({ $unset: { decimal128Field: '' } });