diff --git a/test/types/models.test.ts b/test/types/models.test.ts index 88cff531bc..35dca81ebe 100644 --- a/test/types/models.test.ts +++ b/test/types/models.test.ts @@ -225,10 +225,6 @@ function find() { Project.find({}, { name: 1 }); Project.find({}, { name: 0 }); - // filter + callback - Project.find({}, (error: CallbackError, result: IProject[]) => console.log(error, result)); - Project.find({ name: 'Hello' }, (error: CallbackError, result: IProject[]) => console.log(error, result)); - // filter + projection + options Project.find({}, undefined, { limit: 5 }); Project.find({}, null, { limit: 5 }); diff --git a/test/types/queries.test.ts b/test/types/queries.test.ts index 35ff6f24d6..83c4eb9288 100644 --- a/test/types/queries.test.ts +++ b/test/types/queries.test.ts @@ -15,12 +15,12 @@ import { QuerySelector, InferSchemaType, ProjectionFields, - QueryOptions + QueryOptions, + ProjectionType } from 'mongoose'; import { ModifyResult, ObjectId } from 'mongodb'; import { expectAssignable, expectError, expectNotAssignable, expectType } from 'tsd'; import { autoTypedModel } from './models.test'; -import { AutoTypedSchemaType } from './schema.test'; interface QueryHelpers { _byName(this: QueryWithHelpers, name: string): QueryWithHelpers, ITest, QueryHelpers>; @@ -54,6 +54,9 @@ interface ISubdoc { myId?: Types.ObjectId; id?: number; tags?: string[]; + profiles: { + name?: string + } } interface ITest { @@ -93,16 +96,6 @@ Test.find({ name: ['Test1', 'Test2'] }).exec(); // Implicit `$in` for regex string Test.find({ name: [/Test1/, /Test2/] }); -Test.find({ name: 'test' }, (err: Error | null, docs: ITest[]) => { - console.log(!!err, docs[0].age); -}); - -Test.findOne({ name: 'test' }, (err: Error | null, doc: ITest | null) => { - if (doc != null) { - console.log(!!err, doc.age); - } -}); - Test.find({ name: { $gte: 'Test' } }, null, { collation: { locale: 'en-us' } }).exec(). then((res: Array) => console.log(res[0].name)); @@ -160,6 +153,43 @@ const p1: Record = Test.find().projection('age docs.id'); const p2: Record | null = Test.find().projection(); const p3: null = Test.find().projection(null); +expectError(Test.find({ }, { name: 'ss' })); // Only 0 and 1 are allowed +expectError(Test.find({ }, { name: 3 })); // Only 0 and 1 are allowed +expectError(Test.find({ }, { name: true, age: false, endDate: true, tags: 1 })); // Exclusion in a inclusion projection is not allowed +expectError(Test.find({ }, { name: true, age: false, endDate: true })); // Inclusion in a exclusion projection is not allowed +expectError(Test.find({ }, { name: false, age: false, tags: false, child: { name: false }, docs: { myId: false, id: true } })); // Inclusion in a exclusion projection is not allowed in nested objects and arrays +expectError(Test.find({ }, { tags: { something: 1 } })); // array of strings or numbers should only be allowed to be a boolean or 1 and 0 +Test.find({}, { name: true, age: true, endDate: true, tags: 1, child: { name: true }, docs: { myId: true, id: true } }); // This should be allowed +Test.find({}, { name: 1, age: 1, endDate: 1, tags: 1, child: { name: 1 }, docs: { myId: 1, id: 1 } }); // This should be allowed +Test.find({}, { _id: 0, name: 1, age: 1, endDate: 1, tags: 1, child: 1, docs: 1 }); // _id is an exception and should be allowed to be excluded +Test.find({}, { name: 0, age: 0, endDate: 0, tags: 0, child: 0, docs: 0 }); // This should be allowed +Test.find({}, { name: 0, age: 0, endDate: 0, tags: 0, child: { name: 0 }, docs: { myId: 0, id: 0 } }); // This should be allowed +Test.find({}, { name: 1, age: 1, _id: 0 }); // This should be allowed since _id is an exception +Test.find({}, { someOtherField: 1 }); // This should be allowed since it's not a field in the schema +expectError(Test.find({}, { name: { $slice: 1 } })); // $slice should only be allowed on arrays +Test.find({}, { tags: { $slice: 1 } }); // $slice should be allowed on arrays +Test.find({}, { tags: { $slice: [1, 2] } }); // $slice with the format of [ , ] should also be allowed on arrays +expectError(Test.find({}, { age: { $elemMatch: {} } })); // $elemMatch should not be allowed on non arrays +Test.find({}, { docs: { $elemMatch: { id: 'aa' } } }); // $elemMatch should be allowed on arrays +expectError(Test.find({}, { tags: { $slice: 1, $elemMatch: {} } })); // $elemMatch and $slice should not be allowed together +Test.find({}, { age: 1, tags: { $slice: 5 } }); // $slice should be allowed in inclusion projection +Test.find({}, { age: 0, tags: { $slice: 5 } }); // $slice should be allowed in exclusion projection +Test.find({}, { age: 1, tags: { $elemMatch: {} } }); // $elemMatch should be allowed in inclusion projection +Test.find({}, { age: 0, tags: { $elemMatch: {} } }); // $elemMatch should be allowed in exclusion projection +expectError(Test.find({}, { 'docs.id': 11 })); // Dot notation should be allowed and does not accept any +expectError(Test.find({}, { docs: { id: '1' } })); // Dot notation should be able to use a combination with objects +Test.find({}, { docs: { id: false } }); // Dot notation should be allowed with valid values - should correctly handle arrays +Test.find({}, { docs: { id: true } }); // Dot notation should be allowed with valid values - should correctly handle arrays +Test.find({ docs: { $elemMatch: { id: 1 } } }, { 'docs.$': 1 }); // $ projection should be allowed +Test.find({}, { child: 1 }); // Dot notation should be able to use a combination with objects +// Test.find({}, { 'docs.profiles': { name: 1 } }); // 3 levels deep not supported +expectError(Test.find({}, { 'docs.profiles': { name: 'aa' } })); // should support a combination of dot notation and objects +expectError(Test.find({}, { endDate: { toString: 1 } })); +expectError(Test.find({}, { tags: { trim: 1 } })); +expectError(Test.find({}, { child: { toJSON: 1 } })); + +// Manual Casting using ProjectionType +Test.find({}, { docs: { unknownParams: 1 } } as ProjectionType); // Sorting Test.find().sort(); Test.find().sort('-name'); diff --git a/types/index.d.ts b/types/index.d.ts index c8d2eaf855..a62ab032cb 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -657,9 +657,37 @@ declare module 'mongoose' { export type ReturnsNewDoc = { new: true } | { returnOriginal: false } | { returnDocument: 'after' }; - export type ProjectionElementType = number | string; - export type ProjectionType = { [P in keyof T]?: ProjectionElementType } | AnyObject | string; - + type ArrayOperators = { $slice: number | [number, number]; $elemMatch?: never } | { $elemMatch: Record; $slice?: never }; + /** + * This Type Assigns `Element | undefined` recursively to the `T` type. + * if it is an array it will do this to the element of the array, if it is an object it will do this for the properties of the object. + * `Element` is the truthy or falsy values that are going to be used as the value of the projection.(1 | true or 0 | false) + * For the elements of the array we will use: `Element | `undefined` | `ArrayOperators` + * @example + * type CalculatedType = Projector<{ a: string, b: number, c: { d: string }, d: string[] }, true> + * type CalculatedType = { + a?: true | undefined; + b?: true | undefined; + c?: true | { + d?: true | undefined; + } | undefined; + d?: true | ArrayOperators | undefined; + } + */ + type Projector = T extends Array + ? Projector | ArrayOperators + : T extends TreatAsPrimitives + ? Element + : T extends Record + ? { + [K in keyof T]?: T[K] extends Record ? Projector | Element : Element; + } + : Element; + type _IDType = { _id?: boolean | 1 | 0 }; + export type InclusionProjection = IsItRecordAndNotAny extends true ? Projector, true | 1> & _IDType : AnyObject; + export type ExclusionProjection = IsItRecordAndNotAny extends true ? Projector, false | 0> & _IDType : AnyObject; + + export type ProjectionType = (InclusionProjection & AnyObject) | (ExclusionProjection & AnyObject) | string; export type SortValues = SortOrder; export type SortOrder = -1 | 1 | 'asc' | 'ascending' | 'desc' | 'descending'; diff --git a/types/query.d.ts b/types/query.d.ts index 020ba181bb..c1f16ea193 100644 --- a/types/query.d.ts +++ b/types/query.d.ts @@ -160,7 +160,7 @@ declare module 'mongoose' { * Set `overwriteImmutable` to `true` to allow updating immutable properties using other update operators. */ overwriteImmutable?: boolean; - projection?: ProjectionType; + projection?: { [P in keyof DocType]?: number | string } | AnyObject | string; /** * if true, returns the full ModifyResult rather than just the document */ diff --git a/types/types.d.ts b/types/types.d.ts index b394525f5b..c29da93be2 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -60,7 +60,7 @@ declare module 'mongoose' { class Decimal128 extends mongodb.Decimal128 { } - class DocumentArray = Types.Subdocument, any, T> & T> extends Types.Array { + class DocumentArray = Types.Subdocument, any, T> & T> extends Types.Array { /** DocumentArray constructor */ constructor(values: AnyObject[]); @@ -85,7 +85,7 @@ declare module 'mongoose' { class ObjectId extends mongodb.ObjectId { } - class Subdocument extends Document { + class Subdocument extends Document { $isSingleNested: true; /** Returns the top level document of this sub-document. */ diff --git a/types/utility.d.ts b/types/utility.d.ts index 145e557161..a39a43262d 100644 --- a/types/utility.d.ts +++ b/types/utility.d.ts @@ -4,20 +4,44 @@ declare module 'mongoose' { type WithLevel1NestedPaths = { [P in K | NestedPaths, K>]: P extends K - ? T[P] + // Handle top-level paths + // First, drill into documents so we don't end up surfacing `$assertPopulated`, etc. + ? Extract, Document> extends never + // If not a document, then return the type. Otherwise, get the DocType. + ? NonNullable + : Extract, Document> extends Document + ? DocType + : never + // Handle nested paths : P extends `${infer Key}.${infer Rest}` ? Key extends keyof T - ? Rest extends keyof NonNullable - ? NonNullable[Rest] - : never + ? T[Key] extends (infer U)[] + ? Rest extends keyof NonNullable + ? NonNullable[Rest] + : never + : Rest extends keyof NonNullable + ? NonNullable[Rest] + : never : never : never; }; type NestedPaths = K extends string - ? T[K] extends Record | null | undefined - ? `${K}.${keyof NonNullable & string}` - : never + ? T[K] extends TreatAsPrimitives + ? never + : Extract, Document> extends never + ? T[K] extends Array + ? U extends Record + ? `${K}.${keyof NonNullable & string}` + : never + : T[K] extends Record | null | undefined + ? `${K}.${keyof NonNullable & string}` + : never + : Extract, Document> extends Document + ? DocType extends Record + ? `${K}.${keyof NonNullable & string}` + : never + : never : never; type WithoutUndefined = T extends undefined ? never : T;