Skip to content
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

feat(types+schema): allow defining schema paths using mongoose.Types.* to work around TS type inference issues #12352

Merged
merged 3 commits into from Oct 2, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions lib/schema.js
Expand Up @@ -1109,6 +1109,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 @@ -1243,6 +1244,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
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');
});
});
61 changes: 60 additions & 1 deletion test/types/schema.test.ts
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 @@ -640,6 +644,28 @@ function gh12030() {
]
});

type A = ResolvePathType<[
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these tests are a bit wrong, I think they should be:

  const a = [
    {
      username: { type: String }
    }
  ];

  expectType<{
    username?: string
  }[]>({} as ResolvePathType<typeof a>);

  const b = {
    users: [
      {
        username: { type: String }
      }
    ]
  };

  expectType<{
    users: {
      username?: string
    }[];
  }>({} as ResolvePathType<typeof b>);

  const Schema1 = new Schema({
    users: [
      {
        username: { type: String }
      }
    ]
  });

{
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 @@ -737,3 +763,36 @@ function pluginOptions() {
schema.plugin<any, SomePluginOptions>(pluginFunction2, { option2: 0 });
expectError(schema.plugin<any, SomePluginOptions>(pluginFunction2, {})); // should error because "option2" is not optional
}

function gh12205() {
const campaignSchema = new Schema(
{
client: {
type: new Types.ObjectId(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is ObjectId instance an acceptable value to pass ?

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 } }>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You use Schema.Types.ObjectId as type, I think this is incorrect, or I might be wrong.
Same thing for the following tests.

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); */
}
57 changes: 33 additions & 24 deletions types/inferschematype.d.ts
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'>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you like to test this ?
note: revert all changes in this file while you testing.

   type InferSchemaType<TSchema> = TSchema extends Schema<any, any, any, any, any, any, string, infer DocType> ? DocType : unknown;


/**
* @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 ? 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']> :
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to be more careful before making changes to ResolvePathType,
I've not checked all changes yet, but I prefer to avoid making all of these changes. " I might be wrong"
Will come back with more details.

IfEquals<PathValueType, String> extends true ? PathEnumOrString<Options['enum']> :
PathValueType extends NumberSchemaDefinition ? 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;