const _ = require('lodash');
const clone = require('clone');
import { normalizeProperties } from './normalize'
export class Schema {
/**
* @classdesc Schema class represents the schema definition. It includes properties, methods, static methods, and any
* middleware we want to define.
*
* @description Creates an object schema
* @class
* @api public
* @param {Object} descriptor
* @param {Object} options
* @param {Boolean} options.strict - By default (<code>true</code>), allow only values in the schema to be set.
* When this is <code>false</code>, setting new fields will dynamically add the field
* to the schema as type "any".
* @param {Boolean} options.dotNotation - Allow fields to be set via dot notation. Default: <code>true</code>.
* <code>obj['user.name'] = 'Joe'; -> obj: { user: 'Joe' }<code>
*
* @param {Boolean} options.minimize - "minimize" schemas by removing empty objects. Default: <code>true</code>
* @param {Object} options.toObject - <code>toObject</code> method options.
* @param {Boolean} options.toObject.minimize - "minimize" schemas by removing empty objects. Default: <code>true</code>
* @param {Function} options.toObject.transform - transform function
* @param {Boolean} options.toObject.virtuals - whether to include virtual properties. Default: <code>false</code>
* @param {Boolean} options.toObject.dateToISO - convert dates to string in ISO format using <code>Date.toISOString()</code>. Default: <code>false</code>
* @param {Object} options.toJSON - options for <code>toJSON</code> method options, similar to above
* @param {Boolean} options.strict - ensures that value passed in ot assigned that were not specified in our
* schema do not get saved
* @param {Function} options.onBeforeValueSet - function called when write operations on an object takes place. Currently,
* it will only notify of write operations on the object itself and will not notify you when child objects are written to.
* If you return false or throw an error within the onBeforeValueSet handler, the write operation will be cancelled.
* Throwing an error will add the error to the error stack.
* @param {Function} options.onValueSet - Similar to <code>onBeforeValueSet</code>, but called after we've set a value on the key,
*
* @example
* var schema = new plaster.Schema({ name: String });
* @example <caption>with <code>onBeforeValueSet</code></caption>
* var User = plaster.schema({ name: String }, {
* onBeforeValueSet: function(key, value) {
* if(key === 'name' && value.indexOf('Joe') >= 0) {
* return false;
* });
* }
* });
*
* var User = plaster.model('User', schema);
* var user = new User();
* user.name = 'Bill'; // name not set
* user.name = 'Joe Smith'; // { name: 'Joe Smith' }
*/
constructor(descriptor, options = {}) {
// Create object for options if doesn't exist and merge with defaults.
this.options = _.extend({
strict: true,
dotNotation: true
}, options);
this.methods = {};
this.statics = {};
this.callQueue = [];
this.add(descriptor);
}
/**
* Creates a instance method for the created model.
* An object of function names and functions can also be passed in.
*
* @api public
* @param {String} name the name of the method
* @param {Function} func the actual function implementation
*
* @example
* var userSchema = plaster.schema({
* firstName: String,
* lastName: String
* });
*
* userSchema.method('getFullName', function () {
* return this.firstName + ' ' + this.lastName
* });
*
* var User = plaster.model('User', userSchema);
* var user = new User({
* firstName: 'Joe',
* lastName: 'Smith'
* });
*
* console.log(user.getFullName()); // Joe Smith
*
*/
method(name, func) {
if (_.isPlainObject(name)) {
for (func in name) {
this.method(func, name[func]);
}
}
else {
if (!_.isString(name)) throw new TypeError('Schema#method expects a string identifier as a function name');
else if (!_.isFunction(func)) throw new TypeError('Schema#method expects a function as a handle');
this.methods[name] = func;
}
}
/**
* Creates a static function for the created model.
* An object of function names and functions can also be passed in.
*
* @api public
* @param {String} name name of the statuc function
* @param {Function} func the actual function
*
* * @example
* var userSchema = plaster.schema({ name: String });
*
* userSchema.static('foo', function () {
* return 'bar';
* });
*
* var User = plaster.model('User', userSchema);
*
* console.log(User.foo()); // 'bar'
*
*/
static(name, func) {
if (_.isPlainObject(name)) {
for (func in name) {
this.statics(func, name[func]);
}
}
else {
if (!_.isString(name)) throw new TypeError('Schema#statics expects a string identifier as a function name');
else if (!_.isFunction(func)) throw new TypeError('Schema#statics expects a function as a handle');
this.statics[name] = func;
}
}
/**
* Creates a virtual property for the created model with the given object
* specifying the get and optionally set function
*
* @api public
* @param {String} name name of the virtual property
* @param {String|Function|Object} type optional type to be used for the virtual property. If not provided default is
* 'any' type.
* @param {Object} options virtual options
* @param {Function} options.get - the virtual getter function
* @param {Function} options.set - the virtual setter function. If not provided the virtual becomes read-only.
*
* @example
* var userSchema = plaster.schema({firstName: String, lastName: String});
*
* userSchema.virtual('fullName', String, {
* get: function () {
* return this.firstName + ' ' + this.lastName;
* },
* set: function (v) {
* if (v !== undefined) {
* var parts = v.split(' ');
* this.firstName = parts[0];
* this.lastName = parts[1];
* }
* }
* });
*
* var User = plaster.model('User', userSchema);
*
* var user = new User({firstName: 'Joe', lastName: 'Smith'});
* console.log(user.fullName); // Joe Smith
* user.fullName = 'Bill Jones';
* console.log(user.firstName); // Bill
* console.log(user.lastName); // Jones
* console.log(user.fullName); // Bill Jones
*/
virtual(name, type, options) {
if (!_.isString(name)) throw new TypeError('Schema#virtual expects a string identifier as a property name');
if (_.isPlainObject(type) && !options) {
options = type;
type = 'any';
}
else if (!_.isPlainObject(options)) throw new TypeError('Schema#virtual expects an object as a handle');
else if (!_.isFunction(options.get)) throw new TypeError('Schema#virtual expects an object with a get function');
var virtualType = {
type: type,
virtual: true,
get: options.get,
invisible: true
};
if (options.set) {
virtualType.set = options.set;
}
else {
virtualType.readOnly = true;
}
this.descriptor[name] = virtualType;
}
/**
* Sets/gets a schema option.
*
* @param {String} key option name
* @param {Object} [value] if not passed, the current option value is returned
* @api public
*/
set(key, value) {
if (1 === arguments.length) {
return this.options[key];
}
this.options[key] = value;
return this;
}
/**
* Gets a schema option.
*
* @api public
* @param {String} key option name
* @return {*} the option value
*/
get(key) {
return this.options[key];
}
/**
* Defines a pre hook for the schema.
* See {@link https://www.npmjs.com/package/hooks-fixed hooks-fixed}.
*/
pre() {
return this.queue('pre', arguments);
}
/**
* Defines a post hook for the schema.
* See {@link https://www.npmjs.com/package/hooks-fixed hooks-fixed}.
*/
post() {
return this.queue('post', arguments);
}
/**
* Adds a method call to the queue.
*
* @param {String} name name of the document method to call later
* @param {Array} args arguments to pass to the method
* @api private
*/
queue(name, args) {
var q = {hook: name, args: args};
if (args[0] && typeof args[0] === 'string') {
q.hooked = args[0];
}
this.callQueue.push(q);
}
/**
* Adds the descriptor to the schema at the given key
* @param key the property key
* @param descriptor the property descriptor
*
* @example
* var userSchema = plaster.schema({firstName: String });
* userSchema.add('lastName', String);
*/
add(key, descriptor) {
if (!this.descriptor) {
this.descriptor = {};
}
// adjust our descriptor
if (key && descriptor) {
this.descriptor[key] = normalizeProperties.call(this, descriptor, key)
}
else if (typeof key === 'object' && !descriptor) {
if (!this.descriptor) {
this.descriptor = {};
}
_.each(key, (properties, index) => {
this.descriptor[index] = normalizeProperties.call(this, properties, index);
});
}
}
/**
* Clones property from other to us
* @param {Schema} other - other schema
* @param {String} prop - property name
* @param {Boolean} add - whether to add() or assign. if true will do deep clone.
* @private
*/
_cloneProp(other, prop, add) {
if (other && other[prop]) {
let p;
for (p in other[prop]) {
if (other[prop].hasOwnProperty(p) && !this[prop][p]) {
if (add) {
this.add(p, clone(other.descriptor[p]));
}
else {
this[prop][p] = other[prop][p];
}
}
}
}
}
/**
* Extends other schema. Copies descriptor properties, methods, statics, virtuals and middleware.
* If this schema has a named property already, the property is not copied.
* @param {Schema} other the schema to extend.
*/
extend(other) {
if (other && other instanceof Schema) {
this._cloneProp(other, 'descriptor', true);
this._cloneProp(other, 'statics');
this._cloneProp(other, 'methods');
var self = this;
other.callQueue.forEach(function (e) {
self.callQueue.unshift(e);
});
}
return this;
}
}