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

fix(NODE-4513): type for nested objects in query & update #3349

Merged
merged 5 commits into from
Aug 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,6 @@ export type {
KeysOfOtherType,
MatchKeysAndValues,
NestedPaths,
NestedPathsOfType,
NonObjectIdLikeDocument,
NotAcceptedFields,
NumericType,
Expand Down
158 changes: 78 additions & 80 deletions src/mongo_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ export type WithoutId<TSchema> = Omit<TSchema, '_id'>;
export type Filter<TSchema> =
| Partial<TSchema>
| ({
[Property in Join<NestedPaths<WithId<TSchema>>, '.'>]?: Condition<
PropertyType<WithId<TSchema>, Property>
[Property in Join<NestedPaths<WithId<TSchema>, true>, '.'>]?: Condition<
PropertyType<WithId<TSchema>, Property, true>
>;
} & RootFilterOperators<WithId<TSchema>>);

Expand Down Expand Up @@ -261,19 +261,9 @@ 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
}
>;
export type MatchKeysAndValues<TSchema> = Readonly<{
[Property in Join<NestedPaths<TSchema, false>, '.'>]?: PropertyType<TSchema, Property, false>;
}>;

/** @public */
export type AddToSetOperators<Type> = {
Expand Down Expand Up @@ -474,75 +464,83 @@ export type Join<T extends unknown[], D extends string> = T extends []
: string;

/** @public */
export type PropertyType<Type, Property extends string> = string extends Property
? unknown
: Property extends keyof Type
? Type[Property]
: Property extends `${number}`
? Type extends ReadonlyArray<infer ArrayType>
? ArrayType
: unknown
: Property extends `${infer Key}.${infer Rest}`
? Key extends `${number}`
? Type extends ReadonlyArray<infer ArrayType>
? PropertyType<ArrayType, Rest>
: unknown
: Key extends keyof Type
? Type[Key] extends Map<string, infer MapType>
export type PropertyType<
Type,
Property extends string,
AllowToSkipArrayIndex extends boolean
> = Type extends unknown
? string extends Property
? Type extends Map<string, infer MapType>
? MapType
: PropertyType<Type[Key], Rest>
: unknown
: unknown;
: never
:
| (AllowToSkipArrayIndex extends false
? never
: Type extends ReadonlyArray<infer ArrayType>
? PropertyType<ArrayType, Property, AllowToSkipArrayIndex>
: never)
| (Property extends keyof Type
? Type[Property]
: Property extends `${number | `$${'' | `[${string}]`}`}`
? Type extends ReadonlyArray<infer ArrayType>
? ArrayType
: never
: Property extends `${infer Key}.${infer Rest}`
? Key extends `${number | `$${'' | `[${string}]`}`}`
? Type extends ReadonlyArray<infer ArrayType>
? PropertyType<ArrayType, Rest, AllowToSkipArrayIndex>
: never
: Key extends keyof Type
? PropertyType<Type[Key], Rest, AllowToSkipArrayIndex>
: never
: never)
: never;

/**
* @public
* returns tuple of strings (keys to be joined on '.') that represent every path into a schema
* https://docs.mongodb.com/manual/tutorial/query-embedded-documents/
*/
export type NestedPaths<Type> = Type extends
| string
| number
| boolean
| Date
| RegExp
| Buffer
| Uint8Array
| ((...args: any[]) => any)
| { _bsontype: string }
? []
: Type extends ReadonlyArray<infer ArrayType>
? [] | [number, ...NestedPaths<ArrayType>]
: Type extends Map<string, any>
? [string]
: Type extends object
? {
[Key in Extract<keyof Type, string>]: Type[Key] extends Type // type of value extends the parent
? [Key]
: // for a recursive union type, the child will never extend the parent type.
// but the parent will still extend the child
Type extends Type[Key]
? [Key]
: Type[Key] extends ReadonlyArray<infer ArrayType> // handling recursive types with arrays
? Type extends ArrayType // is the type of the parent the same as the type of the array?
? [Key] // yes, it's a recursive array type
: // for unions, the child type extends the parent
ArrayType extends Type
? [Key] // we have a recursive array union
: // child is an array, but it's not a recursive array
[Key, ...NestedPaths<Type[Key]>]
: // child is not structured the same as the parent
[Key, ...NestedPaths<Type[Key]>] | [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
>;
export type NestedPaths<Type, AllowToSkipArrayIndex extends boolean> = Type extends unknown
? Type extends
| string
| number
| boolean
| Date
| RegExp
| Buffer
| Uint8Array
| ((...args: any[]) => any)
| { _bsontype: string }
? never
: Type extends ReadonlyArray<infer ArrayType>
? [
...(
| (AllowToSkipArrayIndex extends true ? [] : never)
| [number | `$${'' | `[${string}]`}`]
),
...([] | NestedPaths<ArrayType, AllowToSkipArrayIndex>)
]
: Type extends Map<string, any>
? [string]
: Type extends object
? {
[Key in Extract<keyof Type, string>]: Type[Key] extends Type // type of value extends the parent
? [Key]
: // for a recursive union type, the child will never extend the parent type.
// but the parent will still extend the child
Type extends Type[Key]
? [Key]
: Type[Key] extends ReadonlyArray<infer ArrayType> // handling recursive types with arrays
? Type extends ArrayType // is the type of the parent the same as the type of the array?
? [Key] // yes, it's a recursive array type
: // for unions, the child type extends the parent
ArrayType extends Type
? [Key] // we have a recursive array union
: // child is an array, but it's not a recursive array
[Key, ...([] | NestedPaths<Type[Key], AllowToSkipArrayIndex>)]
: // child is not structured the same as the parent
[Key, ...([] | NestedPaths<Type[Key], AllowToSkipArrayIndex>)];
}[Extract<keyof Type, string>]
: never
: never;
35 changes: 34 additions & 1 deletion test/types/community/collection/filterQuery.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
} from 'bson';
import { expectAssignable, expectError, expectNotType, expectType } from 'tsd';

import { Collection, Filter, MongoClient, WithId } from '../../../../src';
import { Collection, Filter, MongoClient, UpdateFilter, WithId } from '../../../../src';

/**
* test the Filter type using collection.find<T>() method
Expand Down Expand Up @@ -406,3 +406,36 @@ nonSpecifiedCollection.find({
hello: 'world'
}
});

// NODE-4513: improves support for union types and array operators
type MyArraySchema = {
nested: { array: { a: number; b: boolean }[] };
something: { a: number } | { b: boolean };
};

// "element" now refers to the name used in arrayFilters, it can be any string
expectAssignable<UpdateFilter<MyArraySchema>>({
$set: { 'nested.array.$[element]': { a: 2, b: false } }
});
expectAssignable<Filter<MyArraySchema>>({
$set: { 'nested.array.$[element]': { a: 2, b: false } }
});

// Specifying an identifier in the brackets is optional
expectAssignable<UpdateFilter<MyArraySchema>>({
$set: { 'nested.array.$[].a': 2 }
});
expectAssignable<Filter<MyArraySchema>>({
$set: { 'nested.array.$[].a': 2 }
});

// Union usage examples
expectAssignable<Filter<MyArraySchema>>({
'something.a': 2
});
expectError<Filter<MyArraySchema>>({
'something.a': false
});
expectAssignable<Filter<MyArraySchema>>({
'something.b': false
});