Skip to content

Commit

Permalink
Merge pull request #12352 from Automattic/vkarpov15/gh-12205
Browse files Browse the repository at this point in the history
feat(types+schema): allow defining schema paths using mongoose.Types.* to work around TS type inference issues
  • Loading branch information
vkarpov15 authored Oct 2, 2022
2 parents 2e19ae2 + d290d9a commit 24a768b
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 25 deletions.
5 changes: 5 additions & 0 deletions lib/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -1110,6 +1110,7 @@ Schema.prototype.interpretAsType = function(path, obj, options) {
// If this schema has an associated Mongoose object, use the Mongoose object's
// copy of SchemaTypes re: gh-7158 gh-6933
const MongooseTypes = this.base != null ? this.base.Schema.Types : Schema.Types;
const Types = this.base != null ? this.base.Types : require('./types');

if (!utils.isPOJO(obj) && !(obj instanceof SchemaTypeOptions)) {
const constructorName = utils.getFunctionName(obj.constructor);
Expand Down Expand Up @@ -1244,6 +1245,10 @@ Schema.prototype.interpretAsType = function(path, obj, options) {
name = 'Buffer';
} else if (typeof type === 'function' || typeof type === 'object') {
name = type.schemaName || utils.getFunctionName(type);
} else if (type === Types.ObjectId) {
name = 'ObjectId';
} else if (type === Types.Decimal128) {
name = 'Decimal128';
} else {
name = type == null ? '' + type : type.toString();
}
Expand Down
18 changes: 18 additions & 0 deletions test/schema.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2867,4 +2867,22 @@ describe('schema', function() {
assert.equal(doc1.domain, mongooseDomain);
assert.equal(doc1.domain, doc2.domain);
});

it('allows defining ObjectIds and Decimal128s using Types.* (gh-12205)', function() {
const schema = new Schema({
testId: mongoose.Types.ObjectId,
testId2: {
type: mongoose.Types.ObjectId
},
num: mongoose.Types.Decimal128,
num2: {
type: mongoose.Types.Decimal128
}
});

assert.equal(schema.path('testId').instance, 'ObjectID');
assert.equal(schema.path('testId2').instance, 'ObjectID');
assert.equal(schema.path('num').instance, 'Decimal128');
assert.equal(schema.path('num2').instance, 'Decimal128');
});
});
93 changes: 92 additions & 1 deletion test/types/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,14 @@ import {
InferSchemaType,
SchemaType,
Query,
model,
HydratedDocument,
SchemaOptions
SchemaOptions,
ObtainDocumentType,
ObtainSchemaGeneric
} from 'mongoose';
import { expectType, expectError, expectAssignable } from 'tsd';
import { ObtainDocumentPathType, ResolvePathType } from '../../types/inferschematype';

enum Genre {
Action,
Expand Down Expand Up @@ -665,6 +669,28 @@ function gh12030() {
]
});

type A = ResolvePathType<[
{
username: { type: String }
}
]>;
expectType<{
username?: string
}[]>({} as A);

type B = ObtainDocumentType<{
users: [
{
username: { type: String }
}
]
}>;
expectType<{
users: {
username?: string
}[];
}>({} as B);

expectType<{
users: {
username?: string
Expand Down Expand Up @@ -763,6 +789,71 @@ function pluginOptions() {
expectError(schema.plugin<any, SomePluginOptions>(pluginFunction2, {})); // should error because "option2" is not optional
}

function gh12205() {
const campaignSchema = new Schema(
{
client: {
type: new Types.ObjectId(),
required: true
}
},
{ timestamps: true }
);

const Campaign = model('Campaign', campaignSchema);
const doc = new Campaign();
expectType<Types.ObjectId>(doc.client);

type ICampaign = InferSchemaType<typeof campaignSchema>;
expectType<{ client: Types.ObjectId }>({} as ICampaign);

expectType<'type'>({} as ObtainSchemaGeneric<typeof campaignSchema, 'TPathTypeKey'>);

type A = ObtainDocumentType<{ client: { type: Schema.Types.ObjectId, required: true } }>;
expectType<{ client: Types.ObjectId }>({} as A);

type Foo = ObtainDocumentPathType<{ type: Schema.Types.ObjectId, required: true }, 'type'>;
expectType<Types.ObjectId>({} as Foo);

type Bar = ResolvePathType<Schema.Types.ObjectId, { required: true }>;
expectType<Types.ObjectId>({} as Bar);

/* type Baz = Schema.Types.ObjectId extends typeof Schema.Types.ObjectId ? string : number;
expectType<string>({} as Baz); */
}


function gh12450() {
const ObjectIdSchema = new Schema({
user: { type: Schema.Types.ObjectId }
});

expectType<{
user?: Types.ObjectId;
}>({} as InferSchemaType<typeof ObjectIdSchema>);

const Schema2 = new Schema({
createdAt: { type: Date, required: true },
decimalValue: { type: Schema.Types.Decimal128, required: true }
});

expectType<{ createdAt: Date, decimalValue: Types.Decimal128 }>({} as InferSchemaType<typeof Schema2>);

const Schema3 = new Schema({
createdAt: { type: Date, required: true },
decimalValue: { type: Schema.Types.Decimal128 }
});

expectType<{ createdAt: Date, decimalValue?: Types.Decimal128 }>({} as InferSchemaType<typeof Schema3>);

const Schema4 = new Schema({
createdAt: { type: Date },
decimalValue: { type: Schema.Types.Decimal128 }
});

expectType<{ createdAt?: Date, decimalValue?: Types.Decimal128 }>({} as InferSchemaType<typeof Schema4>);
}

function gh12242() {
const dbExample = new Schema(
{
Expand Down
57 changes: 33 additions & 24 deletions types/inferschematype.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,14 @@ declare module 'mongoose' {

/**
* @summary Obtains document schema type from Schema instance.
* @param {SchemaType} SchemaType A generic of schema type instance.
* @param {Schema} TSchema `typeof` a schema instance.
* @example
* const userSchema = new Schema({userName:String});
* type UserType = InferSchemaType<typeof userSchema>;
* // result
* type UserType = {userName?: string}
*/
type InferSchemaType<SchemaType> = ObtainSchemaGeneric<SchemaType, 'DocType'>;
type InferSchemaType<TSchema> = ObtainSchemaGeneric<TSchema, 'DocType'>;

/**
* @summary Obtains schema Generic type by using generic alias.
Expand All @@ -65,7 +65,7 @@ declare module 'mongoose' {
* @param {P} P Document path.
* @param {TypeKey} TypeKey A generic of literal string type."Refers to the property used for path type definition".
*/
type IsPathRequired<P, TypeKey extends TypeKeyBaseType> =
type IsPathRequired<P, TypeKey extends TypeKeyBaseType = DefaultTypeKey> =
P extends { required: true | [true, string | undefined] } | ArrayConstructor | any[]
? true
: P extends (Record<TypeKey, ArrayConstructor | any[]>)
Expand All @@ -83,15 +83,15 @@ type IsPathRequired<P, TypeKey extends TypeKeyBaseType> =
* @description It helps to check if a path is defined by TypeKey OR not.
* @param {TypeKey} TypeKey A literal string refers to path type property key.
*/
type PathWithTypePropertyBaseType<TypeKey extends TypeKeyBaseType> = { [k in TypeKey]: any };
type PathWithTypePropertyBaseType<TypeKey extends TypeKeyBaseType = DefaultTypeKey> = { [k in TypeKey]: any };

/**
* @summary A Utility to obtain schema's required path keys.
* @param {T} T A generic refers to document definition.
* @param {TypeKey} TypeKey A generic of literal string type."Refers to the property used for path type definition".
* @returns required paths keys of document definition.
*/
type RequiredPathKeys<T, TypeKey extends TypeKeyBaseType> = {
type RequiredPathKeys<T, TypeKey extends TypeKeyBaseType = DefaultTypeKey> = {
[K in keyof T]: IsPathRequired<T[K], TypeKey> extends true ? IfEquals<T[K], any, never, K> : never;
}[keyof T];

Expand All @@ -101,7 +101,7 @@ type RequiredPathKeys<T, TypeKey extends TypeKeyBaseType> = {
* @param {TypeKey} TypeKey A generic of literal string type."Refers to the property used for path type definition".
* @returns a record contains required paths with the corresponding type.
*/
type RequiredPaths<T, TypeKey extends TypeKeyBaseType> = {
type RequiredPaths<T, TypeKey extends TypeKeyBaseType = DefaultTypeKey> = {
[K in RequiredPathKeys<T, TypeKey>]: T[K];
};

Expand All @@ -111,7 +111,7 @@ type RequiredPaths<T, TypeKey extends TypeKeyBaseType> = {
* @param {TypeKey} TypeKey A generic of literal string type."Refers to the property used for path type definition".
* @returns optional paths keys of document definition.
*/
type OptionalPathKeys<T, TypeKey extends TypeKeyBaseType> = {
type OptionalPathKeys<T, TypeKey extends TypeKeyBaseType = DefaultTypeKey> = {
[K in keyof T]: IsPathRequired<T[K], TypeKey> extends true ? never : K;
}[keyof T];

Expand All @@ -121,7 +121,7 @@ type OptionalPathKeys<T, TypeKey extends TypeKeyBaseType> = {
* @param {TypeKey} TypeKey A generic of literal string type."Refers to the property used for path type definition".
* @returns a record contains optional paths with the corresponding type.
*/
type OptionalPaths<T, TypeKey extends TypeKeyBaseType> = {
type OptionalPaths<T, TypeKey extends TypeKeyBaseType = DefaultTypeKey> = {
[K in OptionalPathKeys<T, TypeKey>]?: T[K];
};

Expand All @@ -131,7 +131,7 @@ type OptionalPaths<T, TypeKey extends TypeKeyBaseType> = {
* @param {PathValueType} PathValueType Document definition path type.
* @param {TypeKey} TypeKey A generic refers to document definition.
*/
type ObtainDocumentPathType<PathValueType, TypeKey extends TypeKeyBaseType> = PathValueType extends Schema<any>
type ObtainDocumentPathType<PathValueType, TypeKey extends TypeKeyBaseType = DefaultTypeKey> = PathValueType extends Schema<any>
? InferSchemaType<PathValueType>
: ResolvePathType<
PathValueType extends PathWithTypePropertyBaseType<TypeKey> ? PathValueType[TypeKey] : PathValueType,
Expand All @@ -154,19 +154,28 @@ type PathEnumOrString<T extends SchemaTypeOptions<string>['enum']> = T extends (
*/
type ResolvePathType<PathValueType, Options extends SchemaTypeOptions<PathValueType> = {}, TypeKey extends TypeKeyBaseType = DefaultTypeKey> =
PathValueType extends Schema ? InferSchemaType<PathValueType> :
PathValueType extends (infer Item)[] ? IfEquals<Item, never, any[], Item extends Schema ? Types.DocumentArray<ResolvePathType<Item>> : ResolvePathType<Item>[]> :
PathValueType extends (infer Item)[] ? IfEquals<Item, never, any[], Item extends Schema ? Types.DocumentArray<ObtainDocumentPathType<Item, TypeKey>> : ObtainDocumentPathType<Item, TypeKey>[]> :
PathValueType extends StringSchemaDefinition ? PathEnumOrString<Options['enum']> :
PathValueType extends NumberSchemaDefinition ? Options['enum'] extends ReadonlyArray<any> ? Options['enum'][number] : number :
PathValueType extends DateSchemaDefinition ? Date :
PathValueType extends typeof Buffer | 'buffer' | 'Buffer' | typeof Schema.Types.Buffer ? Buffer :
PathValueType extends BooleanSchemaDefinition ? boolean :
PathValueType extends ObjectIdSchemaDefinition ? Types.ObjectId :
PathValueType extends 'decimal128' | 'Decimal128' | typeof Schema.Types.Decimal128 ? Types.Decimal128 :
PathValueType extends MapConstructor ? Map<string, ResolvePathType<Options['of']>> :
PathValueType extends ArrayConstructor ? any[] :
PathValueType extends typeof Schema.Types.Mixed ? any:
IfEquals<PathValueType, ObjectConstructor> extends true ? any:
IfEquals<PathValueType, {}> extends true ? any:
PathValueType extends typeof SchemaType ? PathValueType['prototype'] :
PathValueType extends Record<string, any> ? ObtainDocumentType<PathValueType, any, TypeKey> :
unknown;
IfEquals<PathValueType, Schema.Types.String> extends true ? PathEnumOrString<Options['enum']> :
IfEquals<PathValueType, String> extends true ? PathEnumOrString<Options['enum']> :
PathValueType extends NumberSchemaDefinition ? Options['enum'] extends ReadonlyArray<any> ? Options['enum'][number] : number :
IfEquals<PathValueType, Schema.Types.Number> extends true ? number :
PathValueType extends DateSchemaDefinition ? Date :
IfEquals<PathValueType, Schema.Types.Date> extends true ? Date :
PathValueType extends typeof Buffer | 'buffer' | 'Buffer' | typeof Schema.Types.Buffer ? Buffer :
PathValueType extends BooleanSchemaDefinition ? boolean :
IfEquals<PathValueType, Schema.Types.Boolean> extends true ? boolean :
PathValueType extends ObjectIdSchemaDefinition ? Types.ObjectId :
IfEquals<PathValueType, Types.ObjectId> extends true ? Types.ObjectId :
IfEquals<PathValueType, Schema.Types.ObjectId> extends true ? Types.ObjectId :
PathValueType extends 'decimal128' | 'Decimal128' | typeof Schema.Types.Decimal128 ? Types.Decimal128 :
IfEquals<PathValueType, Schema.Types.Decimal128> extends true ? Types.Decimal128 :
IfEquals<PathValueType, Types.Decimal128> extends true ? Types.Decimal128 :
PathValueType extends MapConstructor ? Map<string, ResolvePathType<Options['of']>> :
PathValueType extends ArrayConstructor ? any[] :
PathValueType extends typeof Schema.Types.Mixed ? any:
IfEquals<PathValueType, ObjectConstructor> extends true ? any:
IfEquals<PathValueType, {}> extends true ? any:
PathValueType extends typeof SchemaType ? PathValueType['prototype'] :
PathValueType extends Record<string, any> ? ObtainDocumentType<PathValueType, any, TypeKey> :
unknown;

0 comments on commit 24a768b

Please sign in to comment.