Skip to content

Commit

Permalink
feat(NODE-4267): support nested fields in type completion for UpdateF…
Browse files Browse the repository at this point in the history
…ilter (#3259)

Co-authored-by: Julien Chaumond <julien@huggingface.co>
  • Loading branch information
coyotte508 and julien-c committed Jun 17, 2022
1 parent 904252c commit 1a9a44c
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 20 deletions.
2 changes: 2 additions & 0 deletions src/index.ts
Expand Up @@ -305,6 +305,7 @@ export type {
AcceptedFields,
AddToSetOperators,
AlternativeType,
ArrayElement,
ArrayOperator,
BitwiseFilter,
BSONTypeAlias,
Expand All @@ -322,6 +323,7 @@ export type {
KeysOfOtherType,
MatchKeysAndValues,
NestedPaths,
NestedPathsOfType,
NonObjectIdLikeDocument,
NotAcceptedFields,
NumericType,
Expand Down
29 changes: 28 additions & 1 deletion src/mongo_types.ts
Expand Up @@ -214,6 +214,9 @@ export type IsAny<Type, ResultIfAny, ResultIfNotAny> = true extends false & Type
/** @public */
export type Flatten<Type> = Type extends ReadonlyArray<infer Item> ? Item : Type;

/** @public */
export type ArrayElement<Type> = Type extends ReadonlyArray<infer Item> ? Item : never;

/** @public */
export type SchemaMember<T, V> = { [P in keyof T]?: V } | { [key: string]: V };

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

/** @public */
export type MatchKeysAndValues<TSchema> = Readonly<Partial<TSchema>> & Record<string, any>;
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
}
>;

/** @public */
export type AddToSetOperators<Type> = {
Expand Down Expand Up @@ -520,3 +535,15 @@ export type NestedPaths<Type> = Type extends
[Key, ...NestedPaths<Type[Key]>];
}[Extract<keyof Type, string>]
: [];

/**
* @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<TSchema, Type> = KeysOfAType<
{
[Property in Join<NestedPaths<TSchema>, '.'>]: PropertyType<TSchema, Property>;
},
Type
>;
4 changes: 2 additions & 2 deletions test/types/community/collection/bulkWrite.test-d.ts
Expand Up @@ -74,7 +74,7 @@ collectionType.bulkWrite([
update: {
$set: {
numberField: 123,
'dot.notation': true
'subInterfaceField.field1': 'true'
}
}
}
Expand Down Expand Up @@ -123,7 +123,7 @@ collectionType.bulkWrite([
update: {
$set: {
numberField: 123,
'dot.notation': true
'subInterfaceField.field2': 'true'
}
}
}
Expand Down
60 changes: 43 additions & 17 deletions test/types/community/collection/updateX.test-d.ts
Expand Up @@ -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<MatchKeysAndValues<{ a: number; b: string }>>({ a: 2, 'dot.notation': true });
expectAssignable<MatchKeysAndValues<{ a: number; b: string; c: { d: boolean } }>>({
a: 2,
'c.d': true
});
expectNotType<MatchKeysAndValues<{ a: number; b: string }>>({ b: 2 });

// AddToSetOperators
Expand Down Expand Up @@ -70,6 +73,7 @@ interface SubTestModel {
_id: ObjectId;
field1: string;
field2?: string;
field3?: number;
}

type FruitTypes = 'apple' | 'pear';
Expand All @@ -78,6 +82,7 @@ type FruitTypes = 'apple' | 'pear';
interface TestModel {
stringField: string;
numberField: number;
numberArray: number[];
decimal128Field: Decimal128;
doubleField: Double;
int32Field: Int32;
Expand Down Expand Up @@ -148,10 +153,13 @@ expectAssignable<UpdateFilter<TestModel>>({ $min: { doubleField: new Double(1.23
expectAssignable<UpdateFilter<TestModel>>({ $min: { int32Field: new Int32(10) } });
expectAssignable<UpdateFilter<TestModel>>({ $min: { longField: Long.fromString('999') } });
expectAssignable<UpdateFilter<TestModel>>({ $min: { stringField: 'a' } });
expectAssignable<UpdateFilter<TestModel>>({ $min: { 'dot.notation': 2 } });
expectAssignable<UpdateFilter<TestModel>>({ $min: { 'subInterfaceArray.$': 'string' } });
expectAssignable<UpdateFilter<TestModel>>({ $min: { 'subInterfaceArray.$[bla]': 40 } });
expectAssignable<UpdateFilter<TestModel>>({ $min: { 'subInterfaceArray.$[]': 1000.2 } });
expectAssignable<UpdateFilter<TestModel>>({ $min: { 'subInterfaceField.field1': '2' } });
expectAssignable<UpdateFilter<TestModel>>({ $min: { 'numberArray.$': 40 } });
expectAssignable<UpdateFilter<TestModel>>({ $min: { 'numberArray.$[bla]': 40 } });
expectAssignable<UpdateFilter<TestModel>>({ $min: { 'numberArray.$[]': 1000.2 } });
expectAssignable<UpdateFilter<TestModel>>({ $min: { 'subInterfaceArray.$.field3': 40 } });
expectAssignable<UpdateFilter<TestModel>>({ $min: { 'subInterfaceArray.$[bla].field3': 40 } });
expectAssignable<UpdateFilter<TestModel>>({ $min: { 'subInterfaceArray.$[].field3': 1000.2 } });

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

Expand All @@ -163,10 +171,13 @@ expectAssignable<UpdateFilter<TestModel>>({ $max: { doubleField: new Double(1.23
expectAssignable<UpdateFilter<TestModel>>({ $max: { int32Field: new Int32(10) } });
expectAssignable<UpdateFilter<TestModel>>({ $max: { longField: Long.fromString('999') } });
expectAssignable<UpdateFilter<TestModel>>({ $max: { stringField: 'a' } });
expectAssignable<UpdateFilter<TestModel>>({ $max: { 'dot.notation': 2 } });
expectAssignable<UpdateFilter<TestModel>>({ $max: { 'subInterfaceArray.$': -10 } });
expectAssignable<UpdateFilter<TestModel>>({ $max: { 'subInterfaceArray.$[bla]': 40 } });
expectAssignable<UpdateFilter<TestModel>>({ $max: { 'subInterfaceArray.$[]': 1000.2 } });
expectAssignable<UpdateFilter<TestModel>>({ $max: { 'subInterfaceField.field1': '2' } });
expectAssignable<UpdateFilter<TestModel>>({ $max: { 'numberArray.$': 40 } });
expectAssignable<UpdateFilter<TestModel>>({ $max: { 'numberArray.$[bla]': 40 } });
expectAssignable<UpdateFilter<TestModel>>({ $max: { 'numberArray.$[]': 1000.2 } });
expectAssignable<UpdateFilter<TestModel>>({ $max: { 'subInterfaceArray.$.field3': 40 } });
expectAssignable<UpdateFilter<TestModel>>({ $max: { 'subInterfaceArray.$[bla].field3': 40 } });
expectAssignable<UpdateFilter<TestModel>>({ $max: { 'subInterfaceArray.$[].field3': 1000.2 } });

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

Expand All @@ -192,10 +203,16 @@ expectAssignable<UpdateFilter<TestModel>>({ $set: { int32Field: new Int32(10) }
expectAssignable<UpdateFilter<TestModel>>({ $set: { longField: Long.fromString('999') } });
expectAssignable<UpdateFilter<TestModel>>({ $set: { stringField: 'a' } });
expectError(buildUpdateFilter({ $set: { stringField: 123 } }));
expectAssignable<UpdateFilter<TestModel>>({ $set: { 'dot.notation': 2 } });
expectAssignable<UpdateFilter<TestModel>>({ $set: { 'subInterfaceArray.$': -10 } });
expectAssignable<UpdateFilter<TestModel>>({ $set: { 'subInterfaceArray.$[bla]': 40 } });
expectAssignable<UpdateFilter<TestModel>>({ $set: { 'subInterfaceArray.$[]': 1000.2 } });
expectAssignable<UpdateFilter<TestModel>>({ $set: { 'subInterfaceField.field2': '2' } });
expectError(buildUpdateFilter({ $set: { 'subInterfaceField.field2': 2 } }));
expectError(buildUpdateFilter({ $set: { 'unknown.field': null } }));
expectAssignable<UpdateFilter<TestModel>>({ $set: { 'numberArray.$': 40 } });
expectAssignable<UpdateFilter<TestModel>>({ $set: { 'numberArray.$[bla]': 40 } });
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 @@ -206,10 +223,19 @@ expectAssignable<UpdateFilter<TestModel>>({ $setOnInsert: { int32Field: new Int3
expectAssignable<UpdateFilter<TestModel>>({ $setOnInsert: { longField: Long.fromString('999') } });
expectAssignable<UpdateFilter<TestModel>>({ $setOnInsert: { stringField: 'a' } });
expectError(buildUpdateFilter({ $setOnInsert: { stringField: 123 } }));
expectAssignable<UpdateFilter<TestModel>>({ $setOnInsert: { 'dot.notation': 2 } });
expectAssignable<UpdateFilter<TestModel>>({ $setOnInsert: { 'subInterfaceArray.$': -10 } });
expectAssignable<UpdateFilter<TestModel>>({ $setOnInsert: { 'subInterfaceArray.$[bla]': 40 } });
expectAssignable<UpdateFilter<TestModel>>({ $setOnInsert: { 'subInterfaceArray.$[]': 1000.2 } });
expectAssignable<UpdateFilter<TestModel>>({ $setOnInsert: { 'subInterfaceField.field1': '2' } });
expectError(buildUpdateFilter({ $setOnInsert: { 'subInterfaceField.field2': 2 } }));
expectError(buildUpdateFilter({ $setOnInsert: { 'unknown.field': null } }));
expectAssignable<UpdateFilter<TestModel>>({ $setOnInsert: { 'numberArray.$': 40 } });
expectAssignable<UpdateFilter<TestModel>>({ $setOnInsert: { 'numberArray.$[bla]': 40 } });
expectAssignable<UpdateFilter<TestModel>>({ $setOnInsert: { 'numberArray.$[]': 1000.2 } });
expectAssignable<UpdateFilter<TestModel>>({ $setOnInsert: { 'subInterfaceArray.$.field3': 40 } });
expectAssignable<UpdateFilter<TestModel>>({
$setOnInsert: { 'subInterfaceArray.$[bla].field3': 40 }
});
expectAssignable<UpdateFilter<TestModel>>({
$ssetOnInsert: { 'subInterfaceArray.$[].field3': 1000.2 }
});

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

0 comments on commit 1a9a44c

Please sign in to comment.