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

types: implement Schema.discriminator #11855

Merged
merged 4 commits into from
Jun 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
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
34 changes: 32 additions & 2 deletions lib/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -473,9 +473,39 @@ Schema.prototype.defaultOptions = function(options) {
return options;
};

/**
* Inherit a Schema by applying a discriminator on an existing Schema.
*
*
* ####Example:
*
* const options = { discriminatorKey: 'kind' };
*
* const eventSchema = new mongoose.Schema({ time: Date }, options);
* const Event = mongoose.model('Event', eventSchema);
*
* // ClickedLinkEvent is a special type of Event that has
* // a URL.
* const ClickedLinkEvent = Event.discriminator('ClickedLink',
* new mongoose.Schema({ url: String }, options));
*
* // When you create a generic event, it can't have a URL field...
* const genericEvent = new Event({ time: Date.now(), url: 'google.com' });
* assert.ok(!genericEvent.url);
* // But a ClickedLinkEvent can
* const clickedEvent = new ClickedLinkEvent({ time: Date.now(), url: 'google.com' });
* assert.ok(clickedEvent.url);
*
* @param {String} name the name of the discriminator
* @param {Schema} schema the Schema of the discriminated Schema
* @return {Schema} the Schema instance
* @api public
*/

Schema.prototype.discriminator = function(name, schema) {
this._applyDiscriminators = {};
this._applyDiscriminators[name] = schema;
this._applyDiscriminators = Object.assign({}, this._applyDiscriminators, { [name]: schema });
Copy link
Collaborator

Choose a reason for hiding this comment

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

There's no need to create a new object every single time. Just Object.assign(this._applyDiscriminators, { [name]: schema }); should be enough.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Unit Tests fail if i apply your Suggestion

Copy link
Collaborator

Choose a reason for hiding this comment

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

Can't see why. But I'll merge this and try it out for myself.


return this;
};

/**
Expand Down
15 changes: 11 additions & 4 deletions test/types/schema.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Schema, Document, SchemaDefinition, Model, Types } from 'mongoose';
import { expectType, expectNotType, expectError } from 'tsd';
import { expectType, expectError } from 'tsd';

enum Genre {
Action,
Expand Down Expand Up @@ -78,7 +78,7 @@ movieSchema.index({ title: 1 }, { unique: true });
interface IProfile {
age: number;
}
interface ProfileDoc extends Document, IProfile {}
interface ProfileDoc extends Document, IProfile { }
const ProfileSchemaDef: SchemaDefinition<IProfile> = { age: Number };
export const ProfileSchema = new Schema<ProfileDoc, Model<ProfileDoc>, ProfileDoc>(ProfileSchemaDef);

Expand All @@ -87,7 +87,7 @@ interface IUser {
profile: ProfileDoc;
}

interface UserDoc extends Document, IUser {}
interface UserDoc extends Document, IUser { }

const ProfileSchemaDef2: SchemaDefinition<IProfile> = {
age: Schema.Types.Number
Expand Down Expand Up @@ -318,4 +318,11 @@ function gh10900(): void {
const patientSchema = new Schema<IUserProp>({
menuStatus: { type: Schema.Types.Mixed, default: {} }
});
}
}

// discriminator
const eventSchema = new Schema<{ message: string }>({ message: String }, { discriminatorKey: 'kind' });
const batchSchema = new Schema<{ name: string }>({ name: String }, { discriminatorKey: 'kind' });
const discriminatedSchema = batchSchema.discriminator('event', eventSchema);

expectType<Schema<Omit<{ name: string }, 'message'> & { message: string }>>(discriminatedSchema);
19 changes: 16 additions & 3 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,17 @@ declare module 'mongoose' {
useProjection?: boolean;
}

export type DiscriminatorModel<M, T> = T extends Model<infer T1, infer T2, infer T3, infer T4>
?
M extends Model<infer M1, infer M2, infer M3, infer M4>
? Model<Omit<M1, keyof T1> & T1, M2 | T2, M3 | T3, M4 | T4>
: M
: M;

export type DiscriminatorSchema<DocType, M, TInstanceMethods, TQueryHelpers, TVirtuals, T> = T extends Schema<infer T1, infer T2, infer T3, infer T4, infer T5>
? Schema<Omit<DocType, keyof T1> & T1, DiscriminatorModel<T2, M>, T3 | TInstanceMethods, T4 | TQueryHelpers, T5 | TVirtuals>
: Schema<DocType, M, TInstanceMethods, TQueryHelpers, TVirtuals>;

export class Schema<DocType = any, M = Model<DocType, any, any, any>, TInstanceMethods = {}, TQueryHelpers = {}, TVirtuals = any> extends events.EventEmitter {
/**
* Create a new schema
Expand All @@ -142,6 +153,8 @@ declare module 'mongoose' {
/** Returns a copy of this schema */
clone<T = this>(): T;

discriminator<T extends Schema = Schema>(name: string, schema: T): DiscriminatorSchema<DocType, M, TInstanceMethods, TQueryHelpers, TVirtuals, T>;

/** Returns a new schema that has the picked `paths` from this schema. */
pick<T = this>(paths: string[], options?: SchemaOptions): T;

Expand Down Expand Up @@ -474,10 +487,10 @@ declare module 'mongoose' {
type LeanTypeOrArray<T> = T extends unknown[] ? LeanArray<T> : LeanType<T>;

export type LeanArray<T extends unknown[]> =
// By checking if it extends Types.Array we can get the original base type before collapsing down,
// rather than trying to manually remove the old types. This matches both Array and DocumentArray
// By checking if it extends Types.Array we can get the original base type before collapsing down,
// rather than trying to manually remove the old types. This matches both Array and DocumentArray
T extends Types.Array<infer U> ? LeanTypeOrArray<U>[] :
// If it isn't a custom mongoose type we fall back to "do our best"
// If it isn't a custom mongoose type we fall back to "do our best"
T extends unknown[][] ? LeanArray<T[number]>[] : LeanType<T[number]>[];

export type _LeanDocument<T> = {
Expand Down