Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
43 changes: 26 additions & 17 deletions src/mongo_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export type WithoutId<TSchema> = Omit<TSchema, '_id'>;
export type Filter<TSchema> =
| Partial<TSchema>
| ({
[Property in Join<NestedPaths<WithId<TSchema>>, '.'>]?: Condition<
[Property in Join<NestedPaths<WithId<TSchema>, []>, '.'>]?: Condition<
PropertyType<WithId<TSchema>, Property>
>;
} & RootFilterOperators<WithId<TSchema>>);
Expand Down Expand Up @@ -263,7 +263,7 @@ export type OnlyFieldsOfType<TSchema, FieldType = any, AssignableType = FieldTyp
/** @public */
export type MatchKeysAndValues<TSchema> = Readonly<
{
[Property in Join<NestedPaths<TSchema>, '.'>]?: PropertyType<TSchema, Property>;
[Property in Join<NestedPaths<TSchema, []>, '.'>]?: PropertyType<TSchema, Property>;
} & {
[Property in `${NestedPathsOfType<TSchema, any[]>}.$${`[${string}]` | ''}`]?: ArrayElement<
PropertyType<TSchema, Property extends `${infer Key}.$${string}` ? Key : never>
Expand All @@ -272,7 +272,7 @@ export type MatchKeysAndValues<TSchema> = Readonly<
[Property in `${NestedPathsOfType<TSchema, Record<string, any>[]>}.$${
| `[${string}]`
| ''}.${string}`]?: any; // Could be further narrowed
}
} & Document
>;

/** @public */
Expand Down Expand Up @@ -498,20 +498,29 @@ export type PropertyType<Type, Property extends string> = string extends Propert
* @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/
*
* @remarks
* Through testing we determined that a depth of 8 is safe for the typescript compiler
* and provides reasonable compilation times. This number is otherwise not special and
* should be changed if issues are found with this level of checking. Beyond this
* depth any helpers that make use of NestedPaths should devolve to not asserting any
* type safety on the input.
*/
export type NestedPaths<Type> = Type extends
| string
| number
| boolean
| Date
| RegExp
| Buffer
| Uint8Array
| ((...args: any[]) => any)
| { _bsontype: string }
export type NestedPaths<Type, Depth extends number[]> = Depth['length'] extends 8
? []
: Type extends
| string
| number
| boolean
| Date
| RegExp
| Buffer
| Uint8Array
| ((...args: any[]) => any)
| { _bsontype: string }
? []
: Type extends ReadonlyArray<infer ArrayType>
? [] | [number, ...NestedPaths<ArrayType>]
? [] | [number, ...NestedPaths<ArrayType, [...Depth, 1]>]
: Type extends Map<string, any>
? [string]
: Type extends object
Expand All @@ -529,9 +538,9 @@ export type NestedPaths<Type> = Type extends
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]>]
[Key, ...NestedPaths<Type[Key], [...Depth, 1]>]
: // child is not structured the same as the parent
[Key, ...NestedPaths<Type[Key]>] | [Key];
[Key, ...NestedPaths<Type[Key], [...Depth, 1]>] | [Key];
}[Extract<keyof Type, string>]
: [];

Expand All @@ -542,7 +551,7 @@ export type NestedPaths<Type> = Type extends
*/
export type NestedPathsOfType<TSchema, Type> = KeysOfAType<
{
[Property in Join<NestedPaths<TSchema>, '.'>]: PropertyType<TSchema, Property>;
[Property in Join<NestedPaths<TSchema, []>, '.'>]: PropertyType<TSchema, Property>;
},
Type
>;
4 changes: 2 additions & 2 deletions test/types/basic_schema.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expectAssignable, expectNotAssignable, expectNotType, expectType } from 'tsd';

import { ObjectId } from '../../src/bson';
import { Document, ObjectId } from '../../src/bson';
import { Collection } from '../../src/collection';
import { Db } from '../../src/db';
import { MongoClient } from '../../src/mongo_client';
Expand All @@ -20,7 +20,7 @@ expectType<Collection<ACounterWithId>>(new Collection<ACounterWithId>(db, ''));
////////////////////////////////////////////////////////////////////////////////////////////////////
// Simple Schema that does not define an _id
// With _id
type InsertOneArgOf<S> = Parameters<Collection<S>['insertOne']>[0];
type InsertOneArgOf<S extends Document> = Parameters<Collection<S>['insertOne']>[0];
expectAssignable<InsertOneArgOf<ACounter>>({ _id: new ObjectId(), a: 3 });
// Without _id
expectAssignable<InsertOneArgOf<ACounter>>({ a: 3 });
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,70 @@
import { expectError } from 'tsd';
import { expectAssignable, expectError, expectNotAssignable, expectNotType } from 'tsd';

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

/**
* mutually recursive types are not supported and will not get type safety
*/
interface A {
b: B;
interface Author {
name: string;
favoritePublication: Book;
}

interface B {
a: A;
interface Book {
title: string;
author: Author;
}

declare const mutuallyRecursive: Collection<A>;
//@ts-expect-error
mutuallyRecursive.find({});
mutuallyRecursive.find({
b: {}
expectAssignable<Filter<Author>>({
favoritePublication: {
title: 'book title',
author: {
name: 'author name'
}
}
});
expectNotType<UpdateFilter<Author>>({
$set: {
favoritePublication: {
title: 'a title',
published: new Date(),
author: {
name: 23
}
}
}
});

// Extremely deep type checking for recursive schemas
expectNotAssignable<Filter<Author>>({
'favoritePublication.author.favoritePublication.author.favoritePublication.author.favoritePublication.title': 23
});
expectAssignable<Filter<Author>>({
'favoritePublication.author.favoritePublication.author.favoritePublication.author.favoritePublication.author.favoritePublication.title':
'good soup'
});
expectNotAssignable<Filter<Author>>({
'favoritePublication.author.favoritePublication.author.favoritePublication.author.name': 23
});

// Beyond the depth of 10, `extends Document` permits anything (number for name is permitted)
expectAssignable<Filter<Author>>({
'favoritePublication.author.favoritePublication.author.favoritePublication.author.favoritePublication.author.favoritePublication.author.name': 23
});

// Update filter has similar depth limit
expectAssignable<UpdateFilter<Author>>({
$set: {
'favoritePublication.author.favoritePublication.author.favoritePublication.author.favoritePublication.author.name':
'joe'
}
});

// Depth 7 or below is type checked
expectNotAssignable<UpdateFilter<Author>>({
$set: {
'favoritePublication.author.favoritePublication.author.favoritePublication.author.name': 3
}
});

/**
Expand Down Expand Up @@ -173,3 +220,41 @@ recursiveSchemaWithArray.findOne({
name: 3
}
});

// Modeling A -> B -> C -> D -> A recursive type
type A = {
name: string;
b: B;
};

type B = {
name: string;
c: C;
};

type C = {
name: string;
d: D;
};

type D = {
name: string;
a: A;
};

expectAssignable<Filter<A>>({
'b.c.d.a.b.c.d.a.b.name': 'a'
});

// Beyond the depth supported, there is no type checking
expectAssignable<Filter<A>>({
'b.c.d.a.b.c.d.a.b.c.name': 3
});

expectAssignable<UpdateFilter<A>>({
$set: { 'b.c.d.a.b.c.d.a.b.name': 'a' }
});

expectAssignable<UpdateFilter<A>>({
$set: { 'b.c.d.a.b.c.d.a.b.c.name': 'a' }
});
10 changes: 8 additions & 2 deletions test/types/community/collection/updateX.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,10 @@ expectError<UpdateFilter<TestModel>>({
$set: { 'subInterfaceField.nestedObject': { a: 1, b: '2' } }
});
expectError(buildUpdateFilter({ $set: { 'subInterfaceField.field2': 2 } }));
expectError(buildUpdateFilter({ $set: { 'unknown.field': null } }));

// NODE-3875 introduced intersection with Document to the MatchKeysAndValues so this no longer errors
expectAssignable<UpdateFilter<TestModel>>({ $set: { 'unknown.field': null } });

expectAssignable<UpdateFilter<TestModel>>({ $set: { 'numberArray.$': 40 } });
expectAssignable<UpdateFilter<TestModel>>({ $set: { 'numberArray.$[bla]': 40 } });
expectAssignable<UpdateFilter<TestModel>>({ $set: { 'numberArray.$[]': 1000.2 } });
Expand All @@ -241,7 +244,10 @@ expectAssignable<UpdateFilter<TestModel>>({ $setOnInsert: { stringField: 'a' } }
expectError(buildUpdateFilter({ $setOnInsert: { stringField: 123 } }));
expectAssignable<UpdateFilter<TestModel>>({ $setOnInsert: { 'subInterfaceField.field1': '2' } });
expectError(buildUpdateFilter({ $setOnInsert: { 'subInterfaceField.field2': 2 } }));
expectError(buildUpdateFilter({ $setOnInsert: { 'unknown.field': null } }));

// NODE-3875 introduced intersection with Document to the MatchKeysAndValues so this no longer errors
expectAssignable<UpdateFilter<TestModel>>({ $setOnInsert: { 'unknown.field': null } });

expectAssignable<UpdateFilter<TestModel>>({ $setOnInsert: { 'numberArray.$': 40 } });
expectAssignable<UpdateFilter<TestModel>>({ $setOnInsert: { 'numberArray.$[bla]': 40 } });
expectAssignable<UpdateFilter<TestModel>>({ $setOnInsert: { 'numberArray.$[]': 1000.2 } });
Expand Down
Loading