Skip to content

Instantly share code, notes, and snippets.

@tcha-tcho
Last active August 15, 2023 20:31
Show Gist options
  • Save tcha-tcho/1675fb39c4f4b28ec50d844364796705 to your computer and use it in GitHub Desktop.
Save tcha-tcho/1675fb39c4f4b28ec50d844364796705 to your computer and use it in GitHub Desktop.
Small ORM part of the framework duJS
/**
* Module to work with Models and Classes
*/
(function(o){
o.Model = {_:{}};
// o.Model.connection;
// o.Model.connect;
var counter = 0;
o.Model._.isDoc = function(obj){
return obj instanceof du.Model.Doc;
};
o.Model._.isModel = function(obj){
return typeof obj == "function" && !!obj.modelName;
}
var isFunc = function(cb) {
return (cb && typeof cb == "function");
};
var isNative = function(obj) {
return obj.toString().indexOf("[native code]") != -1;
}
var empty = [];
var callCB = function(cb, doc, args) {
if (typeof args == "undefined") args = [];
if (!(args instanceof Array)) args = [args];
args = [doc.errors||doc.invalid||null, doc]
.concat(empty.slice.call(args, 0))
if (isFunc(cb)) cb.apply(doc, args);
}
// normal Methods
var eventName = function(model, type) {
return "__"+model.modelName+"."+model._.id+"_"+type;
}
// USE AUTOLOAD ON SCHEME!!!
o.Model._.embeddedID = function(doc, collectionName) {
if (typeof doc == "object") {
return doc.modelName+o.opt.embeddedIDseparator+((doc._||{}).id);
} else if (typeof doc == "string" && collectionName) {
if (!o.Model._.isEmbeddedID(doc)) {
return collectionName+o.opt.embeddedIDseparator+doc;
} else {
return doc;
}
}
};
// var embRegex = new RegExp("(.*?)"+o.opt.embeddedIDseparator+"(.*?)");
o.Model._.isEmbeddedID = function(id) {
if (typeof id == "string") {
// return embRegex.exec(id);
var splitted = id.split(o.opt.embeddedIDseparator);
return (splitted.length == 2)?splitted:false;
};
};
o.Model._.id = function(id){
if (typeof id != "string") id = "";
var splitted = id.split("::");
return splitted[1]||splitted[0]||"";
};
var applyEvents = function(methods) {
methods.on = function(type, func, one) {
o.on(eventName(this,type), func, this, one);
}
methods.one = function(type, func) {
this.on(type, func, 1);
}
methods.off = function(type, func) {
o.off(eventName(this,type), func);
}
methods.trigger = function(type) {
arguments[0] = eventName(this,type);
o.trigger.apply(o.trigger, arguments);
}
methods.change = function(func) {
this[func?"on":"trigger"]("change", func);
}
methods.hasEvent = function(type) {
return !!o.events[eventName(this,type)]
}
};
/**
* DOC - Class and methods
*/
var docMethods = {};
// //DOC and Model events
// ["tp", "type", "on", "one", "off", "trigger", "change", "state"]
// .map(function(mtd){
// docMethods[mtd] = o.Methods.Set[mtd];
// });
o.opt.docVersionLimit = 3;
applyEvents(docMethods);
docMethods.toObject = function(){
var obj = {};
for (prop in this) {
if (this.hasOwnProperty(prop)) obj[prop] = this[prop];
}
return obj;
};
docMethods.toString = function(){
return JSON.stringify(this.toObject());
};
docMethods.log = function(subject) {
console.log((subject?subject+": ":"") + JSON.stringify(this, null, '\t'));
return this;
};
docMethods.validate = function() {
o._.hidden(this, "invalid", this.Model.validate(this));
return this.invalid;
};
// docMethods.invalidate = function(path, msg, value){}
docMethods.save = function(cb){
return this.Model.save(this, cb);
};
docMethods.reset = function(){
return this.Model.reset(this);
};
docMethods.rollback = function(cb){
return this.Model.rollback(this, cb);
};
docMethods.update = function(obj, cb) {
return this.Model.update(this, obj, cb);
};
docMethods.remove = function(cb) {
return this.Model.remove(this, cb);
};
docMethods.attr = function(path, value) {
// Talvez aqui poderia ser como no Node attr() ou prop()
if (typeof path === "object") {
for (var prop in path) {
if (!docMethods[prop]) this[prop] = path[prop];
}
return this;
} else {
return o._.path(this, path, value);
}
};
docMethods.clone = function(){
return new this.Model(this);
};
docMethods.is = function(compare, recurr) { // is accept partials
var matches = 0, count = 0;
if (!recurr && o._.isEmpty(compare)) return false;
for (var prop in compare) {
count ++
if (typeof compare[prop] == "object") {
if (docMethods.is.call(this[prop], compare[prop], recurr)) matches ++;
} else {
if (o._.deepEqual(this[prop], compare[prop])) matches ++;
}
}
return matches == count;
};
docMethods.isModified = function (path) {
if (!this.version[0]) return true;
return !o._.deepEqual(this.attr(path), this.version[0].attr(path));
};
docMethods.toObject = function(){
return o._.clone(this);
};
docMethods.toJSON = function(){
return JSON.parse(JSON.stringify(o._.clone(this)));
};
docMethods.state = function(states, data) {
return o.state(states, data, this)
};
docMethods.sync = function(next) {
var self = this;
return o.Model[this.modelName].get(this._.id).then(function(docs){
o._.replaceObj(self, docs[0]);
next(self)
});
};
docMethods.clean = function(propToClean) {
var self = this;
var isEmb = o.Model._.isEmbeddedID(self[propToClean]);
if (isEmb) {
return isEmb[1]||isEmb[0]
} else {
return self[propToClean]
};
};
docMethods.get = function(propToLoad, cb) {
var self = this;
var isEmb = o.Model._.isEmbeddedID(self[propToLoad]);
if (isEmb) {
var prom = o.Model[isEmb[0]].get(isEmb[1], cb);
prom.firstOnly = true;
return prom;
} else {
if (typeof cb == "function") cb.call(self, self[propToLoad]);
return {then: function(fun){
if (typeof fun == "function") fun.call(self, self[propToLoad]);
}}
}
};
docMethods.load = function(propToLoad, next) {
if (typeof propToLoad == "function") {
next = propToLoad;
propToLoad = false;
};
if (!propToLoad) propToLoad = "*";
if (propToLoad && !(propToLoad instanceof Array)) {
propToLoad = [propToLoad];
};
var self = this;
var queue = [];
var queueCount = 0;
var toLoad = [];
// this.Model.base(this._.id)
o._.eachAll(self, function(attr, key, upperKey) {
var _this = this
var _key = key;
if (du._.isNumber(key)) key = upperKey;
if (propToLoad.indexOf(key) != -1 || propToLoad[0] == "*") {
var isEmb = o.Model._.isEmbeddedID(attr);
if (isEmb) toLoad.push({attr: attr, obj: _this, key: _key});
}
});
toLoad.forEach(function(load) {
var attr = load.attr;
var obj = load.obj;
var key = load.key;
// var scheme = self.Model.schema.scheme
var type;
if (o.Model._.isDoc(attr)) {
obj[key] = o.Model._.embeddedID(attr);
attr = obj[key];
// attr = attr._.id;
// scheme[key].type = attr.modelName
type = attr.modelName;
};
var isEmb = o.Model._.isEmbeddedID(attr);
if (isEmb) {
// scheme[prop].type = isEmb[0];
type = isEmb[0];
attr = isEmb[1];
};
if (attr && type) {
queue.push({type: type, id: attr, obj: obj, key: key});
queueCount++;
};
})
/// call after ready!!!!
var resolve = function(){}
var reject = function(){}
function finished() {
if (typeof next == "function") {
next.call(self, self)
};
resolve.call(self, self)
};
queue.forEach(function(get){
var Model = o.Model[get.type];
if (Model) {
Model.find(get.id).then(function(docs){
var doc = docs[0];
get.obj[get.key] = doc || get.id;
queueCount--;
if (queueCount <= 0) finished()
})
};
});
return {
then: function(func){
resolve = func;
}
,catch: function(func){
reject = func;
}
}
};
// mongoosejs
// http://mongoosejs.com/docs/api.html#document-js - compat pack here
o.Methods.Doc = docMethods;
o.Methods.DocModel = {};
/**
* Main Document Class
* All other Classes will be instance of this Model
* And they will have any method from this one
* @param {String} collection Model name
*/
o.Model.Doc = function(modelName, obj, cb) {
if (typeof obj == "object" && obj.__id) {
var id = obj.__id;
delete obj.__id;
} else {
counter ++;
var id = o._.to_id(o.opt.ID+counter);
}
var self = this;
var initial = {
Model: o.Model[modelName]
,modelName: modelName
,version: []
,_: {body: {state: "*"}, type:"Doc", id: id}
,before: {}
,after: {}
};
for (var prop in initial) {
o._.hidden(self, prop, initial[prop]);
}
Object.keys(o.Methods.Doc).forEach(function(method) {
o._.hidden(self, method, o.Methods.Doc[method]);
});
if (o.Methods.DocModel[modelName]) {
Object.keys(o.Methods.DocModel[modelName]).forEach(function(method) {
o._.hidden(self, method, o.Methods.DocModel[modelName][method]);
});
};
for (var attr in obj) { // Doc-specific properties
self[attr] = obj[attr];
};
if (cb) self.save(cb);
};
o.Model.ViewMethods = {};
var viewMethods = {};
applyEvents(viewMethods);
var clearFilters = function(filters){
if (!(filters instanceof Array)) filters = [filters];
var self = this;
filters.forEach(function(filter){
self.filters = o.Set(self.filters).removeIf(function(item, i){
if (!item) return true;
return item[0] == filter;
})
});
};
// Default ViewMethods
o.Model.ViewMethods.query = function(query, next) {
if (typeof query != "object") query = {};
if (!this.called) this.docs = this.Model.filter(query);
next();
};
o.Model.ViewMethods.limit = function(num, next) {
this.docs = this.docs.limit(num);
next();
};
o.Model.ViewMethods.select = function(fields, next) {
if (typeof fields == "string") fields = [fields];
if (fields instanceof Array) {
this.docs = this.docs.map(function(item){
var clone = o._.clone(item, true);
for (var prop in clone) {
if (fields.indexOf(prop) == -1) {
delete clone[prop];
}
}
return clone;
})
};
next();
};
o.Model.ViewMethods.skip = function(num, next) {
if (!this.called) this.docs = this.docs.skip(num);
next();
};
o.Model.ViewMethods.slice = function(start, num, next) {
if (!this.called) this.docs = this.docs.slice(start, num);
next();
};
o.Model.ViewMethods.reverse = function(next) {
this.docs.reverse()
next();
};
o.Model.ViewMethods.first = function(next) {
this.docs = this.docs.first();
next();
};
o.Model.ViewMethods.last = function(next) {
this.docs = this.docs.last();
next();
};
o.Model.ViewMethods.sort_by = function(field, reverse, primer, next) {
if (!next && typeof reverse == "function") next = reverse;
if (!next && typeof primer == "function") {
next = primer;
primer = false;
}
this.docs = this.docs.sort_by(field, reverse, primer);
next();
};
o.Model.ViewMethods.orderBy = o.Model.ViewMethods.sort_by;
o.Model.ViewMethods.startAt = function(item, next){
this.docs = this.docs.startAt(item); // include item
next()
};
o.Model.ViewMethods.startAfter = function(item, next){
this.docs = this.docs.startAt(item); // include item
this.docs.shift();
next();
};
o.Model.ViewMethods.endAt = function(item, next){
this.docs = this.docs.endAt(item); // include item
next()
}
o.Model.ViewMethods.endBefore = function(){
this.docs = this.docs.endAt(item); // include item
this.docs.pop();
next();
}
o.Model.ViewMethods.removeMore = function(next) {
// if (this._.inMore) {
this.docs = this.Model.docs;
delete this.called;
delete this._.inMore;
this.filters = this._.backupFilters || this.filters;
delete this._.backupFilters
clearFilters.call(this, ["more","removeMore"]);
// return o.Model.ViewMethods.process(this, next)
this.docs = this.docs.slice(0,10);
this.change();
next();
// } else {
// next();
// }
};
o.Model.ViewMethods.more = function(num, reverse, next) {
if (!next && typeof num == "function") {
next = num;
num = false;
reverse = false;
};
if (!next && typeof reverse == "function") {
next = reverse;
reverse = false;
}
var self = this;
// var lastdoc = self.docs[reverse?"first":"last"](); //.last()??
var lastdoc = self.docs["last"](); //.last()??
if (lastdoc) {
if (!self._.inMore) {
self._.inMore = true;
self._.backupFilters = o._.clone(self.filters);
};
clearFilters.call(self,
["live","more", "startAt", "limit", "removeMore"]);
if (self.Model.docs.length > self.docs.length) {
var filters = o._.clone(self.filters);
self.Model.view(false, filters)
.startAfter(lastdoc).limit(num||20).then(function(docs){
if (docs.length) {
// if (reverse) docs.reverse();
// self.docs = reverse ?
// docs.concat(self.docs) : self.docs.concat(docs);
self.docs = self.docs.concat(docs);
// self.docs.forEach(function(item) {
// console.log(item.uid)
// })
self.change()
next()
} else {
next()
}
})
}
} else {
next();
};
};
o.Model.ViewMethods.finally = function(next) {
if (this.firstOnly && this.docs instanceof Array) this.docs = this.docs[0];
next();
}
o.Model.ViewMethods.process = function(promise, resolve, reject) {
promise.docs = promise.docs || promise.Model.docs;
var queue = [];
if (promise.Model.ViewMethods.beforeStart) {
queue.push(promise.Model.ViewMethods.beforeStart)
}
promise.lastFilters = o._.clone(promise.filters);
for (var i = 0; i < promise.filters.length; i++) {
var fun = function(args, next) {
var argsNext = [].slice.call(args.args||[]).concat(next);
if (promise.Model.ViewMethods[args.filter]) {
promise.Model.ViewMethods[args.filter].apply(this, argsNext);
}
}
fun.args = {
filter: promise.filters[i][0]
,args: promise.filters[i][1]
};
queue.push(fun)
};
if (promise.Model.ViewMethods.finally) {
queue.push(promise.Model.ViewMethods.finally)
}
queue.push(function() {
promise.called = true;
// promise.change()
if (promise.errors.length) {
if (typeof reject == "function") {
reject.call(this, this.errors);
};
if(promise._errorCatch) {
promise._errorCatch.call(promise, promise.errors)
}
} else {
if (typeof resolve == "function") {
resolve.call(this, this.firstOnly?this.docs:o.Set(this.docs));
};
};
})
o._.queue(queue, promise)
return promise;
};
/**
* Main View Class
*/
o.Model.View = function(Model, docs, filters) {
// var promise = new Promise(function(resolve, reject) {
// setTimeout(function(){
// o.Model.processView(promise, resolve, reject);
// }, 0);
// });
var promise = {
then: function(resolve, reject) {
return o.Model.ViewMethods.process(promise, resolve, reject)
}
,catch: function(error) {
// do something about that error
if (typeof error == "function") {
promise._errorCatch = error;
};
return promise
}
};
counter ++;
var initial = {
Model: Model
,modelName: Model.modelName
,version: []
,_: {body: {state: "*"}, type:"View", id: counter}
,errors: []
,before: {}
,after: {}
,filters: filters || []
,docs: docs || false
};
for (var prop in initial) {
o._.hidden(promise, prop, initial[prop]);
}
for (var method in viewMethods) {
promise[method] = viewMethods[method];
};
var methods = {};
o._.extend(methods, Model.ViewMethods);
Object.keys(methods).forEach(function(method){
promise[method] = function() {
promise.filters.push([method, arguments])
return promise;
};
});
return promise
};
/**
* DOC - Class and methods
*/
var schemaMethods = {};
schemaMethods.path = function(){};
// prop: {
// type: String
// ,validate: f() - a function
// ,content: {} - Validate contents
// ,unique: true - If other exist
// ,default: "value" - Will fill with this - if nodug is present
// ,required: true - Will test du._.isEmpty()
// ,index: true - Create a index for this field ???
// ,match: "value" || /value/ - Only will accept this value or test
// ,before: func(val) - will return the new value
// ,lowercase: true - Will lowerCase the content
// ,uppperCase: true - Will upperCase the content
// ,trim: true - Will trim the value before save
// ,enum: true - If the value being set is not in this array
// ,max: 30 - length
// ,min: 6 - length
// } - mixed
schemaMethods.validate = function(doc, scheme) { // return [err] or undefined
var scheme = (scheme || this.scheme);
if (!scheme) return undefined;
var all_errors = [];
var keys = Object.keys(scheme);
if (scheme["*"]) {
for (var attr in doc) {
scheme[attr] = scheme[attr] || scheme["*"];
};
delete scheme["*"];
};
for (var prop in scheme) {
var test = scheme[prop];
//redefine values
for (var attr in test) {
var valid = test[attr];
switch(attr) {
case "type":
if (du.opt.embedAsReference && typeof valid == "string"
&& typeof doc[prop] == "string") {
doc[prop] = o.Model._.embeddedID(doc[prop], valid)
}
break;
case "default":
if (typeof doc[prop] == "undefined") doc[prop] = valid;
break;
case "lowercase":
doc[prop] = doc[prop].toLowerCase();
break;
case "uppercase":
doc[prop] = doc[prop].toUpperCase();
break;
case "trim":
doc[prop] = doc[prop].trim();
break;
case "before":
doc[prop] = valid.call(doc, doc[prop]);
break;
case "remove":
delete doc[prop];
break;
default:
// if (/d/.test()) {
// }
break;
}
};
//test values
var val = doc[prop];
var undef = typeof doc[prop] == "undefined";
var errors = [];
for (var check in test) {
var valid = test[check];
var ok = true;
switch(check) {
case "type":
if (typeof valid == "string" && o.Model[valid]) {
if (undef) { // difficult to check
ok = true;
} else if (typeof val == "string") {
var isEmb = o.Model._.isEmbeddedID(val);
ok = isEmb instanceof Array
&& isEmb[0].toLowerCase() == valid.toLowerCase();
} else if (typeof val == "object") {
ok = !!o.Model[valid].modelName && !!val.modelName;
}
} else {
ok = !undef && val.constructor === valid;
};
break;
case "validate":
ok = valid.call(doc, val);
break;
case "unique":
ok = !this.Model.filter(doc.toObject()).length
break;
case "required":
ok = !undef;
break;
case "index":
// Model indexing
break;
case "match":
ok = valid instanceof RegExp?
new RegExp(valid).test(val):
o._.deepEqual(valid, val);
break;
case "max":
case "min":
var len = (typeof val == "number")?val:val.length;
ok = check == "min"?
len >= valid:
len <= valid;
break;
case "enum":
ok = valid.indexOf(val) != -1;
break;
case "content":
if (!undef) {
if (val instanceof Array) {
var content_errs = 0;
for (var i = 0; i < val.length; i++) {
if (isNative(valid)) {
if (val[i].constructor !== valid) content_errs ++;
} else if ( this.validate.call(this, val[i], valid) ) {
content_errs ++;
};
};
ok = !content_errs;
} else {
ok = this.validate.call(this, val, valid)
};
}
break;
// default:
// break;
};
if (undef && o.opt.allowUndefinedOnModel) ok = true;
if (!ok) errors.push(check)
};
if (errors.length) all_errors.push({
msg: o._.tmpl(test.msg || o.opt.defaultErrorMsg, {
PATH: prop
,VALUE: val
,TYPE: errors
})
,errors: errors
,validation: test
,value: val
,prop: prop
});
};
return all_errors.length?all_errors:undefined;
};
schemaMethods.parseScheme = function(scheme) { // normalize
for (prop in scheme) {
var test = scheme[prop];
switch(typeof test) {
case "function":
if (isNative(test)) {
// String, Number, Date, Boolean, Object, Array
scheme[prop] = {type: test}; // constructor
} else {
// func()
scheme[prop] = {validate: test};
};
break;
case "object":
if (test instanceof Array) {
// [String]
scheme[prop] = {
type: Array
,content: schemaMethods.parseScheme(test[0])
};
} else if (test instanceof RegExp) {
// RegExp
scheme[prop] = {match: test};
} else if (test === null) {
// Null
scheme[prop] = {match: test}
} else {
// Object
if (test.content) {
scheme[prop].content = schemaMethods.parseScheme(test.content);
}
}
break;
default:
// string, number, boolean, undefined, xml
scheme[prop] = {match: test}
break;
};
};
return scheme;
};
// schemaMethods.__ = {event:{}}
o.Methods.Schema = schemaMethods;
/**
* Main Schema Class
*/
o.Model.Schema = function(scheme, model) {
this.Model = model;
this.__ = {type: "Schema"}
for (method in o.Methods.Schema) {
this[method] = o.Methods.Schema[method];
};
this.scheme = this.parseScheme(scheme);
return this;
};
/**
* MODEL - Class and methods
*/
var modelMethods = {};
applyEvents(modelMethods);
// // import du.Set methods
// Object.keys(o.Methods.Set).map(function(method){
// modelMethods[method] = function() {
// return this.docs[method].apply(this.docs, arguments);
// }
// });
// TODO: MongoDB style queries
// User.filter({age: {$gte: 21, $lte: 65}}, callback);
// we can instead write:
// User.where('age').gte(21).lte(65).exec(callback);
/**
* Overwriten by DB
*/
var save = function(doc, next) {
var self = this;
var taintedDocs = [];
var queueCount = 0;
var scheme = doc.Model.schema.scheme
var transform = function(attr, key) {
if (o.Model._.isDoc(attr)) {
if (o.opt.autoSaveModified) {
if (attr.isModified()) {
queueCount++
taintedDocs.push({doc: attr, obj: this, key: key})
}
}
if (o.opt.embedAsReference) {
// if (!scheme[prop]) scheme[prop] = {};
// scheme[prop].type = doc[prop].modelName
this[key] = o.Model._.embeddedID(attr);
}
}
};
if (o.opt.autoSaveModified || o.opt.embedAsReference) {
du._.eachAll(doc, transform);
};
var finished = function(){
doc.validate();
if (!doc.invalid) {
doc.version.unshift(doc.clone());
doc.version.splice(o.opt.docVersionLimit || 1);
} else {
for (var i = 0; i < doc.invalid.length; i++) {
var prop = doc.invalid[i].prop
if (doc.version[0]) { // reset wrong values
doc[prop] = doc.version[0][prop];
} else {
// var scheme_prop = doc.Model.schema.scheme[prop];
// if (scheme_prop && scheme_prop.default) {
// // ?? necessary ?? to fill with default value
// } else {
delete doc[prop];
// }
}
}
};
if (typeof next == "function") next()
}
if (taintedDocs.length) {
taintedDocs.forEach(function(toSave){
toSave.doc.save(function(err, doc){
// o._.replaceObj(toSave[0], doc);
// self[toSave[0]] = doc || toSave[0];
queueCount--;
if (queueCount <= 0) finished()
})
});
} else {
finished();
}
};
var change = function(doc) {
doc.change(); // notify _subscribers
doc.Model.change() // notify _subscribers
// if (o.render) o.render();
};
///////////////////////////////
// overwrited in other DBs
modelMethods.save = function(doc, cb, args) {
if (!doc.invalid && !doc.added) {
this.docs.push(doc);
this.base[doc._.id] = doc;
o._.hidden(doc, "added", true);
this._.methods["afterSave"].apply(this, doc);
};
callCB(cb, doc, args);
return doc;
};
modelMethods.insert = function(doc, at){
if (!doc.tp) doc = new this(doc);
if (at==undefined) at = this.docs.length;
this.docs.splice(at, 0, doc);
o._.hidden(doc, "added", true).save();
return this;
};
modelMethods.size = function() {
return this.docs.length;
};
modelMethods.get = function(query, cb) {
if (typeof query == "number") {
return this.docs[query];
} else if (typeof query == "string") {
return this.base[query];
} else {
return o.Model.View(this, this.docs).query(query);
};
};
modelMethods.find = modelMethods.get; // alias
modelMethods.filter = function(query, not) { // models act as Node - filter
// return this.get(query, cb);
if (typeof query == "number") {
return this.docs[query];
} else if (typeof query == "string") {
return this.base[query];
} else {
var matches = this.docs; // It will be a o.Set() if present!
if (o.Model._.isDoc(query)) { // I know, but necessary for polyvalence
matches = [query];
} else {
matches = matches.filter(function(doc){
return checkDoc(doc, query, not)
});
}
return matches;
}
};
//////////////////////
modelMethods.view = function(docs, filters) {
return o.Model.View(this, docs, filters);
};
modelMethods.clear = function(cb, args) {
this.docs.length = 0;
this.base = {};
callCB(cb, this, args)
return this;
};
modelMethods.delete = function(doc, cb, args) { // only for memory ORM
this.docs.splice(this.docs.indexOf(doc), 1)
callCB(cb, doc, args)
return this;
}
modelMethods.remove = function(query, cb, args) { // add or update
if (typeof query == "function") {
cb = query;
query = undefined;
};
var self = this;
var resolve = function(){}
var callResolve = function(doc){
callCB(cb, doc||self, args);
resolve.call(self, doc||self)
}
var lastly = function(docs){
if (docs && docs.length) {
docs.forEach(function(doc, i) {
self.delete(doc)
if (i == docs.length-1) {
callResolve(docs.length==1?doc:self)
}
})
} else {
callResolve();
}
};
setTimeout(function(){
if (du.Model._.isDoc(query)) {
lastly([query]);
} else if (typeof query == "object" || o.Model._.isEmbeddedID(query)) {
self.get(query).then(lastly);
} else if (typeof query == "undefined") {
self.docs.clear();
callResolve()
}
}, 0);
return {
then: function(func){
resolve = func;
}
,catch: function(func){}
};
};
modelMethods.update = function(query, obj, cb, args) { // add or update
// safe (boolean) safe mode (defaults to value set in schema (true))
// upsert (boolean) whether to create the doc if it doesn't match (false)
// multi (boolean) whether multiple documents should be updated (false)
// strict (boolean) overrides the strict option for this update
// overwrite (boolean) disables update-only mode, allowing you
// to overwrite the doc (false)
var docs = this.filter(query);
var errs = [];
if (docs.length) {
docs.forEach(function(doc, i) {
o._.extend(doc, obj);
doc.save(function(err){
if (err) errs.push(err)
if (i == docs.length-1) {
if (errs.length) {
doc.errors = errs;
} else {
delete doc.errors
}
callCB(cb, this, args)
}
})
})
};
return obj;
};
modelMethods.upsert = function(query, obj, cb) { // add or update
var docs = this.filter(query);
if (docs.length) {
return this.update(query, obj, cb);
} else {
obj = new this(obj);
return obj.save(cb)
};
};
modelMethods.validate = function(doc){
return this.schema.validate(doc); // return [errs] or undefined
};
modelMethods.reset = function(doc) {
return o._.replaceObj(doc, (doc.version[0] || {_:doc._}));
};
modelMethods.rollback = function(doc, cb, args) {
if (doc.version.length > 1) doc.version.shift();
if (doc.version[0]) o._.replaceObj(doc, doc.version[0]);
return doc;
};
modelMethods.add = function(obj, cb) {
return (new this(obj).save(cb));
};
modelMethods.create = modelMethods.add;
// TODO: index Model to quick find by col
// modelMethods.index = function(){}
// modelMethods.findBy - consumes index
// modelMethods.virtual - Achieved by ModelName.prototype.custom()
modelMethods.state = function(states, data) { // Interceptable??
return o.state(states, data, this)
};
Object.getOwnPropertyNames(Array.prototype).forEach(function(method){
if(!(/(length|constructor|filter|find)/.test(method))) {
modelMethods[method] = function() {
return Array.prototype[method].apply(this.docs, arguments);
}
}
})
modelMethods.toString = function() {
return this.modelName + ".docs has "+this.size()+" items";
}
modelMethods.log = docMethods.log;
var checkDoc = function(obj, query, not) {
var matches = 0;
var counter = 0;
for ( prop in query) {
counter ++;
var type = typeof query[prop];
switch (true) {
case (query[prop] instanceof RegExp):
//problem with global inverter - lastIndex - Create a new RegExp
if (new RegExp(query[prop]).test(obj[prop])) matches ++;
break;
case (type == "object"):
if (checkDoc(obj[prop], query[prop], not)) matches ++;
break;
case (type == "function"):
if (query[prop](obj[prop])) matches ++;
break;
default:
var scheme = ((obj.Model||{}).schema||{}).scheme||{};
var schemeType = (scheme[prop]||{}).type
if (du.opt.embedAsReference && typeof schemeType == "string") {
query[prop] = du.Model._.embeddedID(query[prop], schemeType)
}
if (o._.deepEqual(obj[prop], query[prop])) matches ++;
};
};
var found = matches == counter;
return not?!found:found;
};
modelMethods.findOne = function(query, not) {
return this.filter(query, not)[0];
};
modelMethods.not = function(query) {
return this.filter(query, true);
};
modelMethods.all = function() {
return this.get({});
};
modelMethods.addView = function(method, methodFun) {
var model = this;
model.ViewMethods[method] = methodFun;
};
modelMethods.addMethod = function(method, methodFun, notOnDoc) {
var model = this;
if (direct.indexOf(method) != -1
|| typeof methodFun != "function") {
o._.hidden(model, method, model._.methods[method])
// } else if (method == "view") {
// o._.hidden(model, method, model._.methods[method])
} else if (method == "beforeSave" || method == "afterSave"){
this._.methods[method] = methodFun; // they will triggered by
} else {
if (typeof methodFun == "function") {
model._.methods[method] = methodFun;
};
o._.hidden(model, method, function() {
var arg = empty.slice.call(arguments, 0);
var args = direct.slice.call(arg, 0);
args.unshift(method);
this.trigger.apply(this, args)
var res, no;
if (method == "save" && this._.methods["beforeSave"]) {
var self = this;
no = true;
var argsNext = [arg[0], function(err){ // next
if (!err) model._.methods.save.apply(self, arg);
}];
res = this._.methods["beforeSave"].apply(this, argsNext);
} else if (method == "save") {
save.call(this, arg[0]);
};
if (!no) res = this._.methods[method].apply(this, arg);
if (method == "save" || method == "delete") change(arg[0]);
return res;
});
}
if (!notOnDoc && typeof methodFun == "function") {
if (!o.Methods.DocModel[model.modelName]) {
o.Methods.DocModel[model.modelName] = {}
}
o.Methods.DocModel[model.modelName][method] = methodFun;
}
};
modelMethods.before = {};
modelMethods.after = {};
// o.Set(["tp", "type"]).each(function(mtd){
// modelMethods[mtd] = o.Methods.Set[mtd];
// });
o.Model.Connectors = {};
o.Methods.Model = modelMethods;
o.Model.addMethod = function(method, methodFun){
o.Methods.Model[method] = methodFun;
}
var direct = ["on","one","off","trigger","change","hasEvent"];
/**
* Define a new Model, using doc as constructor
* @param {String} modelName Model Name
* @return {undefined}
*/
o.Model.define = function(modelName, scheme, methods, otherMethods) {
modelName = modelName || "Default";
var model = function(obj, cb) {
o.Model.Doc.call(this, modelName, obj, cb);
};
model.docs = o.Set?o.Set():[];
model.docs.__ = {mBox: true, Model: model};
model.base = {};
model.schema = new o.Model.Schema(scheme, model);
model.modelName = modelName; // .name is reserved :(
model.ViewMethods = o._.extend({}, o.Model.ViewMethods);
counter++;
model._ = {body: {state: "*"}, type: "Model", id: counter };
model.prototype = Object.create(o.Model.Doc.prototype);
o._.hidden(model, "constructor", o.Model[modelName]);
// model.prototype.constructor = o.Model[modelName];
du.Model[modelName] = model;
var methodsModel = o._.clone(o.Methods.Model);
if (typeof methods == "string") {
model.Connector = methods;
methods = o._.clone(o.Model.Connectors[methods]);
};
if (typeof methods == "object") {
if (methods && typeof otherMethods == "object") {
o._.extend(methods, otherMethods)
}
if (!o.Methods.DocModel[modelName]) o.Methods.DocModel[modelName] = {};
Object.keys(methods).forEach(function(exe) {
// o.Methods.Doc[exe] = methods[exe];
if (typeof methods[exe] == "function") {
o.Methods.DocModel[modelName][exe] = function() {
arguments = empty.slice.call(arguments, 0);
arguments.unshift(this);
return this.Model[exe].apply(this.Model, arguments);
}
}
})
if (methods.DocMethods) {
o._.extend(o.Methods.DocModel[modelName], methods.DocMethods);
delete methods.DocMethods;
};
methods = o._.extend(methodsModel, methods);
} else {
methods = methodsModel;
}
if (methods.ViewMethods) {
o._.extend(model.ViewMethods, methods.ViewMethods);
delete methods.ViewMethods;
};
if (methods.Model) {
o._.extend(methods, methods.Model);
delete methods.Model;
}
// if (!methods.beforeSave) methods.beforeSave = function(next){next()};
if (!methods.afterSave) methods.afterSave = function(){};
model._.methods = methods;
model.addMethod = o.Methods.Model.addMethod;
Object.keys(methods).forEach(function(method) {
if (method != "addMethod") {
o.Methods.Model.addMethod.call(model, method, methods[method], true)
};
});
return model;
};
// shortcut for JMLs and React Style
var __States = o.Model.define("__States");
o.Model.useState = function(val) {
var _state = new o.Model.__States({val: val}).save();
return [ _state, function(newVal, callback) {
_state.attr("val", newVal).save(callback);
}];
};
})(du);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment