diff --git a/src/collection.ts b/src/collection.ts index 73be2759e92..86429cbabbc 100644 --- a/src/collection.ts +++ b/src/collection.ts @@ -102,7 +102,7 @@ import { WriteConcern, WriteConcernOptions } from './write_concern'; /** @public */ export interface ModifyResult { - value: TSchema | null; + value: WithId | null; lastErrorObject?: Document; ok: 0 | 1; } diff --git a/src/mongo_types.ts b/src/mongo_types.ts index 60564a87cf5..83bf11ad6a5 100644 --- a/src/mongo_types.ts +++ b/src/mongo_types.ts @@ -65,11 +65,13 @@ export type EnhancedOmit = string extends keyof TRecor export type WithoutId = Omit; /** A MongoDB filter can be some portion of the schema or a set of operators @public */ -export type Filter = { - [Property in Join>, '.'>]?: Condition< - PropertyType, Property> - >; -} & RootFilterOperators>; +export type Filter = + | Partial + | ({ + [Property in Join>, '.'>]?: Condition< + PropertyType, Property> + >; + } & RootFilterOperators>); /** @public */ export type Condition = AlternativeType | FilterOperators>; @@ -477,8 +479,11 @@ export type PropertyType = string extends Propert : unknown : unknown; -// We dont't support nested circular references -/** @public */ +/** + * @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 extends | string | number @@ -497,6 +502,21 @@ export type NestedPaths = Type extends : // eslint-disable-next-line @typescript-eslint/ban-types Type extends object ? { - [Key in Extract]: [Key, ...NestedPaths]; + [Key in Extract]: 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 // 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] + : // child is not structured the same as the parent + [Key, ...NestedPaths]; }[Extract] : []; diff --git a/test/types/community/collection/findX-recursive-types.test-d.ts b/test/types/community/collection/findX-recursive-types.test-d.ts new file mode 100644 index 00000000000..96c4e095cce --- /dev/null +++ b/test/types/community/collection/findX-recursive-types.test-d.ts @@ -0,0 +1,175 @@ +import { expectError } from 'tsd'; + +import type { Collection } from '../../../../src'; + +/** + * mutually recursive types are not supported and will not get type safety + */ +interface A { + b: B; +} + +interface B { + a: A; +} + +declare const mutuallyRecursive: Collection; +//@ts-expect-error +mutuallyRecursive.find({}); +mutuallyRecursive.find({ + b: {} +}); + +/** + * types that are not recursive in name but are recursive in structure are + * still supported + */ +interface RecursiveButNotReally { + a: { a: number; b: string }; + b: string; +} + +declare const recursiveButNotReallyCollection: Collection; +expectError( + recursiveButNotReallyCollection.find({ + 'a.a': 'asdf' + }) +); +recursiveButNotReallyCollection.find({ + 'a.a': 2 +}); + +/** + * recursive schemas are now supported, but with limited type checking support + */ +interface RecursiveSchema { + name: RecursiveSchema; + age: number; +} + +declare const recursiveCollection: Collection; +recursiveCollection.find({ + name: { + name: { + age: 23 + } + } +}); + +recursiveCollection.find({ + age: 23 +}); + +/** + * Recursive optional schemas are also supported with the same capabilities as + * standard recursive schemas + */ +interface RecursiveOptionalSchema { + name?: RecursiveOptionalSchema; + age: number; +} + +declare const recursiveOptionalCollection: Collection; + +recursiveOptionalCollection.find({ + name: { + name: { + age: 23 + } + } +}); + +recursiveOptionalCollection.find({ + age: 23 +}); + +/** + * recursive union types are supported + */ +interface Node { + next: Node | null; +} + +declare const nodeCollection: Collection; + +nodeCollection.find({ + next: null +}); + +expectError( + nodeCollection.find({ + next: 'asdf' + }) +); + +nodeCollection.find({ + 'next.next': 'asdf' +}); + +nodeCollection.find({ 'next.next.next': 'yoohoo' }); + +/** + * Recursive schemas with arrays are also supported + */ +interface MongoStrings { + projectId: number; + branches: Branch[]; + twoLevelsDeep: { + name: string; + }; +} + +interface Branch { + id: number; + name: string; + title?: string; + directories: Directory[]; +} + +interface Directory { + id: number; + name: string; + title?: string; + branchId: number; + files: (number | Directory)[]; +} + +declare const recursiveSchemaWithArray: Collection; +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 +recursiveSchemaWithArray.findOne({ + 'branches.0.directories.0.files.0.id': 'hello' +}); + +recursiveSchemaWithArray.findOne({ + branches: [ + { + id: 'asdf' + } + ] +}); + +// type inference works on properties but only at the top level +expectError( + recursiveSchemaWithArray.findOne({ + projectId: 'asdf' + }) +); + +recursiveSchemaWithArray.findOne({ + twoLevelsDeep: { + name: 3 + } +}); diff --git a/test/types/community/collection/findX.test-d.ts b/test/types/community/collection/findX.test-d.ts index 1310f1bf796..463ba1518e7 100644 --- a/test/types/community/collection/findX.test-d.ts +++ b/test/types/community/collection/findX.test-d.ts @@ -1,6 +1,6 @@ import { expectAssignable, expectNotType, expectType } from 'tsd'; -import type { Projection, ProjectionOperators } from '../../../../src'; +import type { Filter, Projection, ProjectionOperators } from '../../../../src'; import { Collection, Db, @@ -300,3 +300,51 @@ expectAssignable(await schemaWithUserDefinedId.f // should allow _id as a number await schemaWithUserDefinedId.findOne({ _id: 5 }); await schemaWithUserDefinedId.find({ _id: 5 }); + +// We should be able to use a doc of type T as a filter object when performing findX operations +interface Foo { + a: string; +} + +const fooObj: Foo = { + a: 'john doe' +}; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const fooFilter: Filter = fooObj; + +// Specifically test that arrays can be included as a part of an object +// ensuring that a bug reported in https://jira.mongodb.org/browse/NODE-3856 is addressed +interface FooWithArray { + a: number[]; +} + +const fooObjWithArray: FooWithArray = { + a: [1, 2, 3, 4] +}; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const fooFilterWithArray: Filter = fooObjWithArray; + +declare const coll: Collection<{ a: number; b: string }>; +expectType | null>((await coll.findOneAndDelete({ a: 3 })).value); +expectType | null>( + (await coll.findOneAndReplace({ a: 3 }, { a: 5, b: 'new string' })).value +); +expectType | null>( + ( + await coll.findOneAndUpdate( + { a: 3 }, + { + $set: { + a: 5 + } + } + ) + ).value +); + +// projections do not change the return type - our typing doesn't support this +expectType | null>( + (await coll.findOneAndDelete({ a: 3 }, { projection: { _id: 0 } })).value +);