Skip to content

fix: Strict Projection Object Typing #13993

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

Merged
merged 10 commits into from
Nov 24, 2023
15 changes: 14 additions & 1 deletion test/types/queries.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import {
QuerySelector,
InferSchemaType,
ProjectionFields,
QueryOptions
QueryOptions,
ProjectionType
} from 'mongoose';
import { ObjectId } from 'mongodb';
import { expectAssignable, expectError, expectNotAssignable, expectType } from 'tsd';
Expand Down Expand Up @@ -154,6 +155,18 @@ const p1: Record<string, number> = Test.find().projection('age docs.id');
const p2: Record<string, number> | 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
// Sorting
Test.find().sort();
Test.find().sort('-name');
Expand Down
2 changes: 1 addition & 1 deletion test/types/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -760,7 +760,7 @@ function pluginOptions() {
}

const schema = new Schema({});
expectType<Schema<any>>(schema.plugin(pluginFunction)); // test that chaining would be possible
expectType<typeof schema>(schema.plugin(pluginFunction)); // test that chaining would be possible

// could not add strict tests that the parameters are inferred correctly, because i dont know how this would be done in tsd

Expand Down
23 changes: 21 additions & 2 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -588,8 +588,27 @@ declare module 'mongoose' {

export type ReturnsNewDoc = { new: true } | { returnOriginal: false } | { returnDocument: 'after' };

export type ProjectionElementType = number | string;
export type ProjectionType<T> = { [P in keyof T]?: ProjectionElementType } | AnyObject | string;
type Projector<T, Element> = T extends Array<infer U> ? Projector<U, Element> : T extends object ? {
[K in keyof T]?: T[K] extends object ? Projector<T[K], Element> | Element : Element;
} : Element;
type _IDType = { _id?: boolean | 1 | 0 };
type InclusionProjection<T> = NestedPartial<Projector<NestedRequired<T>, true | 1> & _IDType>;
type ExclusionProjection<T> = NestedPartial<Projector<NestedRequired<T>, false | 0> & _IDType>;

type NestedRequired<T> = T extends Array<infer U>
? Array<NestedRequired<U>>
: T extends object
? {
[K in keyof T]-?: NestedRequired<T[K]>;
}
: T;
type NestedPartial<T> = T extends object
? {
[K in keyof T]?: NestedPartial<T[K]>;
}
: T;

export type ProjectionType<T> = (InclusionProjection<T> | ExclusionProjection<T>) & AnyObject | string | ((...agrs: any) => any);

export type SortValues = SortOrder;

Expand Down
4 changes: 2 additions & 2 deletions types/query.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ declare module 'mongoose' {
updatedAt?: boolean;
}

interface QueryOptions<DocType = unknown> extends
interface QueryOptions<DocType = any> extends
PopulateOption,
SessionOption {
arrayFilters?: { [key: string]: any }[];
Expand All @@ -122,7 +122,7 @@ declare module 'mongoose' {
new?: boolean;

overwriteDiscriminatorKey?: boolean;
projection?: ProjectionType<DocType>;
projection?: ProjectionType<any>;
/**
* if true, returns the raw result from the MongoDB driver
*/
Expand Down