Skip to content

Commit

Permalink
fix(NODE-3852,NODE-3854,NODE-3856): Misc typescript fixes for 4.3.1 (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
baileympearson committed Jan 14, 2022
1 parent 2adc7cd commit dd5195a
Show file tree
Hide file tree
Showing 4 changed files with 253 additions and 10 deletions.
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
36 changes: 28 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,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
@@ -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)[];
}

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
@@ -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
);

0 comments on commit dd5195a

Please sign in to comment.