Skip to content

Commit

Permalink
Merge pull request #12731 from Automattic/vkarpov15/gh-12069
Browse files Browse the repository at this point in the history
Infer timestamps option from schema
  • Loading branch information
vkarpov15 committed Nov 29, 2022
2 parents 0c98cbb + accb7e5 commit c9ed03d
Show file tree
Hide file tree
Showing 6 changed files with 77 additions and 41 deletions.
22 changes: 17 additions & 5 deletions test/types/schema.test.ts
Expand Up @@ -627,7 +627,7 @@ function gh11997() {
}

function gh12003() {
const baseSchemaOptions: SchemaOptions = {
const baseSchemaOptions = {
versionKey: false
};

Expand All @@ -637,6 +637,8 @@ function gh12003() {

type BaseSchemaType = InferSchemaType<typeof BaseSchema>;

expectType<'type'>({} as ObtainSchemaGeneric<typeof BaseSchema, 'TSchemaOptions'>['typeKey']);

expectType<{ name?: string }>({} as BaseSchemaType);
}

Expand Down Expand Up @@ -794,8 +796,7 @@ function gh12205() {
type: new Types.ObjectId(),
required: true
}
},
{ timestamps: true }
}
);

const Campaign = model('Campaign', campaignSchema);
Expand All @@ -805,8 +806,6 @@ function gh12205() {
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);

Expand Down Expand Up @@ -863,6 +862,19 @@ function gh12242() {
expectType<0 | 1>({} as Example['active']);
}

function testInferTimestamps() {
const schema = new Schema({
name: String
}, { timestamps: true });

type WithTimestamps = InferSchemaType<typeof schema>;
// For some reason, expectType<{ createdAt: Date, updatedAt: Date, name?: string }> throws
// an error "Parameter type { createdAt: Date; updatedAt: Date; name?: string | undefined; }
// is not identical to argument type { createdAt: NativeDate; updatedAt: NativeDate; } &
// { name?: string | undefined; }"
expectType<{ createdAt: Date, updatedAt: Date } & { name?: string }>({} as WithTimestamps);
}

function gh12431() {
const testSchema = new Schema({
testDate: { type: Date },
Expand Down
10 changes: 4 additions & 6 deletions types/index.d.ts
Expand Up @@ -185,13 +185,14 @@ declare module 'mongoose' {
TQueryHelpers = {},
TVirtuals = {},
TStaticMethods = {},
TPathTypeKey extends TypeKeyBaseType = DefaultTypeKey,
DocType extends ObtainDocumentType<DocType, EnforcedDocType, TPathTypeKey> = ObtainDocumentType<any, EnforcedDocType, TPathTypeKey>>
TSchemaOptions extends ResolveSchemaOptions<TSchemaOptions> = DefaultSchemaOptions,
DocType extends ApplySchemaOptions<ObtainDocumentType<DocType, EnforcedDocType, TSchemaOptions>, TSchemaOptions> = ApplySchemaOptions<ObtainDocumentType<any, EnforcedDocType, TSchemaOptions>, TSchemaOptions>,
>
extends events.EventEmitter {
/**
* Create a new schema
*/
constructor(definition?: SchemaDefinition<SchemaDefinitionType<EnforcedDocType>> | DocType, options?: SchemaOptions<TPathTypeKey, FlatRecord<DocType>, TInstanceMethods, TQueryHelpers, TStaticMethods, TVirtuals>);
constructor(definition?: SchemaDefinition<SchemaDefinitionType<EnforcedDocType>> | DocType, options?: SchemaOptions<FlatRecord<DocType>, TInstanceMethods, TQueryHelpers, TStaticMethods, TVirtuals> | TSchemaOptions);

/** Adds key path / schema type pairs to this schema. */
add(obj: SchemaDefinition<SchemaDefinitionType<EnforcedDocType>> | Schema, prefix?: string): this;
Expand Down Expand Up @@ -550,9 +551,6 @@ declare module 'mongoose' {

export type SchemaDefinitionType<T> = T extends Document ? Omit<T, Exclude<keyof Document, '_id' | 'id' | '__v'>> : T;

// Helpers to simplify checks
type IfUnknown<IFTYPE, THENTYPE> = unknown extends IFTYPE ? THENTYPE : IFTYPE;

// tests for these two types are located in test/types/lean.test.ts
export type DocTypeFromUnion<T> = T extends (Document<infer T1, infer T2, infer T3> & infer U) ?
[U] extends [Document<T1, T2, T3> & infer U] ? IfUnknown<IfAny<U, false>, false> : false : false;
Expand Down
61 changes: 35 additions & 26 deletions types/inferschematype.d.ts
Expand Up @@ -12,7 +12,8 @@ import {
ObtainDocumentType,
DefaultTypeKey,
ObjectIdSchemaDefinition,
IfEquals
IfEquals,
DefaultSchemaOptions
} from 'mongoose';

declare module 'mongoose' {
Expand All @@ -23,10 +24,10 @@ declare module 'mongoose' {
* @param {EnforcedDocType} EnforcedDocType A generic type enforced by user "provided before schema constructor".
* @param {TypeKey} TypeKey A generic of literal string type."Refers to the property used for path type definition".
*/
type ObtainDocumentType<DocDefinition, EnforcedDocType = any, TypeKey extends TypeKeyBaseType = DefaultTypeKey> =
type ObtainDocumentType<DocDefinition, EnforcedDocType = any, TSchemaOptions extends Record<any, any> = DefaultSchemaOptions> =
IsItRecordAndNotAny<EnforcedDocType> extends true ? EnforcedDocType : {
[K in keyof (RequiredPaths<DocDefinition, TypeKey> &
OptionalPaths<DocDefinition, TypeKey>)]: ObtainDocumentPathType<DocDefinition[K], TypeKey>;
[K in keyof (RequiredPaths<DocDefinition, TSchemaOptions['typeKey']> &
OptionalPaths<DocDefinition, TSchemaOptions['typeKey']>)]: ObtainDocumentPathType<DocDefinition[K], TSchemaOptions['typeKey']>;
};

/**
Expand All @@ -45,19 +46,27 @@ declare module 'mongoose' {
* @param {TSchema} TSchema A generic of schema type instance.
* @param {alias} alias Targeted generic alias.
*/
type ObtainSchemaGeneric<TSchema, alias extends 'EnforcedDocType' | 'M' | 'TInstanceMethods' | 'TQueryHelpers' | 'TVirtuals' | 'TStaticMethods' | 'TPathTypeKey' | 'DocType'> =
TSchema extends Schema<infer EnforcedDocType, infer M, infer TInstanceMethods, infer TQueryHelpers, infer TVirtuals, infer TStaticMethods, infer TPathTypeKey, infer DocType>
? {
EnforcedDocType: EnforcedDocType;
M: M;
TInstanceMethods: TInstanceMethods;
TQueryHelpers: TQueryHelpers;
TVirtuals: TVirtuals;
TStaticMethods: TStaticMethods;
TPathTypeKey: TPathTypeKey;
DocType: DocType;
}[alias]
: unknown;
type ObtainSchemaGeneric<TSchema, alias extends 'EnforcedDocType' | 'M' | 'TInstanceMethods' | 'TQueryHelpers' | 'TVirtuals' | 'TStaticMethods' | 'TSchemaOptions' | 'DocType'> =
TSchema extends Schema<infer EnforcedDocType, infer M, infer TInstanceMethods, infer TQueryHelpers, infer TVirtuals, infer TStaticMethods, infer TSchemaOptions, infer DocType>
? {
EnforcedDocType: EnforcedDocType;
M: M;
TInstanceMethods: TInstanceMethods;
TQueryHelpers: TQueryHelpers;
TVirtuals: TVirtuals;
TStaticMethods: TStaticMethods;
TSchemaOptions: TSchemaOptions;
DocType: DocType;
}[alias]
: unknown;

type ResolveSchemaOptions<T> = Omit<MergeType<DefaultSchemaOptions, T>, 'statics' | 'methods' | 'query' | 'virtuals'>;

type ApplySchemaOptions<T, O = DefaultSchemaOptions> = ResolveTimestamps<T, O>;

type ResolveTimestamps<T, O> = O extends { timestamps: true }
? { createdAt: NativeDate; updatedAt: NativeDate; } & T
: T;
}

type IsPathDefaultUndefined<PathType> = PathType extends { default: undefined } ?
Expand All @@ -71,7 +80,7 @@ type IsPathDefaultUndefined<PathType> = PathType extends { default: undefined }
* @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 = DefaultTypeKey> =
type IsPathRequired<P, TypeKey extends string = DefaultTypeKey> =
P extends { required: true | [true, string | undefined] } | ArrayConstructor | any[]
? true
: P extends (Record<TypeKey, ArrayConstructor | any[]>)
Expand All @@ -89,15 +98,15 @@ type IsPathRequired<P, TypeKey extends TypeKeyBaseType = DefaultTypeKey> =
* @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 = DefaultTypeKey> = { [k in TypeKey]: any };
type PathWithTypePropertyBaseType<TypeKey extends string = 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 = DefaultTypeKey> = {
type RequiredPathKeys<T, TypeKey extends string = DefaultTypeKey> = {
[K in keyof T]: IsPathRequired<T[K], TypeKey> extends true ? IfEquals<T[K], any, never, K> : never;
}[keyof T];

Expand All @@ -107,7 +116,7 @@ type RequiredPathKeys<T, TypeKey extends TypeKeyBaseType = DefaultTypeKey> = {
* @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 = DefaultTypeKey> = {
type RequiredPaths<T, TypeKey extends string = DefaultTypeKey> = {
[K in RequiredPathKeys<T, TypeKey>]: T[K];
};

Expand All @@ -117,7 +126,7 @@ type RequiredPaths<T, TypeKey extends TypeKeyBaseType = DefaultTypeKey> = {
* @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 = DefaultTypeKey> = {
type OptionalPathKeys<T, TypeKey extends string = DefaultTypeKey> = {
[K in keyof T]: IsPathRequired<T[K], TypeKey> extends true ? never : K;
}[keyof T];

Expand All @@ -127,7 +136,7 @@ type OptionalPathKeys<T, TypeKey extends TypeKeyBaseType = DefaultTypeKey> = {
* @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 = DefaultTypeKey> = {
type OptionalPaths<T, TypeKey extends string = DefaultTypeKey> = {
[K in OptionalPathKeys<T, TypeKey>]?: T[K];
};

Expand All @@ -137,7 +146,7 @@ type OptionalPaths<T, TypeKey extends TypeKeyBaseType = DefaultTypeKey> = {
* @param {PathValueType} PathValueType Document definition path type.
* @param {TypeKey} TypeKey A generic refers to document definition.
*/
type ObtainDocumentPathType<PathValueType, TypeKey extends TypeKeyBaseType = DefaultTypeKey> = PathValueType extends Schema<any>
type ObtainDocumentPathType<PathValueType, TypeKey extends string = DefaultTypeKey> = PathValueType extends Schema<any>
? InferSchemaType<PathValueType>
: ResolvePathType<
PathValueType extends PathWithTypePropertyBaseType<TypeKey> ? PathValueType[TypeKey] : PathValueType,
Expand All @@ -158,7 +167,7 @@ type PathEnumOrString<T extends SchemaTypeOptions<string>['enum']> = T extends (
* @param {TypeKey} TypeKey A generic of literal string type."Refers to the property used for path type definition".
* @returns Number, "Number" or "number" will be resolved to number type.
*/
type ResolvePathType<PathValueType, Options extends SchemaTypeOptions<PathValueType> = {}, TypeKey extends TypeKeyBaseType = DefaultTypeKey> =
type ResolvePathType<PathValueType, Options extends SchemaTypeOptions<PathValueType> = {}, TypeKey extends string = DefaultSchemaOptions['typeKey']> =
PathValueType extends Schema ? InferSchemaType<PathValueType> :
PathValueType extends (infer Item)[] ? IfEquals<Item, never, any[], Item extends Schema ? Types.DocumentArray<ObtainDocumentPathType<Item, TypeKey>> : ObtainDocumentPathType<Item, TypeKey>[]> :
PathValueType extends ReadonlyArray<infer Item> ? IfEquals<Item, never, any[], Item extends Schema ? Types.DocumentArray<ObtainDocumentPathType<Item, TypeKey>> : ObtainDocumentPathType<Item, TypeKey>[]> :
Expand Down Expand Up @@ -186,5 +195,5 @@ type ResolvePathType<PathValueType, Options extends SchemaTypeOptions<PathValueT
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> :
PathValueType extends Record<string, any> ? ObtainDocumentType<PathValueType, any, { typeKey: TypeKey }> :
unknown;
12 changes: 10 additions & 2 deletions types/models.d.ts
Expand Up @@ -129,8 +129,16 @@ declare module 'mongoose' {
AcceptsDiscriminator,
IndexManager,
SessionStarter {
new <DocType = T>(doc?: DocType, fields?: any | null, options?: boolean | AnyObject): HydratedDocument<T, TMethodsAndOverrides,
IfEquals<TVirtuals, {}, ObtainSchemaGeneric<TSchema, 'TVirtuals'>, TVirtuals>> & ObtainSchemaGeneric<TSchema, 'TStaticMethods'>;
new <DocType = T>(doc?: DocType, fields?: any | null, options?: boolean | AnyObject): HydratedDocument<
T,
TMethodsAndOverrides,
IfEquals<
TVirtuals,
{},
ObtainSchemaGeneric<TSchema, 'TVirtuals'>,
TVirtuals
>
> & ObtainSchemaGeneric<TSchema, 'TStaticMethods'>;

aggregate<R = any>(pipeline?: PipelineStage[], options?: mongodb.AggregateOptions, callback?: Callback<R[]>): Aggregate<Array<R>>;
aggregate<R = any>(pipeline: PipelineStage[], callback?: Callback<R[]>): Aggregate<Array<R>>;
Expand Down
12 changes: 10 additions & 2 deletions types/schemaoptions.d.ts
Expand Up @@ -10,7 +10,7 @@ declare module 'mongoose' {
type TypeKeyBaseType = string;

type DefaultTypeKey = 'type';
interface SchemaOptions<PathTypeKey extends TypeKeyBaseType = DefaultTypeKey, DocType = unknown, TInstanceMethods = {}, QueryHelpers = {}, TStaticMethods = {}, TVirtuals = {}> {
interface SchemaOptions<DocType = unknown, TInstanceMethods = {}, QueryHelpers = {}, TStaticMethods = {}, TVirtuals = {}> {
/**
* By default, Mongoose's init() function creates all the indexes defined in your model's schema by
* calling Model.createIndexes() after you successfully connect to MongoDB. If you want to disable
Expand Down Expand Up @@ -139,7 +139,7 @@ declare module 'mongoose' {
* type declaration. However, for applications like geoJSON, the 'type' property is important. If you want to
* control which key mongoose uses to find type declarations, set the 'typeKey' schema option.
*/
typeKey?: PathTypeKey;
typeKey?: string;

/**
* By default, documents are automatically validated before they are saved to the database. This is to
Expand Down Expand Up @@ -214,4 +214,12 @@ declare module 'mongoose' {
*/
virtuals?: SchemaOptionsVirtualsPropertyType<DocType, TVirtuals, TInstanceMethods>,
}

interface DefaultSchemaOptions {
typeKey: 'type';
id: true;
_id: true;
timestamps: false;
versionKey: '__v'
}
}
1 change: 1 addition & 0 deletions types/utility.d.ts
@@ -1,5 +1,6 @@
declare module 'mongoose' {
type IfAny<IFTYPE, THENTYPE, ELSETYPE = IFTYPE> = 0 extends (1 & IFTYPE) ? THENTYPE : ELSETYPE;
type IfUnknown<IFTYPE, THENTYPE> = unknown extends IFTYPE ? THENTYPE : IFTYPE;

type Unpacked<T> = T extends (infer U)[] ?
U :
Expand Down

0 comments on commit c9ed03d

Please sign in to comment.