'Mongoose: extending schemas
Currently I have two almost identical schemas:
var userSchema = mongoose.Schema({
email: {type: String, unique: true, required: true, validate: emailValidator},
passwordHash: {type: String, required: true},
firstname: {type: String, validate: firstnameValidator},
lastname: {type: String, validate: lastnameValidator},
phone: {type: String, validate: phoneValidator},
});
And
var adminSchema = mongoose.Schema({
email: {type: String, unique: true, required: true, validate: emailValidator},
passwordHash: {type: String, required: true},
firstname: {type: String, validate: firstnameValidator, required: true},
lastname: {type: String, validate: lastnameValidator, required: true},
phone: {type: String, validate: phoneValidator, required: true},
});
Their only difference is in validation: Users do not need a firstname, lastname or phone. Admins however must have these properties defined.
Unfortunately the above code is not very DRY, as they're almost identical. Therefore I am wondering if it is possible to build an adminSchema
based on the userSchema
. E.g.:
var adminSchema = mongoose.Schema(userSchema);
adminSchema.change('firstname', {required: true});
adminSchema.change('lastname', {required: true});
adminSchema.change('phone', {required: true});
Obviously that's just pseudocode. Is something like this possible?
Another very similar question is if it is possible to create a new schema based on another, and add some more properties to it. For example:
var adminSchema = mongoose.Schema(userSchema);
adminSchema.add(adminPower: Number);
Solution 1:[1]
Some people have in other places suggested using utils.inherits to extend schemas. Another simple way would be to simply set up an object with settings and create Schemas from it, like so:
var settings = {
one: Number
};
new Schema(settings);
settings.two = Number;
new Schema(settings);
It's a bit ugly though, since you're modifying the same object. Also I'd like to be able to extend plugins and methods etc. Thus my preferred method is the following:
function UserSchema (add) {
var schema = new Schema({
someField: String
});
if(add) {
schema.add(add);
}
return schema;
}
var userSchema = UserSchema();
var adminSchema = UserSchema({
anotherField: String
});
Which happens to answer your second question that yes, you can add()
fields. So to modify some properties of the Schema, a modified version of the above function would solve your problem:
function UserSchema (add, nameAndPhoneIsRequired) {
var schema = new Schema({
//...
firstname: {type: String, validate: firstnameValidator, required: nameAndPhoneIsRequired},
lastname: {type: String, validate: lastnameValidator, required: nameAndPhoneIsRequired},
phone: {type: String, validate: phoneValidator, required: nameAndPhoneIsRequired},
});
if(add) {
schema.add(add);
}
return schema;
}
Solution 2:[2]
Mongoose 3.8.1 now has support for Discriminators. A sample, from here: http://mongoosejs.com/docs/api.html#model_Model.discriminator
function BaseSchema() {
Schema.apply(this, arguments);
this.add({
name: String,
createdAt: Date
});
}
util.inherits(BaseSchema, Schema);
var PersonSchema = new BaseSchema();
var BossSchema = new BaseSchema({ department: String });
var Person = mongoose.model('Person', PersonSchema);
var Boss = Person.discriminator('Boss', BossSchema);
Solution 3:[3]
The simplest way for extending mongoose schema
import { model, Schema } from 'mongoose';
const ParentSchema = new Schema({
fromParent: Boolean
});
const ChildSchema = new Schema({
...ParentSchema.obj,
fromChild: Boolean // new properties come up here
});
export const Child = model('Child', ChildSchema);
Solution 4:[4]
You can extend the original Schema#obj:
const AdminSchema = new mongoose.Schema({}, Object.assign(UserSchema.obj, {...}))
Example:
const mongoose = require('mongoose');
const UserSchema = new mongoose.Schema({
email: {type: String, unique: true, required: true},
passwordHash: {type: String, required: true},
firstname: {type: String},
lastname: {type: String},
phone: {type: String}
});
// Extend function
const extend = (Schema, obj) => (
new mongoose.Schema(
Object.assign({}, Schema.obj, obj)
)
);
// Usage:
const AdminUserSchema = extend(UserSchema, {
firstname: {type: String, required: true},
lastname: {type: String, required: true},
phone: {type: String, required: true}
});
const User = mongoose.model('users', UserSchema);
const AdminUser = mongoose.model('admins', AdminUserSchema);
const john = new User({
email: '[email protected]',
passwordHash: 'bla-bla-bla',
firstname: 'John'
});
john.save();
const admin = new AdminUser({
email: '[email protected]',
passwordHash: 'bla-bla-bla',
firstname: 'Henry',
lastname: 'Hardcore',
// phone: '+555-5555-55'
});
admin.save();
// Oops! Error 'phone' is required
Or use this npm module with the same approach:
const extendSchema = require('mongoose-extend-schema'); // not 'mongoose-schema-extend'
const UserSchema = new mongoose.Schema({
firstname: {type: String},
lastname: {type: String}
});
const ClientSchema = extendSchema(UserSchema, {
phone: {type: String, required: true}
});
Check the github repo https://github.com/doasync/mongoose-extend-schema
Solution 5:[5]
To add to this discussion, you can also override mongoose.Schema with a custom base schema definition. For code compatibility, add the if statement that allows a Schema to be instantiated without new
. While this can be convenient, think twice before doing this in a public package.
var Schema = mongoose.Schema;
var BaseSyncSchema = function(obj, options) {
if (!(this instanceof BaseSyncSchema))
return new BaseSyncSchema(obj, options);
Schema.apply(this, arguments);
this.methods.update = function() {
this.updated = new Date();
};
this.add({
updated: Date
});
};
util.inherits(BaseSyncSchema, Schema);
// Edit!!!
// mongoose.Schema = BaseSyncSchema; <-- Does not work in mongoose 4
// Do this instead:
Object.defineProperty(mongoose, "Schema", {
value: BaseSyncSchema,
writable: false
});
Solution 6:[6]
I just published a mongoose-super npm module. Although I did some testing, it is still in an experimental stage. I'm interested to know if it works well for the applications of my fellow SO users!
The module provides a inherit() convenience function that returns a child Mongoose.js model based on a parent model and a child schema extension. It also augments models with a super() method to call parent model methods. I added this functionality because it is something I missed in other extension/inheritance libraries.
The inherit convenience function simply uses the discriminator method.
Solution 7:[7]
All of these answers seem rather needlessly complicated, with extension helper functions or extend methods applied to the schema's or using plugins/discriminators. I've used the following solution instead which is simple, clean and easy to work with. It defines a blueprint for the base schema, and then the actual schema's are built using the blueprint:
foo.blueprint.js
module.exports = {
schema: {
foo: String,
bar: Number,
},
methods: {
fooBar() {
return 42;
},
}
};
foo.schema.js
const {schema, methods} = require('./foo.blueprint');
const {Schema} = require('mongoose');
const FooSchema = new Schema(foo);
Object.assign(FooSchema.methods, methods);
module.exports = FooSchema;
bar.schema.js
const {schema, methods} = require('./foo.blueprint');
const {Schema} = require('mongoose');
const BarSchema = new Schema(Object.assign({}, schema, {
bar: String,
baz: Boolean,
}));
Object.assign(BarSchema.methods, methods);
module.exports = BarSchema;
You can use the blueprint for the original schema as is, and using Object.assign
you can extend the blueprint in any way you like for other schema's, without modifying the same object.
Solution 8:[8]
You can create a Schema Factory function that accepts a Schema definition and optional schema options, which then merges the passed in Schema definition and options with the Schema fields and options which you want to share across schemas.
Example illustrating this (assuming you want to share or extend a schema that has the fields email
and is_verified
and the timestamps
option enabled):
// schemaFactory.js
const mongoose = require('mongoose');
const SchemaFactory = (schemaDefinition, schemaOptions) => {
return new mongoose.Schema({
{
email: {type: String, required: true},
is_verified: {type: Boolean, default: false},
// spread/merge passed in schema definition
...schemaDefinition
}
}, {
timestamps: true,
// spread/merge passed in schema options
...schemaOptions
})
}
module.exports = SchemaFactory;
The SchemaFactory
function can then be called with:
// schemas.js
const SchemaFactory = require("./schemaFactory.js")
const UserSchema = SchemaFactory({
first_name: String,
password: {type: String, required: true}
});
const AdminSchema = SchemaFactory({
role: {type: String, required: true}
}, {
// we can pass in schema options to the Schema Factory
strict: false
});
Now the UserSchema
and AdminSchema
will contain both the email
and is_verified
field as well as have the timestamps
option enabled, along with the schema fields and options you pass along.
Solution 9:[9]
I didn't require discrimination, as I was trying to extend sub document schema's which are stored as a part of a parent document anyway.
My solution was to append an "extend" method to the schema that is the base schema, so that you can either use the base schema itself or generate a new schema based on it.
ES6 code follows:
'use strict';
//Dependencies
let Schema = require('mongoose').Schema;
//Schema generator
function extendFooSchema(fields, _id = false) {
//Extend default fields with given fields
fields = Object.assign({
foo: String,
bar: String,
}, fields || {});
//Create schema
let FooSchema = new Schema(fields, {_id});
//Add methods/options and whatnot
FooSchema.methods.bar = function() { ... };
//Return
return FooSchema;
}
//Create the base schema now
let FooSchema = extendFooSchema(null, false);
//Expose generator method
FooSchema.extend = extendFooSchema;
//Export schema
module.exports = FooSchema;
You can now use this schema as is, or "extend" it as needed:
let BazSchema = FooSchema.extend({baz: Number});
Extension in this case creates a brand new Schema definition.
Solution 10:[10]
I also try to create the same thing for our project but the above answers and mongoose-extend-schema
package fail to accommodate the copy of hooks(methods,virtuals, and query helpers) from the base schema.
To solve this problem i did the following
const BaseSchema = new mongoose.Schema({
name: String,
password: {type: String, required: true}
});
const AdminSchemaProperties = {
name: String,
password: {type: String, required: true}
};
function extendSchema(oldSchema,properties){
let cloneSchema = oldSchema.clone()// deep copy the mongoose schema
let schemaObj = {...cloneSchema.obj,...properties} //create new object with schema object and extended properties
cloneSchema.obj = schemaObj
return cloneSchema
}
const AdminSchema = extendSchema(BaseSchema,AdminSchemaProperties)
export default AdminSchema
Solution 11:[11]
Perhaps this has become easier with later versions of mongoose.
const ToySchema = new Schema();
ToySchema.add({ name: 'string', color: 'string', price: 'number' });
const TurboManSchema = new Schema();
// You can also `add()` another schema and copy over all paths, virtuals,
// getters, setters, indexes, methods, and statics.
TurboManSchema.add(ToySchema).add({ year: Number });
Reference: https://mongoosejs.com/docs/api/schema.html#schema_Schema-add
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow