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
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
31 changes: 23 additions & 8 deletions src/mongo_types.ts
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,16 @@ 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
baileympearson marked this conversation as resolved.
Show resolved Hide resolved
? [Key]
: Type extends Type[Key]
? [Key]
: Type[Key] extends ReadonlyArray<infer ArrayType>
? Type extends ArrayType
? [Key]
: ArrayType extends Type
? [Key]
: [Key, ...NestedPaths<Type[Key]>]
: [Key, ...NestedPaths<Type[Key]>];
}[Extract<keyof Type, string>]
: [];
175 changes: 175 additions & 0 deletions 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<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
}
});
52 changes: 50 additions & 2 deletions test/types/community/collection/findX.test-d.ts
@@ -1,6 +1,6 @@
import { expectAssignable, expectNotType, expectType } from 'tsd';
import { expectAssignable, expectError, 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
);