Skip to content

Commit 1a9a44c

Browse files
coyotte508julien-c
andauthored
feat(NODE-4267): support nested fields in type completion for UpdateFilter (#3259)
Co-authored-by: Julien Chaumond <[email protected]>
1 parent 904252c commit 1a9a44c

File tree

4 files changed

+75
-20
lines changed

4 files changed

+75
-20
lines changed

src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,7 @@ export type {
305305
AcceptedFields,
306306
AddToSetOperators,
307307
AlternativeType,
308+
ArrayElement,
308309
ArrayOperator,
309310
BitwiseFilter,
310311
BSONTypeAlias,
@@ -322,6 +323,7 @@ export type {
322323
KeysOfOtherType,
323324
MatchKeysAndValues,
324325
NestedPaths,
326+
NestedPathsOfType,
325327
NonObjectIdLikeDocument,
326328
NotAcceptedFields,
327329
NumericType,

src/mongo_types.ts

+28-1
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,9 @@ export type IsAny<Type, ResultIfAny, ResultIfNotAny> = true extends false & Type
214214
/** @public */
215215
export type Flatten<Type> = Type extends ReadonlyArray<infer Item> ? Item : Type;
216216

217+
/** @public */
218+
export type ArrayElement<Type> = Type extends ReadonlyArray<infer Item> ? Item : never;
219+
217220
/** @public */
218221
export type SchemaMember<T, V> = { [P in keyof T]?: V } | { [key: string]: V };
219222

@@ -258,7 +261,19 @@ export type OnlyFieldsOfType<TSchema, FieldType = any, AssignableType = FieldTyp
258261
>;
259262

260263
/** @public */
261-
export type MatchKeysAndValues<TSchema> = Readonly<Partial<TSchema>> & Record<string, any>;
264+
export type MatchKeysAndValues<TSchema> = Readonly<
265+
{
266+
[Property in Join<NestedPaths<TSchema>, '.'>]?: PropertyType<TSchema, Property>;
267+
} & {
268+
[Property in `${NestedPathsOfType<TSchema, any[]>}.$${`[${string}]` | ''}`]?: ArrayElement<
269+
PropertyType<TSchema, Property extends `${infer Key}.$${string}` ? Key : never>
270+
>;
271+
} & {
272+
[Property in `${NestedPathsOfType<TSchema, Record<string, any>[]>}.$${
273+
| `[${string}]`
274+
| ''}.${string}`]?: any; // Could be further narrowed
275+
}
276+
>;
262277

263278
/** @public */
264279
export type AddToSetOperators<Type> = {
@@ -520,3 +535,15 @@ export type NestedPaths<Type> = Type extends
520535
[Key, ...NestedPaths<Type[Key]>];
521536
}[Extract<keyof Type, string>]
522537
: [];
538+
539+
/**
540+
* @public
541+
* returns keys (strings) for every path into a schema with a value of type
542+
* https://docs.mongodb.com/manual/tutorial/query-embedded-documents/
543+
*/
544+
export type NestedPathsOfType<TSchema, Type> = KeysOfAType<
545+
{
546+
[Property in Join<NestedPaths<TSchema>, '.'>]: PropertyType<TSchema, Property>;
547+
},
548+
Type
549+
>;

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ collectionType.bulkWrite([
7474
update: {
7575
$set: {
7676
numberField: 123,
77-
'dot.notation': true
77+
'subInterfaceField.field1': 'true'
7878
}
7979
}
8080
}
@@ -123,7 +123,7 @@ collectionType.bulkWrite([
123123
update: {
124124
$set: {
125125
numberField: 123,
126-
'dot.notation': true
126+
'subInterfaceField.field2': 'true'
127127
}
128128
}
129129
}

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

+43-17
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ import type {
2222
} from '../../../../src/mongo_types';
2323

2424
// MatchKeysAndValues - for basic mapping keys to their values, restricts that key types must be the same but optional, and permit dot array notation
25-
expectAssignable<MatchKeysAndValues<{ a: number; b: string }>>({ a: 2, 'dot.notation': true });
25+
expectAssignable<MatchKeysAndValues<{ a: number; b: string; c: { d: boolean } }>>({
26+
a: 2,
27+
'c.d': true
28+
});
2629
expectNotType<MatchKeysAndValues<{ a: number; b: string }>>({ b: 2 });
2730

2831
// AddToSetOperators
@@ -70,6 +73,7 @@ interface SubTestModel {
7073
_id: ObjectId;
7174
field1: string;
7275
field2?: string;
76+
field3?: number;
7377
}
7478

7579
type FruitTypes = 'apple' | 'pear';
@@ -78,6 +82,7 @@ type FruitTypes = 'apple' | 'pear';
7882
interface TestModel {
7983
stringField: string;
8084
numberField: number;
85+
numberArray: number[];
8186
decimal128Field: Decimal128;
8287
doubleField: Double;
8388
int32Field: Int32;
@@ -148,10 +153,13 @@ expectAssignable<UpdateFilter<TestModel>>({ $min: { doubleField: new Double(1.23
148153
expectAssignable<UpdateFilter<TestModel>>({ $min: { int32Field: new Int32(10) } });
149154
expectAssignable<UpdateFilter<TestModel>>({ $min: { longField: Long.fromString('999') } });
150155
expectAssignable<UpdateFilter<TestModel>>({ $min: { stringField: 'a' } });
151-
expectAssignable<UpdateFilter<TestModel>>({ $min: { 'dot.notation': 2 } });
152-
expectAssignable<UpdateFilter<TestModel>>({ $min: { 'subInterfaceArray.$': 'string' } });
153-
expectAssignable<UpdateFilter<TestModel>>({ $min: { 'subInterfaceArray.$[bla]': 40 } });
154-
expectAssignable<UpdateFilter<TestModel>>({ $min: { 'subInterfaceArray.$[]': 1000.2 } });
156+
expectAssignable<UpdateFilter<TestModel>>({ $min: { 'subInterfaceField.field1': '2' } });
157+
expectAssignable<UpdateFilter<TestModel>>({ $min: { 'numberArray.$': 40 } });
158+
expectAssignable<UpdateFilter<TestModel>>({ $min: { 'numberArray.$[bla]': 40 } });
159+
expectAssignable<UpdateFilter<TestModel>>({ $min: { 'numberArray.$[]': 1000.2 } });
160+
expectAssignable<UpdateFilter<TestModel>>({ $min: { 'subInterfaceArray.$.field3': 40 } });
161+
expectAssignable<UpdateFilter<TestModel>>({ $min: { 'subInterfaceArray.$[bla].field3': 40 } });
162+
expectAssignable<UpdateFilter<TestModel>>({ $min: { 'subInterfaceArray.$[].field3': 1000.2 } });
155163

156164
expectNotType<UpdateFilter<TestModel>>({ $min: { numberField: 'a' } }); // Matches the type of the keys
157165

@@ -163,10 +171,13 @@ expectAssignable<UpdateFilter<TestModel>>({ $max: { doubleField: new Double(1.23
163171
expectAssignable<UpdateFilter<TestModel>>({ $max: { int32Field: new Int32(10) } });
164172
expectAssignable<UpdateFilter<TestModel>>({ $max: { longField: Long.fromString('999') } });
165173
expectAssignable<UpdateFilter<TestModel>>({ $max: { stringField: 'a' } });
166-
expectAssignable<UpdateFilter<TestModel>>({ $max: { 'dot.notation': 2 } });
167-
expectAssignable<UpdateFilter<TestModel>>({ $max: { 'subInterfaceArray.$': -10 } });
168-
expectAssignable<UpdateFilter<TestModel>>({ $max: { 'subInterfaceArray.$[bla]': 40 } });
169-
expectAssignable<UpdateFilter<TestModel>>({ $max: { 'subInterfaceArray.$[]': 1000.2 } });
174+
expectAssignable<UpdateFilter<TestModel>>({ $max: { 'subInterfaceField.field1': '2' } });
175+
expectAssignable<UpdateFilter<TestModel>>({ $max: { 'numberArray.$': 40 } });
176+
expectAssignable<UpdateFilter<TestModel>>({ $max: { 'numberArray.$[bla]': 40 } });
177+
expectAssignable<UpdateFilter<TestModel>>({ $max: { 'numberArray.$[]': 1000.2 } });
178+
expectAssignable<UpdateFilter<TestModel>>({ $max: { 'subInterfaceArray.$.field3': 40 } });
179+
expectAssignable<UpdateFilter<TestModel>>({ $max: { 'subInterfaceArray.$[bla].field3': 40 } });
180+
expectAssignable<UpdateFilter<TestModel>>({ $max: { 'subInterfaceArray.$[].field3': 1000.2 } });
170181

171182
expectNotType<UpdateFilter<TestModel>>({ $min: { numberField: 'a' } }); // Matches the type of the keys
172183

@@ -192,10 +203,16 @@ expectAssignable<UpdateFilter<TestModel>>({ $set: { int32Field: new Int32(10) }
192203
expectAssignable<UpdateFilter<TestModel>>({ $set: { longField: Long.fromString('999') } });
193204
expectAssignable<UpdateFilter<TestModel>>({ $set: { stringField: 'a' } });
194205
expectError(buildUpdateFilter({ $set: { stringField: 123 } }));
195-
expectAssignable<UpdateFilter<TestModel>>({ $set: { 'dot.notation': 2 } });
196-
expectAssignable<UpdateFilter<TestModel>>({ $set: { 'subInterfaceArray.$': -10 } });
197-
expectAssignable<UpdateFilter<TestModel>>({ $set: { 'subInterfaceArray.$[bla]': 40 } });
198-
expectAssignable<UpdateFilter<TestModel>>({ $set: { 'subInterfaceArray.$[]': 1000.2 } });
206+
expectAssignable<UpdateFilter<TestModel>>({ $set: { 'subInterfaceField.field2': '2' } });
207+
expectError(buildUpdateFilter({ $set: { 'subInterfaceField.field2': 2 } }));
208+
expectError(buildUpdateFilter({ $set: { 'unknown.field': null } }));
209+
expectAssignable<UpdateFilter<TestModel>>({ $set: { 'numberArray.$': 40 } });
210+
expectAssignable<UpdateFilter<TestModel>>({ $set: { 'numberArray.$[bla]': 40 } });
211+
expectAssignable<UpdateFilter<TestModel>>({ $set: { 'numberArray.$[]': 1000.2 } });
212+
expectAssignable<UpdateFilter<TestModel>>({ $set: { 'subInterfaceArray.$.field3': 40 } });
213+
expectAssignable<UpdateFilter<TestModel>>({ $set: { 'subInterfaceArray.$[bla].field3': 40 } });
214+
expectAssignable<UpdateFilter<TestModel>>({ $set: { 'subInterfaceArray.$[].field3': 1000.2 } });
215+
expectError(buildUpdateFilter({ $set: { 'numberArray.$': '20' } }));
199216

200217
expectAssignable<UpdateFilter<TestModel>>({ $setOnInsert: { numberField: 1 } });
201218
expectAssignable<UpdateFilter<TestModel>>({
@@ -206,10 +223,19 @@ expectAssignable<UpdateFilter<TestModel>>({ $setOnInsert: { int32Field: new Int3
206223
expectAssignable<UpdateFilter<TestModel>>({ $setOnInsert: { longField: Long.fromString('999') } });
207224
expectAssignable<UpdateFilter<TestModel>>({ $setOnInsert: { stringField: 'a' } });
208225
expectError(buildUpdateFilter({ $setOnInsert: { stringField: 123 } }));
209-
expectAssignable<UpdateFilter<TestModel>>({ $setOnInsert: { 'dot.notation': 2 } });
210-
expectAssignable<UpdateFilter<TestModel>>({ $setOnInsert: { 'subInterfaceArray.$': -10 } });
211-
expectAssignable<UpdateFilter<TestModel>>({ $setOnInsert: { 'subInterfaceArray.$[bla]': 40 } });
212-
expectAssignable<UpdateFilter<TestModel>>({ $setOnInsert: { 'subInterfaceArray.$[]': 1000.2 } });
226+
expectAssignable<UpdateFilter<TestModel>>({ $setOnInsert: { 'subInterfaceField.field1': '2' } });
227+
expectError(buildUpdateFilter({ $setOnInsert: { 'subInterfaceField.field2': 2 } }));
228+
expectError(buildUpdateFilter({ $setOnInsert: { 'unknown.field': null } }));
229+
expectAssignable<UpdateFilter<TestModel>>({ $setOnInsert: { 'numberArray.$': 40 } });
230+
expectAssignable<UpdateFilter<TestModel>>({ $setOnInsert: { 'numberArray.$[bla]': 40 } });
231+
expectAssignable<UpdateFilter<TestModel>>({ $setOnInsert: { 'numberArray.$[]': 1000.2 } });
232+
expectAssignable<UpdateFilter<TestModel>>({ $setOnInsert: { 'subInterfaceArray.$.field3': 40 } });
233+
expectAssignable<UpdateFilter<TestModel>>({
234+
$setOnInsert: { 'subInterfaceArray.$[bla].field3': 40 }
235+
});
236+
expectAssignable<UpdateFilter<TestModel>>({
237+
$ssetOnInsert: { 'subInterfaceArray.$[].field3': 1000.2 }
238+
});
213239

214240
expectAssignable<UpdateFilter<TestModel>>({ $unset: { numberField: '' } });
215241
expectAssignable<UpdateFilter<TestModel>>({ $unset: { decimal128Field: '' } });

0 commit comments

Comments
 (0)