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-3852,NODE-3854,NODE-3856): Misc typescript fixes for 4.3.1 #3102

Merged
merged 14 commits into from
Jan 14, 2022
Merged
2 changes: 1 addition & 1 deletion src/collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ import { WriteConcern, WriteConcernOptions } from './write_concern';

/** @public */
export interface ModifyResult<TSchema = Document> {
value: TSchema | null;
value: WithId<TSchema> | null;
lastErrorObject?: Document;
ok: 0 | 1;
}
Expand Down
36 changes: 28 additions & 8 deletions src/mongo_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,13 @@ export type EnhancedOmit<TRecordOrUnion, KeyUnion> = string extends keyof TRecor
export type WithoutId<TSchema> = Omit<TSchema, '_id'>;

/** A MongoDB filter can be some portion of the schema or a set of operators @public */
export type Filter<TSchema> = {
[Property in Join<NestedPaths<WithId<TSchema>>, '.'>]?: Condition<
PropertyType<WithId<TSchema>, Property>
>;
} & RootFilterOperators<WithId<TSchema>>;
export type Filter<TSchema> =
| Partial<TSchema>
| ({
[Property in Join<NestedPaths<WithId<TSchema>>, '.'>]?: Condition<
PropertyType<WithId<TSchema>, Property>
>;
} & RootFilterOperators<WithId<TSchema>>);

/** @public */
export type Condition<T> = AlternativeType<T> | FilterOperators<AlternativeType<T>>;
Expand Down Expand Up @@ -477,8 +479,11 @@ export type PropertyType<Type, Property extends string> = 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> = Type extends
| string
| number
Expand All @@ -497,6 +502,21 @@ export type NestedPaths<Type> = Type extends
: // eslint-disable-next-line @typescript-eslint/ban-types
Type extends object
? {
[Key in Extract<keyof Type, string>]: [Key, ...NestedPaths<Type[Key]>];
[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]>];
}[Extract<keyof Type, string>]
: [];
175 changes: 175 additions & 0 deletions test/types/community/collection/findX-recursive-types.test-d.ts
Original file line number Diff line number Diff line change
@@ -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<A>;
//@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<RecursiveButNotReally>;
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<RecursiveSchema>;
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<RecursiveOptionalSchema>;

recursiveOptionalCollection.find({
name: {
name: {
age: 23
}
}
});

recursiveOptionalCollection.find({
age: 23
});

/**
* recursive union types are supported
*/
interface Node {
next: Node | null;
}

declare const nodeCollection: Collection<Node>;

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)[];
dariakp marked this conversation as resolved.
Show resolved Hide resolved
}

declare const recursiveSchemaWithArray: Collection<MongoStrings>;
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
}
});
50 changes: 49 additions & 1 deletion test/types/community/collection/findX.test-d.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -300,3 +300,51 @@ expectAssignable<SchemaWithUserDefinedId | null>(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<Foo> = 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<FooWithArray> = fooObjWithArray;

declare const coll: Collection<{ a: number; b: string }>;
expectType<WithId<{ a: number; b: string }> | null>((await coll.findOneAndDelete({ a: 3 })).value);
expectType<WithId<{ a: number; b: string }> | null>(
(await coll.findOneAndReplace({ a: 3 }, { a: 5, b: 'new string' })).value
);
expectType<WithId<{ a: number; b: string }> | null>(
(
await coll.findOneAndUpdate(
{ a: 3 },
{
$set: {
a: 5
}
}
)
).value
);

// projections do not change the return type - our typing doesn't support this
expectType<WithId<{ a: number; b: string }> | null>(
(await coll.findOneAndDelete({ a: 3 }, { projection: { _id: 0 } })).value
);