@jaysalvat/smart-model
Advanced tools
Comparing version 0.3.2 to 0.3.3
@@ -5,13 +5,6 @@ /**! | ||
* https://github.com/jaysalvat/smart-model | ||
* @version 0.3.2 built 2021-02-22 10:45:15 | ||
* @version 0.3.3 built 2021-02-23 12:22:54 | ||
* @license ISC | ||
* @author Jay Salvat http://jaysalvat.com | ||
*/ | ||
class SmartModelError extends Error { | ||
constructor(data) { | ||
super(data.message); | ||
Object.assign(this, data); | ||
} | ||
} | ||
function isArray(value) { | ||
@@ -25,6 +18,2 @@ return Array.isArray(value); | ||
function isEmpty(value) { | ||
return value === "" || value === null || isUndef(value); | ||
} | ||
function isFn(value) { | ||
@@ -39,10 +28,46 @@ return typeof value === "function"; | ||
function isClass(value) { | ||
return value.toString().startsWith("class"); | ||
return value && value.toString().startsWith("class"); | ||
} | ||
function isSmartModel(value) { | ||
return value.prototype instanceof SmartModel || value instanceof SmartModel; | ||
} | ||
function isPlainObject(value) { | ||
return value.toString() === "[object Object]"; | ||
return value && value.toString() === "[object Object]"; | ||
} | ||
function isType(value, Type) { | ||
function keys(obj, cb = function() {}) { | ||
return Object.keys(obj).map(cb); | ||
} | ||
function toArray(value) { | ||
return [].concat([], value); | ||
} | ||
function merge(source, target) { | ||
target = Object.assign({}, source, target); | ||
keys(source, (key => { | ||
if (isPlainObject(source[key]) && isPlainObject(target[key])) { | ||
target[key] = Object.assign({}, source[key], merge(source[key], target[key])); | ||
} | ||
})); | ||
return target; | ||
} | ||
function eject(target) { | ||
target = Object.assign({}, target); | ||
keys(target, (key => { | ||
if (isSmartModel(target[key])) { | ||
target[key] = target[key].$eject(); | ||
} | ||
})); | ||
return target; | ||
} | ||
function pascalCase(string) { | ||
return string.normalize("NFD").replace(/[\u0300-\u036f]/g, "").match(/[a-z1-9]+/gi).map((word => word.charAt(0).toUpperCase() + word.substr(1).toLowerCase())).join(""); | ||
} | ||
function checkType(value, Type) { | ||
const match = Type && Type.toString().match(/^\s*function (\w+)/); | ||
@@ -69,15 +94,32 @@ const type = (match ? match[1] : "object").toLowerCase(); | ||
function toArray(value) { | ||
return [].concat([], value); | ||
class SmartModelError extends Error { | ||
constructor(data) { | ||
super(data.message); | ||
Object.assign(this, data); | ||
} | ||
} | ||
function pascalCase(string) { | ||
return string.normalize("NFD").replace(/[\u0300-\u036f]/g, "").match(/[a-z]+/gi).map((word => word.charAt(0).toUpperCase() + word.substr(1).toLowerCase())).join(""); | ||
} | ||
SmartModelError.throw = function(settings, code, message, property, source) { | ||
const shortCode = code.split(":")[0]; | ||
if (settings.exceptions === true || isPlainObject(settings.exceptions) && settings.exceptions[shortCode]) { | ||
throw new SmartModelError({ | ||
message: message, | ||
property: property, | ||
code: code, | ||
source: source && source.constructor.name | ||
}); | ||
} | ||
}; | ||
function checkErrors(entry, property, value) { | ||
function checkErrors(entry, property, value, first, settings) { | ||
const errors = []; | ||
if (entry.required && isEmpty(value)) { | ||
if (settings.strict && (!entry || !keys(entry).length)) { | ||
errors.push({ | ||
message: `Invalid value 'required' on property '${property}'`, | ||
message: `Property "${property}" can't be set in strict mode`, | ||
code: "strict" | ||
}); | ||
} | ||
if (entry.required && settings.empty(value)) { | ||
errors.push({ | ||
message: `Property "${property}" is "required"`, | ||
code: "required" | ||
@@ -87,20 +129,29 @@ }); | ||
} | ||
if (entry.readonly && !first) { | ||
errors.push({ | ||
message: `Property '${property}' is 'readonly'`, | ||
code: "readonly" | ||
}); | ||
return errors; | ||
} | ||
if (typeof value === "undefined") { | ||
return errors; | ||
} | ||
if (entry.type && (entry.required || !isEmpty(value))) { | ||
if (!toArray(entry.type).some((type => isType(value, type)))) { | ||
errors.push({ | ||
message: `Invalid type '${typeof value}' on property '${property}'`, | ||
code: "type" | ||
}); | ||
if (entry.type && (entry.required || !settings.empty(value))) { | ||
if (!(isSmartModel(entry.type) && isPlainObject(value))) { | ||
if (!toArray(entry.type).some((type => checkType(value, type)))) { | ||
errors.push({ | ||
message: `Property "${property}" has an invalid type "${typeof value}"`, | ||
code: "type" | ||
}); | ||
} | ||
} | ||
} | ||
if (entry.rule) { | ||
Object.keys(entry.rule).forEach((key => { | ||
keys(entry.rule, (key => { | ||
const rule = entry.rule[key]; | ||
if (rule(value)) { | ||
errors.push({ | ||
message: `Invalid value '${key}' on property '${property}'`, | ||
code: key | ||
message: `Property "${property}" breaks the "${key}" rule`, | ||
code: "rule:" + key | ||
}); | ||
@@ -117,6 +168,8 @@ } | ||
} | ||
const Child = entry.type.prototype instanceof SmartModel ? entry.type : false; | ||
const Child = isSmartModel(entry.type) ? entry.type : false; | ||
const schema = isPlainObject(entry.type) ? entry.type : false; | ||
if (Child || schema) { | ||
return Child ? Child : SmartModel.create(pascalCase(property), schema, settings); | ||
const Model = Child ? Child : SmartModel.create(pascalCase(property), schema, settings); | ||
entry.type = Model; | ||
return Model; | ||
} | ||
@@ -130,18 +183,14 @@ return false; | ||
set(target, property, value) { | ||
let entry = schema[property]; | ||
const entry = schema[property] || {}; | ||
const old = target[property]; | ||
const updated = !isEqual(value, old); | ||
const first = isUndef(old); | ||
const updated = !first && !isEqual(value, old); | ||
const Nested = createNested(entry, property, settings); | ||
function trigger(method, args) { | ||
return Reflect.apply(method, target, args ? args : [ property, value, old, schema ]); | ||
const returned = Reflect.apply(method, target, args ? args : [ property, value, old, schema ]); | ||
return !isUndef(returned) ? returned : value; | ||
} | ||
if (!entry) { | ||
if (settings.strict) { | ||
return true; | ||
} | ||
entry = {}; | ||
} | ||
trigger(target.onBeforeSet); | ||
value = trigger(target.$onBeforeSet); | ||
if (updated) { | ||
trigger(target.onBeforeUpdate); | ||
value = trigger(target.$onBeforeUpdate); | ||
} | ||
@@ -151,20 +200,20 @@ if (isFn(entry.transform)) { | ||
} | ||
if (settings.exceptions) { | ||
const errors = checkErrors(entry, property, value); | ||
if (errors.length) { | ||
throw new SmartModelError({ | ||
message: errors[0].message, | ||
property: property, | ||
code: errors[0].code, | ||
source: target.constructor.name | ||
}); | ||
const errors = checkErrors(entry, property, value, first, settings); | ||
if (errors.length) { | ||
if (settings.exceptions) { | ||
SmartModelError.throw(settings, errors[0].code, errors[0].message, property, target); | ||
} else { | ||
return true; | ||
} | ||
} | ||
if (settings.strict && !keys(entry).length) { | ||
return true; | ||
} | ||
if (Nested) { | ||
value = new Nested(value instanceof Object ? value : {}); | ||
value = new Nested(value); | ||
} | ||
target[property] = value; | ||
trigger(target.onSet); | ||
trigger(target.$onSet); | ||
if (updated) { | ||
trigger(target.onUpdate); | ||
trigger(target.$onUpdate); | ||
} | ||
@@ -176,2 +225,7 @@ return true; | ||
let value = target[property]; | ||
if (property === "$eject") { | ||
return function() { | ||
return eject(target); | ||
}; | ||
} | ||
if (!entry) { | ||
@@ -181,5 +235,6 @@ return target[property]; | ||
function trigger(method, args) { | ||
return Reflect.apply(method, target, args ? args : [ property, value, schema ]); | ||
const returned = Reflect.apply(method, target, args ? args : [ property, value, schema ]); | ||
return !isUndef(returned) ? returned : value; | ||
} | ||
trigger(target.onBeforeGet); | ||
value = trigger(target.$onBeforeGet); | ||
if (isFn(entry)) { | ||
@@ -191,3 +246,3 @@ value = trigger(entry, [ target, schema ]); | ||
} | ||
trigger(target.onGet); | ||
value = trigger(target.$onGet); | ||
return value; | ||
@@ -197,3 +252,3 @@ }, | ||
const value = target[property]; | ||
const entry = schema[property]; | ||
const entry = schema[property] || {}; | ||
function trigger(method, args) { | ||
@@ -203,12 +258,8 @@ return Reflect.apply(method, target, args ? args : [ property, value, schema ]); | ||
if (entry.required) { | ||
throw new SmartModelError({ | ||
message: `Invalid delete on required propery ${property}`, | ||
property: property, | ||
code: "required" | ||
}); | ||
SmartModelError.throw(settings, "required", `Property "${property}" is "required"`, property, target); | ||
} | ||
trigger(target.onBeforeDelete); | ||
trigger(target.$onBeforeDelete); | ||
Reflect.deleteProperty(target, property); | ||
trigger(target.onDelete); | ||
trigger(target.onUpdate); | ||
trigger(target.$onDelete); | ||
trigger(target.$onUpdate); | ||
return true; | ||
@@ -223,3 +274,3 @@ } | ||
super(schema, settings); | ||
Object.keys(schema).forEach((key => { | ||
keys(schema, (key => { | ||
if (isUndef(data[key])) { | ||
@@ -233,26 +284,56 @@ if (!isUndef(schema[key].default)) { | ||
})); | ||
this.feed(data); | ||
this.$patch(data); | ||
} | ||
feed(data) { | ||
Object.keys(data).forEach((key => { | ||
$patch(data) { | ||
keys(data, (key => { | ||
this[key] = data[key]; | ||
})); | ||
} | ||
onBeforeGet() {} | ||
onBeforeSet() {} | ||
onBeforeUpdate() {} | ||
onDelete() {} | ||
onGet() {} | ||
onBeforeDelete() {} | ||
onSet() {} | ||
onUpdate() {} | ||
$put(data) { | ||
keys(this, (key => { | ||
if (data[key]) { | ||
if (isSmartModel(this[key])) { | ||
this[key].$put(data[key]); | ||
} else { | ||
this[key] = data[key]; | ||
} | ||
} else { | ||
this.$delete(key); | ||
} | ||
})); | ||
keys(data, (key => { | ||
if (!this[key]) { | ||
this[key] = data[key]; | ||
} | ||
})); | ||
} | ||
$delete(properties) { | ||
toArray(properties).forEach((key => { | ||
Reflect.deleteProperty(this, key); | ||
})); | ||
} | ||
$onBeforeGet() {} | ||
$onBeforeSet() {} | ||
$onBeforeUpdate() {} | ||
$onDelete() {} | ||
$onGet() {} | ||
$onBeforeDelete() {} | ||
$onSet() {} | ||
$onUpdate() {} | ||
} | ||
SmartModel.settings = { | ||
empty: value => value === "" || value === null || isUndef(value), | ||
strict: false, | ||
exceptions: true | ||
exceptions: { | ||
readonly: false, | ||
required: true, | ||
rule: true, | ||
strict: false, | ||
type: true | ||
} | ||
}; | ||
SmartModel.create = function(name, schema, settings, prototype) { | ||
settings = Object.assign({}, SmartModel.settings, settings); | ||
settings = merge(SmartModel.settings, settings); | ||
const Model = { | ||
@@ -267,3 +348,3 @@ [name]: class extends SmartModel { | ||
const invalidations = {}; | ||
Object.keys(schema).forEach((property => { | ||
keys(schema, (property => { | ||
let subErrors; | ||
@@ -276,3 +357,3 @@ const value = payload[property]; | ||
} | ||
let errors = checkErrors(entry, property, value); | ||
let errors = checkErrors(entry, property, value, false, settings); | ||
if (subErrors) { | ||
@@ -289,3 +370,3 @@ invalidations[property] = subErrors; | ||
})); | ||
return Object.keys(invalidations).length ? invalidations : false; | ||
return keys(invalidations).length ? invalidations : false; | ||
}; | ||
@@ -292,0 +373,0 @@ Model.hydrate = function(payload) { |
@@ -1,2 +0,2 @@ | ||
/*! SmartModel v0.3.2 */ | ||
class e extends Error{constructor(e){super(e.message),Object.assign(this,e)}}function t(e){return Array.isArray(e)}function r(e){return void 0===e}function n(e){return""===e||null===e||r(e)}function o(e){return"function"==typeof e}function s(e){return e.toString().startsWith("class")}function c(e){return[].concat([],e)}function u(e,r,o){const u=[];return e.required&&n(o)?(u.push({message:`Invalid value 'required' on property '${r}'`,code:"required"}),u):(void 0===o||(!e.type||!e.required&&n(o)||c(e.type).some((e=>function(e,r){const n=r&&r.toString().match(/^\s*function (\w+)/),o=(n?n[1]:"object").toLowerCase();if("date"===o&&e instanceof r)return!0;if("array"===o&&t(e))return!0;if("object"===o){if(s(r)&&e instanceof r)return!0;if(!s(r)&&typeof e===o)return!0}else if(typeof e===o)return!0;return!1}(o,e)))||u.push({message:`Invalid type '${typeof o}' on property '${r}'`,code:"type"}),e.rule&&Object.keys(e.rule).forEach((t=>{(0,e.rule[t])(o)&&u.push({message:`Invalid value '${t}' on property '${r}'`,code:t})}))),u)}function i(e={},t,r){if(!e.type)return!1;const n=e.type.prototype instanceof a&&e.type,o="[object Object]"===e.type.toString()&&e.type;return!(!n&&!o)&&(n||a.create(t.normalize("NFD").replace(/[\u0300-\u036f]/g,"").match(/[a-z]+/gi).map((e=>e.charAt(0).toUpperCase()+e.substr(1).toLowerCase())).join(""),o,r))}class a extends class{constructor(t,r){return new Proxy(this,{set(n,s,c){let a=t[s];const f=n[s],p=(l=f,!(JSON.stringify(c)===JSON.stringify(l)));var l;const d=i(a,s,r);function y(e,r){return Reflect.apply(e,n,r||[s,c,f,t])}if(!a){if(r.strict)return!0;a={}}if(y(n.onBeforeSet),p&&y(n.onBeforeUpdate),o(a.transform)&&(c=y(a.transform,[c,t])),r.exceptions){const t=u(a,s,c);if(t.length)throw new e({message:t[0].message,property:s,code:t[0].code,source:n.constructor.name})}return d&&(c=new d(c instanceof Object?c:{})),n[s]=c,y(n.onSet),p&&y(n.onUpdate),!0},get(e,r){const n=t[r];let s=e[r];if(!n)return e[r];function c(n,o){return Reflect.apply(n,e,o||[r,s,t])}return c(e.onBeforeGet),o(n)&&(s=c(n,[e,t])),o(n.format)&&(s=c(n.format,[s,t])),c(e.onGet),s},deleteProperty(r,n){const o=r[n];function s(e,s){return Reflect.apply(e,r,s||[n,o,t])}if(t[n].required)throw new e({message:`Invalid delete on required propery ${n}`,property:n,code:"required"});return s(r.onBeforeDelete),Reflect.deleteProperty(r,n),s(r.onDelete),s(r.onUpdate),!0}})}}{constructor(e={},t={},n){super(e,n),Object.keys(e).forEach((n=>{r(t[n])&&(this[n]=r(e[n].default)?t[n]:e[n].default)})),this.feed(t)}feed(e){Object.keys(e).forEach((t=>{this[t]=e[t]}))}onBeforeGet(){}onBeforeSet(){}onBeforeUpdate(){}onDelete(){}onGet(){}onBeforeDelete(){}onSet(){}onUpdate(){}}a.settings={strict:!1,exceptions:!0},a.create=function(e,r,n,o){n=Object.assign({},a.settings,n);const s={[e]:class extends a{constructor(e){super(r,e,n)}}}[e];return s.checkErrors=function(e,t){const o={};return Object.keys(r).forEach((s=>{let a;const f=e[s],p=r[s],l=i(p,s,n);l&&(a=l.checkErrors(f,t));let d=u(p,s,f);a?o[s]=a:d.length&&(t&&(d=d.filter((e=>!c(t).includes(e.code)))),d.length&&(o[s]=d))})),!!Object.keys(o).length&&o},s.hydrate=function(e){return t(e)?e.map((e=>new s(e))):new s(e)},Object.assign(s.prototype,o),s.schema=r,s};export default a; | ||
/*! SmartModel v0.3.3 */ | ||
function e(e){return Array.isArray(e)}function t(e){return void 0===e}function r(e){return"function"==typeof e}function n(e){return e&&e.toString().startsWith("class")}function o(e){return e.prototype instanceof l||e instanceof l}function s(e){return e&&"[object Object]"===e.toString()}function c(e,t=function(){}){return Object.keys(e).map(t)}function i(e){return[].concat([],e)}function u(e,t){return t=Object.assign({},e,t),c(e,(r=>{s(e[r])&&s(t[r])&&(t[r]=Object.assign({},e[r],u(e[r],t[r])))})),t}class a extends Error{constructor(e){super(e.message),Object.assign(this,e)}}function f(t,r,u,a,f){const p=[];return!f.strict||t&&c(t).length||p.push({message:`Property "${r}" can't be set in strict mode`,code:"strict"}),t.required&&f.empty(u)?(p.push({message:`Property "${r}" is "required"`,code:"required"}),p):t.readonly&&!a?(p.push({message:`Property '${r}' is 'readonly'`,code:"readonly"}),p):(void 0===u||(!t.type||!t.required&&f.empty(u)||o(t.type)&&s(u)||i(t.type).some((t=>function(t,r){const o=r&&r.toString().match(/^\s*function (\w+)/),s=(o?o[1]:"object").toLowerCase();if("date"===s&&t instanceof r)return!0;if("array"===s&&e(t))return!0;if("object"===s){if(n(r)&&t instanceof r)return!0;if(!n(r)&&typeof t===s)return!0}else if(typeof t===s)return!0;return!1}(u,t)))||p.push({message:`Property "${r}" has an invalid type "${typeof u}"`,code:"type"}),t.rule&&c(t.rule,(e=>{(0,t.rule[e])(u)&&p.push({message:`Property "${r}" breaks the "${e}" rule`,code:"rule:"+e})}))),p)}function p(e={},t,r){if(!e.type)return!1;const n=!!o(e.type)&&e.type,c=!!s(e.type)&&e.type;if(n||c){const o=n||l.create(t.normalize("NFD").replace(/[\u0300-\u036f]/g,"").match(/[a-z1-9]+/gi).map((e=>e.charAt(0).toUpperCase()+e.substr(1).toLowerCase())).join(""),c,r);return e.type=o,o}return!1}a.throw=function(e,t,r,n,o){const c=t.split(":")[0];if(!0===e.exceptions||s(e.exceptions)&&e.exceptions[c])throw new a({message:r,property:n,code:t,source:o&&o.constructor.name})};class l extends class{constructor(e,n){return new Proxy(this,{set(o,s,i){const u=e[s]||{},l=o[s],y=t(l),d=!(y||(h=i,$=l,JSON.stringify(h)===JSON.stringify($)));var h,$;const g=p(u,s,n);function m(r,n){const c=Reflect.apply(r,o,n||[s,i,l,e]);return t(c)?i:c}i=m(o.$onBeforeSet),d&&(i=m(o.$onBeforeUpdate)),r(u.transform)&&(i=m(u.transform,[i,e]));const b=f(u,s,i,y,n);if(b.length){if(!n.exceptions)return!0;a.throw(n,b[0].code,b[0].message,s,o)}return n.strict&&!c(u).length||(g&&(i=new g(i)),o[s]=i,m(o.$onSet),d&&m(o.$onUpdate)),!0},get(n,s){const i=e[s];let u=n[s];if("$eject"===s)return function(){return function(e){return c(e=Object.assign({},e),(t=>{o(e[t])&&(e[t]=e[t].$eject())})),e}(n)};if(!i)return n[s];function a(r,o){const c=Reflect.apply(r,n,o||[s,u,e]);return t(c)?u:c}return u=a(n.$onBeforeGet),r(i)&&(u=a(i,[n,e])),r(i.format)&&(u=a(i.format,[u,e])),u=a(n.$onGet),u},deleteProperty(t,r){const o=t[r];function s(n,s){return Reflect.apply(n,t,s||[r,o,e])}return(e[r]||{}).required&&a.throw(n,"required",`Property "${r}" is "required"`,r,t),s(t.$onBeforeDelete),Reflect.deleteProperty(t,r),s(t.$onDelete),s(t.$onUpdate),!0}})}}{constructor(e={},r={},n){super(e,n),c(e,(n=>{t(r[n])&&(this[n]=t(e[n].default)?r[n]:e[n].default)})),this.$patch(r)}$patch(e){c(e,(t=>{this[t]=e[t]}))}$put(e){c(this,(t=>{e[t]?o(this[t])?this[t].$put(e[t]):this[t]=e[t]:this.$delete(t)})),c(e,(t=>{this[t]||(this[t]=e[t])}))}$delete(e){i(e).forEach((e=>{Reflect.deleteProperty(this,e)}))}$onBeforeGet(){}$onBeforeSet(){}$onBeforeUpdate(){}$onDelete(){}$onGet(){}$onBeforeDelete(){}$onSet(){}$onUpdate(){}}l.settings={empty:e=>""===e||null===e||t(e),strict:!1,exceptions:{readonly:!1,required:!0,rule:!0,strict:!1,type:!0}},l.create=function(t,r,n,o){n=u(l.settings,n);const s={[t]:class extends l{constructor(e){super(r,e,n)}}}[t];return s.checkErrors=function(e,t){const o={};return c(r,(s=>{let c;const u=e[s],a=r[s],l=p(a,s,n);l&&(c=l.checkErrors(u,t));let y=f(a,s,u,!1,n);c?o[s]=c:y.length&&(t&&(y=y.filter((e=>!i(t).includes(e.code)))),y.length&&(o[s]=y))})),!!c(o).length&&o},s.hydrate=function(t){return e(t)?t.map((e=>new s(e))):new s(t)},Object.assign(s.prototype,o),s.schema=r,s};export default l; |
@@ -5,3 +5,3 @@ /**! | ||
* https://github.com/jaysalvat/smart-model | ||
* @version 0.3.2 built 2021-02-22 10:45:15 | ||
* @version 0.3.3 built 2021-02-23 12:22:54 | ||
* @license ISC | ||
@@ -12,8 +12,2 @@ * @author Jay Salvat http://jaysalvat.com | ||
"use strict"; | ||
class SmartModelError extends Error { | ||
constructor(data) { | ||
super(data.message); | ||
Object.assign(this, data); | ||
} | ||
} | ||
function isArray(value) { | ||
@@ -25,5 +19,2 @@ return Array.isArray(value); | ||
} | ||
function isEmpty(value) { | ||
return value === "" || value === null || isUndef(value); | ||
} | ||
function isFn(value) { | ||
@@ -36,8 +27,38 @@ return typeof value === "function"; | ||
function isClass(value) { | ||
return value.toString().startsWith("class"); | ||
return value && value.toString().startsWith("class"); | ||
} | ||
function isSmartModel(value) { | ||
return value.prototype instanceof SmartModel || value instanceof SmartModel; | ||
} | ||
function isPlainObject(value) { | ||
return value.toString() === "[object Object]"; | ||
return value && value.toString() === "[object Object]"; | ||
} | ||
function isType(value, Type) { | ||
function keys(obj, cb = function() {}) { | ||
return Object.keys(obj).map(cb); | ||
} | ||
function toArray(value) { | ||
return [].concat([], value); | ||
} | ||
function merge(source, target) { | ||
target = Object.assign({}, source, target); | ||
keys(source, (key => { | ||
if (isPlainObject(source[key]) && isPlainObject(target[key])) { | ||
target[key] = Object.assign({}, source[key], merge(source[key], target[key])); | ||
} | ||
})); | ||
return target; | ||
} | ||
function eject(target) { | ||
target = Object.assign({}, target); | ||
keys(target, (key => { | ||
if (isSmartModel(target[key])) { | ||
target[key] = target[key].$eject(); | ||
} | ||
})); | ||
return target; | ||
} | ||
function pascalCase(string) { | ||
return string.normalize("NFD").replace(/[\u0300-\u036f]/g, "").match(/[a-z1-9]+/gi).map((word => word.charAt(0).toUpperCase() + word.substr(1).toLowerCase())).join(""); | ||
} | ||
function checkType(value, Type) { | ||
const match = Type && Type.toString().match(/^\s*function (\w+)/); | ||
@@ -63,13 +84,30 @@ const type = (match ? match[1] : "object").toLowerCase(); | ||
} | ||
function toArray(value) { | ||
return [].concat([], value); | ||
class SmartModelError extends Error { | ||
constructor(data) { | ||
super(data.message); | ||
Object.assign(this, data); | ||
} | ||
} | ||
function pascalCase(string) { | ||
return string.normalize("NFD").replace(/[\u0300-\u036f]/g, "").match(/[a-z]+/gi).map((word => word.charAt(0).toUpperCase() + word.substr(1).toLowerCase())).join(""); | ||
} | ||
function checkErrors(entry, property, value) { | ||
SmartModelError.throw = function(settings, code, message, property, source) { | ||
const shortCode = code.split(":")[0]; | ||
if (settings.exceptions === true || isPlainObject(settings.exceptions) && settings.exceptions[shortCode]) { | ||
throw new SmartModelError({ | ||
message: message, | ||
property: property, | ||
code: code, | ||
source: source && source.constructor.name | ||
}); | ||
} | ||
}; | ||
function checkErrors(entry, property, value, first, settings) { | ||
const errors = []; | ||
if (entry.required && isEmpty(value)) { | ||
if (settings.strict && (!entry || !keys(entry).length)) { | ||
errors.push({ | ||
message: `Invalid value 'required' on property '${property}'`, | ||
message: `Property "${property}" can't be set in strict mode`, | ||
code: "strict" | ||
}); | ||
} | ||
if (entry.required && settings.empty(value)) { | ||
errors.push({ | ||
message: `Property "${property}" is "required"`, | ||
code: "required" | ||
@@ -79,20 +117,29 @@ }); | ||
} | ||
if (entry.readonly && !first) { | ||
errors.push({ | ||
message: `Property '${property}' is 'readonly'`, | ||
code: "readonly" | ||
}); | ||
return errors; | ||
} | ||
if (typeof value === "undefined") { | ||
return errors; | ||
} | ||
if (entry.type && (entry.required || !isEmpty(value))) { | ||
if (!toArray(entry.type).some((type => isType(value, type)))) { | ||
errors.push({ | ||
message: `Invalid type '${typeof value}' on property '${property}'`, | ||
code: "type" | ||
}); | ||
if (entry.type && (entry.required || !settings.empty(value))) { | ||
if (!(isSmartModel(entry.type) && isPlainObject(value))) { | ||
if (!toArray(entry.type).some((type => checkType(value, type)))) { | ||
errors.push({ | ||
message: `Property "${property}" has an invalid type "${typeof value}"`, | ||
code: "type" | ||
}); | ||
} | ||
} | ||
} | ||
if (entry.rule) { | ||
Object.keys(entry.rule).forEach((key => { | ||
keys(entry.rule, (key => { | ||
const rule = entry.rule[key]; | ||
if (rule(value)) { | ||
errors.push({ | ||
message: `Invalid value '${key}' on property '${property}'`, | ||
code: key | ||
message: `Property "${property}" breaks the "${key}" rule`, | ||
code: "rule:" + key | ||
}); | ||
@@ -108,6 +155,8 @@ } | ||
} | ||
const Child = entry.type.prototype instanceof SmartModel ? entry.type : false; | ||
const Child = isSmartModel(entry.type) ? entry.type : false; | ||
const schema = isPlainObject(entry.type) ? entry.type : false; | ||
if (Child || schema) { | ||
return Child ? Child : SmartModel.create(pascalCase(property), schema, settings); | ||
const Model = Child ? Child : SmartModel.create(pascalCase(property), schema, settings); | ||
entry.type = Model; | ||
return Model; | ||
} | ||
@@ -120,18 +169,14 @@ return false; | ||
set(target, property, value) { | ||
let entry = schema[property]; | ||
const entry = schema[property] || {}; | ||
const old = target[property]; | ||
const updated = !isEqual(value, old); | ||
const first = isUndef(old); | ||
const updated = !first && !isEqual(value, old); | ||
const Nested = createNested(entry, property, settings); | ||
function trigger(method, args) { | ||
return Reflect.apply(method, target, args ? args : [ property, value, old, schema ]); | ||
const returned = Reflect.apply(method, target, args ? args : [ property, value, old, schema ]); | ||
return !isUndef(returned) ? returned : value; | ||
} | ||
if (!entry) { | ||
if (settings.strict) { | ||
return true; | ||
} | ||
entry = {}; | ||
} | ||
trigger(target.onBeforeSet); | ||
value = trigger(target.$onBeforeSet); | ||
if (updated) { | ||
trigger(target.onBeforeUpdate); | ||
value = trigger(target.$onBeforeUpdate); | ||
} | ||
@@ -141,20 +186,20 @@ if (isFn(entry.transform)) { | ||
} | ||
if (settings.exceptions) { | ||
const errors = checkErrors(entry, property, value); | ||
if (errors.length) { | ||
throw new SmartModelError({ | ||
message: errors[0].message, | ||
property: property, | ||
code: errors[0].code, | ||
source: target.constructor.name | ||
}); | ||
const errors = checkErrors(entry, property, value, first, settings); | ||
if (errors.length) { | ||
if (settings.exceptions) { | ||
SmartModelError.throw(settings, errors[0].code, errors[0].message, property, target); | ||
} else { | ||
return true; | ||
} | ||
} | ||
if (settings.strict && !keys(entry).length) { | ||
return true; | ||
} | ||
if (Nested) { | ||
value = new Nested(value instanceof Object ? value : {}); | ||
value = new Nested(value); | ||
} | ||
target[property] = value; | ||
trigger(target.onSet); | ||
trigger(target.$onSet); | ||
if (updated) { | ||
trigger(target.onUpdate); | ||
trigger(target.$onUpdate); | ||
} | ||
@@ -166,2 +211,7 @@ return true; | ||
let value = target[property]; | ||
if (property === "$eject") { | ||
return function() { | ||
return eject(target); | ||
}; | ||
} | ||
if (!entry) { | ||
@@ -171,5 +221,6 @@ return target[property]; | ||
function trigger(method, args) { | ||
return Reflect.apply(method, target, args ? args : [ property, value, schema ]); | ||
const returned = Reflect.apply(method, target, args ? args : [ property, value, schema ]); | ||
return !isUndef(returned) ? returned : value; | ||
} | ||
trigger(target.onBeforeGet); | ||
value = trigger(target.$onBeforeGet); | ||
if (isFn(entry)) { | ||
@@ -181,3 +232,3 @@ value = trigger(entry, [ target, schema ]); | ||
} | ||
trigger(target.onGet); | ||
value = trigger(target.$onGet); | ||
return value; | ||
@@ -187,3 +238,3 @@ }, | ||
const value = target[property]; | ||
const entry = schema[property]; | ||
const entry = schema[property] || {}; | ||
function trigger(method, args) { | ||
@@ -193,12 +244,8 @@ return Reflect.apply(method, target, args ? args : [ property, value, schema ]); | ||
if (entry.required) { | ||
throw new SmartModelError({ | ||
message: `Invalid delete on required propery ${property}`, | ||
property: property, | ||
code: "required" | ||
}); | ||
SmartModelError.throw(settings, "required", `Property "${property}" is "required"`, property, target); | ||
} | ||
trigger(target.onBeforeDelete); | ||
trigger(target.$onBeforeDelete); | ||
Reflect.deleteProperty(target, property); | ||
trigger(target.onDelete); | ||
trigger(target.onUpdate); | ||
trigger(target.$onDelete); | ||
trigger(target.$onUpdate); | ||
return true; | ||
@@ -212,3 +259,3 @@ } | ||
super(schema, settings); | ||
Object.keys(schema).forEach((key => { | ||
keys(schema, (key => { | ||
if (isUndef(data[key])) { | ||
@@ -222,24 +269,54 @@ if (!isUndef(schema[key].default)) { | ||
})); | ||
this.feed(data); | ||
this.$patch(data); | ||
} | ||
feed(data) { | ||
Object.keys(data).forEach((key => { | ||
$patch(data) { | ||
keys(data, (key => { | ||
this[key] = data[key]; | ||
})); | ||
} | ||
onBeforeGet() {} | ||
onBeforeSet() {} | ||
onBeforeUpdate() {} | ||
onDelete() {} | ||
onGet() {} | ||
onBeforeDelete() {} | ||
onSet() {} | ||
onUpdate() {} | ||
$put(data) { | ||
keys(this, (key => { | ||
if (data[key]) { | ||
if (isSmartModel(this[key])) { | ||
this[key].$put(data[key]); | ||
} else { | ||
this[key] = data[key]; | ||
} | ||
} else { | ||
this.$delete(key); | ||
} | ||
})); | ||
keys(data, (key => { | ||
if (!this[key]) { | ||
this[key] = data[key]; | ||
} | ||
})); | ||
} | ||
$delete(properties) { | ||
toArray(properties).forEach((key => { | ||
Reflect.deleteProperty(this, key); | ||
})); | ||
} | ||
$onBeforeGet() {} | ||
$onBeforeSet() {} | ||
$onBeforeUpdate() {} | ||
$onDelete() {} | ||
$onGet() {} | ||
$onBeforeDelete() {} | ||
$onSet() {} | ||
$onUpdate() {} | ||
} | ||
SmartModel.settings = { | ||
empty: value => value === "" || value === null || isUndef(value), | ||
strict: false, | ||
exceptions: true | ||
exceptions: { | ||
readonly: false, | ||
required: true, | ||
rule: true, | ||
strict: false, | ||
type: true | ||
} | ||
}; | ||
SmartModel.create = function(name, schema, settings, prototype) { | ||
settings = Object.assign({}, SmartModel.settings, settings); | ||
settings = merge(SmartModel.settings, settings); | ||
const Model = { | ||
@@ -254,3 +331,3 @@ [name]: class extends SmartModel { | ||
const invalidations = {}; | ||
Object.keys(schema).forEach((property => { | ||
keys(schema, (property => { | ||
let subErrors; | ||
@@ -263,3 +340,3 @@ const value = payload[property]; | ||
} | ||
let errors = checkErrors(entry, property, value); | ||
let errors = checkErrors(entry, property, value, false, settings); | ||
if (subErrors) { | ||
@@ -276,3 +353,3 @@ invalidations[property] = subErrors; | ||
})); | ||
return Object.keys(invalidations).length ? invalidations : false; | ||
return keys(invalidations).length ? invalidations : false; | ||
}; | ||
@@ -279,0 +356,0 @@ Model.hydrate = function(payload) { |
@@ -1,2 +0,2 @@ | ||
/*! SmartModel v0.3.2 */ | ||
var SmartModel=function(){"use strict";class e extends Error{constructor(e){super(e.message),Object.assign(this,e)}}function t(e){return Array.isArray(e)}function r(e){return void 0===e}function n(e){return""===e||null===e||r(e)}function o(e){return"function"==typeof e}function s(e){return e.toString().startsWith("class")}function c(e){return[].concat([],e)}function u(e,r,o){const u=[];return e.required&&n(o)?(u.push({message:`Invalid value 'required' on property '${r}'`,code:"required"}),u):(void 0===o||(!e.type||!e.required&&n(o)||c(e.type).some((e=>function(e,r){const n=r&&r.toString().match(/^\s*function (\w+)/),o=(n?n[1]:"object").toLowerCase();if("date"===o&&e instanceof r)return!0;if("array"===o&&t(e))return!0;if("object"===o){if(s(r)&&e instanceof r)return!0;if(!s(r)&&typeof e===o)return!0}else if(typeof e===o)return!0;return!1}(o,e)))||u.push({message:`Invalid type '${typeof o}' on property '${r}'`,code:"type"}),e.rule&&Object.keys(e.rule).forEach((t=>{(0,e.rule[t])(o)&&u.push({message:`Invalid value '${t}' on property '${r}'`,code:t})}))),u)}function i(e={},t,r){if(!e.type)return!1;const n=e.type.prototype instanceof a&&e.type,o="[object Object]"===e.type.toString()&&e.type;return!(!n&&!o)&&(n||a.create(t.normalize("NFD").replace(/[\u0300-\u036f]/g,"").match(/[a-z]+/gi).map((e=>e.charAt(0).toUpperCase()+e.substr(1).toLowerCase())).join(""),o,r))}class a extends class{constructor(t,r){return new Proxy(this,{set(n,s,c){let a=t[s];const f=n[s],p=(l=f,!(JSON.stringify(c)===JSON.stringify(l)));var l;const d=i(a,s,r);function y(e,r){return Reflect.apply(e,n,r||[s,c,f,t])}if(!a){if(r.strict)return!0;a={}}if(y(n.onBeforeSet),p&&y(n.onBeforeUpdate),o(a.transform)&&(c=y(a.transform,[c,t])),r.exceptions){const t=u(a,s,c);if(t.length)throw new e({message:t[0].message,property:s,code:t[0].code,source:n.constructor.name})}return d&&(c=new d(c instanceof Object?c:{})),n[s]=c,y(n.onSet),p&&y(n.onUpdate),!0},get(e,r){const n=t[r];let s=e[r];if(!n)return e[r];function c(n,o){return Reflect.apply(n,e,o||[r,s,t])}return c(e.onBeforeGet),o(n)&&(s=c(n,[e,t])),o(n.format)&&(s=c(n.format,[s,t])),c(e.onGet),s},deleteProperty(r,n){const o=r[n];function s(e,s){return Reflect.apply(e,r,s||[n,o,t])}if(t[n].required)throw new e({message:`Invalid delete on required propery ${n}`,property:n,code:"required"});return s(r.onBeforeDelete),Reflect.deleteProperty(r,n),s(r.onDelete),s(r.onUpdate),!0}})}}{constructor(e={},t={},n){super(e,n),Object.keys(e).forEach((n=>{r(t[n])&&(this[n]=r(e[n].default)?t[n]:e[n].default)})),this.feed(t)}feed(e){Object.keys(e).forEach((t=>{this[t]=e[t]}))}onBeforeGet(){}onBeforeSet(){}onBeforeUpdate(){}onDelete(){}onGet(){}onBeforeDelete(){}onSet(){}onUpdate(){}}return a.settings={strict:!1,exceptions:!0},a.create=function(e,r,n,o){n=Object.assign({},a.settings,n);const s={[e]:class extends a{constructor(e){super(r,e,n)}}}[e];return s.checkErrors=function(e,t){const o={};return Object.keys(r).forEach((s=>{let a;const f=e[s],p=r[s],l=i(p,s,n);l&&(a=l.checkErrors(f,t));let d=u(p,s,f);a?o[s]=a:d.length&&(t&&(d=d.filter((e=>!c(t).includes(e.code)))),d.length&&(o[s]=d))})),!!Object.keys(o).length&&o},s.hydrate=function(e){return t(e)?e.map((e=>new s(e))):new s(e)},Object.assign(s.prototype,o),s.schema=r,s},a}(); | ||
/*! SmartModel v0.3.3 */ | ||
var SmartModel=function(){"use strict";function e(e){return Array.isArray(e)}function t(e){return void 0===e}function r(e){return"function"==typeof e}function n(e){return e&&e.toString().startsWith("class")}function o(e){return e.prototype instanceof l||e instanceof l}function s(e){return e&&"[object Object]"===e.toString()}function c(e,t=function(){}){return Object.keys(e).map(t)}function i(e){return[].concat([],e)}function u(e,t){return t=Object.assign({},e,t),c(e,(r=>{s(e[r])&&s(t[r])&&(t[r]=Object.assign({},e[r],u(e[r],t[r])))})),t}class a extends Error{constructor(e){super(e.message),Object.assign(this,e)}}function f(t,r,u,a,f){const p=[];return!f.strict||t&&c(t).length||p.push({message:`Property "${r}" can't be set in strict mode`,code:"strict"}),t.required&&f.empty(u)?(p.push({message:`Property "${r}" is "required"`,code:"required"}),p):t.readonly&&!a?(p.push({message:`Property '${r}' is 'readonly'`,code:"readonly"}),p):(void 0===u||(!t.type||!t.required&&f.empty(u)||o(t.type)&&s(u)||i(t.type).some((t=>function(t,r){const o=r&&r.toString().match(/^\s*function (\w+)/),s=(o?o[1]:"object").toLowerCase();if("date"===s&&t instanceof r)return!0;if("array"===s&&e(t))return!0;if("object"===s){if(n(r)&&t instanceof r)return!0;if(!n(r)&&typeof t===s)return!0}else if(typeof t===s)return!0;return!1}(u,t)))||p.push({message:`Property "${r}" has an invalid type "${typeof u}"`,code:"type"}),t.rule&&c(t.rule,(e=>{(0,t.rule[e])(u)&&p.push({message:`Property "${r}" breaks the "${e}" rule`,code:"rule:"+e})}))),p)}function p(e={},t,r){if(!e.type)return!1;const n=!!o(e.type)&&e.type,c=!!s(e.type)&&e.type;if(n||c){const o=n||l.create(t.normalize("NFD").replace(/[\u0300-\u036f]/g,"").match(/[a-z1-9]+/gi).map((e=>e.charAt(0).toUpperCase()+e.substr(1).toLowerCase())).join(""),c,r);return e.type=o,o}return!1}a.throw=function(e,t,r,n,o){const c=t.split(":")[0];if(!0===e.exceptions||s(e.exceptions)&&e.exceptions[c])throw new a({message:r,property:n,code:t,source:o&&o.constructor.name})};class l extends class{constructor(e,n){return new Proxy(this,{set(o,s,i){const u=e[s]||{},l=o[s],y=t(l),d=!(y||(h=i,$=l,JSON.stringify(h)===JSON.stringify($)));var h,$;const g=p(u,s,n);function m(r,n){const c=Reflect.apply(r,o,n||[s,i,l,e]);return t(c)?i:c}i=m(o.$onBeforeSet),d&&(i=m(o.$onBeforeUpdate)),r(u.transform)&&(i=m(u.transform,[i,e]));const b=f(u,s,i,y,n);if(b.length){if(!n.exceptions)return!0;a.throw(n,b[0].code,b[0].message,s,o)}return n.strict&&!c(u).length||(g&&(i=new g(i)),o[s]=i,m(o.$onSet),d&&m(o.$onUpdate)),!0},get(n,s){const i=e[s];let u=n[s];if("$eject"===s)return function(){return function(e){return c(e=Object.assign({},e),(t=>{o(e[t])&&(e[t]=e[t].$eject())})),e}(n)};if(!i)return n[s];function a(r,o){const c=Reflect.apply(r,n,o||[s,u,e]);return t(c)?u:c}return u=a(n.$onBeforeGet),r(i)&&(u=a(i,[n,e])),r(i.format)&&(u=a(i.format,[u,e])),u=a(n.$onGet),u},deleteProperty(t,r){const o=t[r];function s(n,s){return Reflect.apply(n,t,s||[r,o,e])}return(e[r]||{}).required&&a.throw(n,"required",`Property "${r}" is "required"`,r,t),s(t.$onBeforeDelete),Reflect.deleteProperty(t,r),s(t.$onDelete),s(t.$onUpdate),!0}})}}{constructor(e={},r={},n){super(e,n),c(e,(n=>{t(r[n])&&(this[n]=t(e[n].default)?r[n]:e[n].default)})),this.$patch(r)}$patch(e){c(e,(t=>{this[t]=e[t]}))}$put(e){c(this,(t=>{e[t]?o(this[t])?this[t].$put(e[t]):this[t]=e[t]:this.$delete(t)})),c(e,(t=>{this[t]||(this[t]=e[t])}))}$delete(e){i(e).forEach((e=>{Reflect.deleteProperty(this,e)}))}$onBeforeGet(){}$onBeforeSet(){}$onBeforeUpdate(){}$onDelete(){}$onGet(){}$onBeforeDelete(){}$onSet(){}$onUpdate(){}}return l.settings={empty:e=>""===e||null===e||t(e),strict:!1,exceptions:{readonly:!1,required:!0,rule:!0,strict:!1,type:!0}},l.create=function(t,r,n,o){n=u(l.settings,n);const s={[t]:class extends l{constructor(e){super(r,e,n)}}}[t];return s.checkErrors=function(e,t){const o={};return c(r,(s=>{let c;const u=e[s],a=r[s],l=p(a,s,n);l&&(c=l.checkErrors(u,t));let y=f(a,s,u,!1,n);c?o[s]=c:y.length&&(t&&(y=y.filter((e=>!i(t).includes(e.code)))),y.length&&(o[s]=y))})),!!c(o).length&&o},s.hydrate=function(t){return e(t)?t.map((e=>new s(e))):new s(t)},Object.assign(s.prototype,o),s.schema=r,s},l}(); |
@@ -5,3 +5,3 @@ /**! | ||
* https://github.com/jaysalvat/smart-model | ||
* @version 0.3.2 built 2021-02-22 10:45:15 | ||
* @version 0.3.3 built 2021-02-23 12:22:54 | ||
* @license ISC | ||
@@ -15,8 +15,2 @@ * @author Jay Salvat http://jaysalvat.com | ||
"use strict"; | ||
class SmartModelError extends Error { | ||
constructor(data) { | ||
super(data.message); | ||
Object.assign(this, data); | ||
} | ||
} | ||
function isArray(value) { | ||
@@ -28,5 +22,2 @@ return Array.isArray(value); | ||
} | ||
function isEmpty(value) { | ||
return value === "" || value === null || isUndef(value); | ||
} | ||
function isFn(value) { | ||
@@ -39,8 +30,38 @@ return typeof value === "function"; | ||
function isClass(value) { | ||
return value.toString().startsWith("class"); | ||
return value && value.toString().startsWith("class"); | ||
} | ||
function isSmartModel(value) { | ||
return value.prototype instanceof SmartModel || value instanceof SmartModel; | ||
} | ||
function isPlainObject(value) { | ||
return value.toString() === "[object Object]"; | ||
return value && value.toString() === "[object Object]"; | ||
} | ||
function isType(value, Type) { | ||
function keys(obj, cb = function() {}) { | ||
return Object.keys(obj).map(cb); | ||
} | ||
function toArray(value) { | ||
return [].concat([], value); | ||
} | ||
function merge(source, target) { | ||
target = Object.assign({}, source, target); | ||
keys(source, (key => { | ||
if (isPlainObject(source[key]) && isPlainObject(target[key])) { | ||
target[key] = Object.assign({}, source[key], merge(source[key], target[key])); | ||
} | ||
})); | ||
return target; | ||
} | ||
function eject(target) { | ||
target = Object.assign({}, target); | ||
keys(target, (key => { | ||
if (isSmartModel(target[key])) { | ||
target[key] = target[key].$eject(); | ||
} | ||
})); | ||
return target; | ||
} | ||
function pascalCase(string) { | ||
return string.normalize("NFD").replace(/[\u0300-\u036f]/g, "").match(/[a-z1-9]+/gi).map((word => word.charAt(0).toUpperCase() + word.substr(1).toLowerCase())).join(""); | ||
} | ||
function checkType(value, Type) { | ||
const match = Type && Type.toString().match(/^\s*function (\w+)/); | ||
@@ -66,13 +87,30 @@ const type = (match ? match[1] : "object").toLowerCase(); | ||
} | ||
function toArray(value) { | ||
return [].concat([], value); | ||
class SmartModelError extends Error { | ||
constructor(data) { | ||
super(data.message); | ||
Object.assign(this, data); | ||
} | ||
} | ||
function pascalCase(string) { | ||
return string.normalize("NFD").replace(/[\u0300-\u036f]/g, "").match(/[a-z]+/gi).map((word => word.charAt(0).toUpperCase() + word.substr(1).toLowerCase())).join(""); | ||
} | ||
function checkErrors(entry, property, value) { | ||
SmartModelError.throw = function(settings, code, message, property, source) { | ||
const shortCode = code.split(":")[0]; | ||
if (settings.exceptions === true || isPlainObject(settings.exceptions) && settings.exceptions[shortCode]) { | ||
throw new SmartModelError({ | ||
message: message, | ||
property: property, | ||
code: code, | ||
source: source && source.constructor.name | ||
}); | ||
} | ||
}; | ||
function checkErrors(entry, property, value, first, settings) { | ||
const errors = []; | ||
if (entry.required && isEmpty(value)) { | ||
if (settings.strict && (!entry || !keys(entry).length)) { | ||
errors.push({ | ||
message: `Invalid value 'required' on property '${property}'`, | ||
message: `Property "${property}" can't be set in strict mode`, | ||
code: "strict" | ||
}); | ||
} | ||
if (entry.required && settings.empty(value)) { | ||
errors.push({ | ||
message: `Property "${property}" is "required"`, | ||
code: "required" | ||
@@ -82,20 +120,29 @@ }); | ||
} | ||
if (entry.readonly && !first) { | ||
errors.push({ | ||
message: `Property '${property}' is 'readonly'`, | ||
code: "readonly" | ||
}); | ||
return errors; | ||
} | ||
if (typeof value === "undefined") { | ||
return errors; | ||
} | ||
if (entry.type && (entry.required || !isEmpty(value))) { | ||
if (!toArray(entry.type).some((type => isType(value, type)))) { | ||
errors.push({ | ||
message: `Invalid type '${typeof value}' on property '${property}'`, | ||
code: "type" | ||
}); | ||
if (entry.type && (entry.required || !settings.empty(value))) { | ||
if (!(isSmartModel(entry.type) && isPlainObject(value))) { | ||
if (!toArray(entry.type).some((type => checkType(value, type)))) { | ||
errors.push({ | ||
message: `Property "${property}" has an invalid type "${typeof value}"`, | ||
code: "type" | ||
}); | ||
} | ||
} | ||
} | ||
if (entry.rule) { | ||
Object.keys(entry.rule).forEach((key => { | ||
keys(entry.rule, (key => { | ||
const rule = entry.rule[key]; | ||
if (rule(value)) { | ||
errors.push({ | ||
message: `Invalid value '${key}' on property '${property}'`, | ||
code: key | ||
message: `Property "${property}" breaks the "${key}" rule`, | ||
code: "rule:" + key | ||
}); | ||
@@ -111,6 +158,8 @@ } | ||
} | ||
const Child = entry.type.prototype instanceof SmartModel ? entry.type : false; | ||
const Child = isSmartModel(entry.type) ? entry.type : false; | ||
const schema = isPlainObject(entry.type) ? entry.type : false; | ||
if (Child || schema) { | ||
return Child ? Child : SmartModel.create(pascalCase(property), schema, settings); | ||
const Model = Child ? Child : SmartModel.create(pascalCase(property), schema, settings); | ||
entry.type = Model; | ||
return Model; | ||
} | ||
@@ -123,18 +172,14 @@ return false; | ||
set(target, property, value) { | ||
let entry = schema[property]; | ||
const entry = schema[property] || {}; | ||
const old = target[property]; | ||
const updated = !isEqual(value, old); | ||
const first = isUndef(old); | ||
const updated = !first && !isEqual(value, old); | ||
const Nested = createNested(entry, property, settings); | ||
function trigger(method, args) { | ||
return Reflect.apply(method, target, args ? args : [ property, value, old, schema ]); | ||
const returned = Reflect.apply(method, target, args ? args : [ property, value, old, schema ]); | ||
return !isUndef(returned) ? returned : value; | ||
} | ||
if (!entry) { | ||
if (settings.strict) { | ||
return true; | ||
} | ||
entry = {}; | ||
} | ||
trigger(target.onBeforeSet); | ||
value = trigger(target.$onBeforeSet); | ||
if (updated) { | ||
trigger(target.onBeforeUpdate); | ||
value = trigger(target.$onBeforeUpdate); | ||
} | ||
@@ -144,20 +189,20 @@ if (isFn(entry.transform)) { | ||
} | ||
if (settings.exceptions) { | ||
const errors = checkErrors(entry, property, value); | ||
if (errors.length) { | ||
throw new SmartModelError({ | ||
message: errors[0].message, | ||
property: property, | ||
code: errors[0].code, | ||
source: target.constructor.name | ||
}); | ||
const errors = checkErrors(entry, property, value, first, settings); | ||
if (errors.length) { | ||
if (settings.exceptions) { | ||
SmartModelError.throw(settings, errors[0].code, errors[0].message, property, target); | ||
} else { | ||
return true; | ||
} | ||
} | ||
if (settings.strict && !keys(entry).length) { | ||
return true; | ||
} | ||
if (Nested) { | ||
value = new Nested(value instanceof Object ? value : {}); | ||
value = new Nested(value); | ||
} | ||
target[property] = value; | ||
trigger(target.onSet); | ||
trigger(target.$onSet); | ||
if (updated) { | ||
trigger(target.onUpdate); | ||
trigger(target.$onUpdate); | ||
} | ||
@@ -169,2 +214,7 @@ return true; | ||
let value = target[property]; | ||
if (property === "$eject") { | ||
return function() { | ||
return eject(target); | ||
}; | ||
} | ||
if (!entry) { | ||
@@ -174,5 +224,6 @@ return target[property]; | ||
function trigger(method, args) { | ||
return Reflect.apply(method, target, args ? args : [ property, value, schema ]); | ||
const returned = Reflect.apply(method, target, args ? args : [ property, value, schema ]); | ||
return !isUndef(returned) ? returned : value; | ||
} | ||
trigger(target.onBeforeGet); | ||
value = trigger(target.$onBeforeGet); | ||
if (isFn(entry)) { | ||
@@ -184,3 +235,3 @@ value = trigger(entry, [ target, schema ]); | ||
} | ||
trigger(target.onGet); | ||
value = trigger(target.$onGet); | ||
return value; | ||
@@ -190,3 +241,3 @@ }, | ||
const value = target[property]; | ||
const entry = schema[property]; | ||
const entry = schema[property] || {}; | ||
function trigger(method, args) { | ||
@@ -196,12 +247,8 @@ return Reflect.apply(method, target, args ? args : [ property, value, schema ]); | ||
if (entry.required) { | ||
throw new SmartModelError({ | ||
message: `Invalid delete on required propery ${property}`, | ||
property: property, | ||
code: "required" | ||
}); | ||
SmartModelError.throw(settings, "required", `Property "${property}" is "required"`, property, target); | ||
} | ||
trigger(target.onBeforeDelete); | ||
trigger(target.$onBeforeDelete); | ||
Reflect.deleteProperty(target, property); | ||
trigger(target.onDelete); | ||
trigger(target.onUpdate); | ||
trigger(target.$onDelete); | ||
trigger(target.$onUpdate); | ||
return true; | ||
@@ -215,3 +262,3 @@ } | ||
super(schema, settings); | ||
Object.keys(schema).forEach((key => { | ||
keys(schema, (key => { | ||
if (isUndef(data[key])) { | ||
@@ -225,24 +272,54 @@ if (!isUndef(schema[key].default)) { | ||
})); | ||
this.feed(data); | ||
this.$patch(data); | ||
} | ||
feed(data) { | ||
Object.keys(data).forEach((key => { | ||
$patch(data) { | ||
keys(data, (key => { | ||
this[key] = data[key]; | ||
})); | ||
} | ||
onBeforeGet() {} | ||
onBeforeSet() {} | ||
onBeforeUpdate() {} | ||
onDelete() {} | ||
onGet() {} | ||
onBeforeDelete() {} | ||
onSet() {} | ||
onUpdate() {} | ||
$put(data) { | ||
keys(this, (key => { | ||
if (data[key]) { | ||
if (isSmartModel(this[key])) { | ||
this[key].$put(data[key]); | ||
} else { | ||
this[key] = data[key]; | ||
} | ||
} else { | ||
this.$delete(key); | ||
} | ||
})); | ||
keys(data, (key => { | ||
if (!this[key]) { | ||
this[key] = data[key]; | ||
} | ||
})); | ||
} | ||
$delete(properties) { | ||
toArray(properties).forEach((key => { | ||
Reflect.deleteProperty(this, key); | ||
})); | ||
} | ||
$onBeforeGet() {} | ||
$onBeforeSet() {} | ||
$onBeforeUpdate() {} | ||
$onDelete() {} | ||
$onGet() {} | ||
$onBeforeDelete() {} | ||
$onSet() {} | ||
$onUpdate() {} | ||
} | ||
SmartModel.settings = { | ||
empty: value => value === "" || value === null || isUndef(value), | ||
strict: false, | ||
exceptions: true | ||
exceptions: { | ||
readonly: false, | ||
required: true, | ||
rule: true, | ||
strict: false, | ||
type: true | ||
} | ||
}; | ||
SmartModel.create = function(name, schema, settings, prototype) { | ||
settings = Object.assign({}, SmartModel.settings, settings); | ||
settings = merge(SmartModel.settings, settings); | ||
const Model = { | ||
@@ -257,3 +334,3 @@ [name]: class extends SmartModel { | ||
const invalidations = {}; | ||
Object.keys(schema).forEach((property => { | ||
keys(schema, (property => { | ||
let subErrors; | ||
@@ -266,3 +343,3 @@ const value = payload[property]; | ||
} | ||
let errors = checkErrors(entry, property, value); | ||
let errors = checkErrors(entry, property, value, false, settings); | ||
if (subErrors) { | ||
@@ -279,3 +356,3 @@ invalidations[property] = subErrors; | ||
})); | ||
return Object.keys(invalidations).length ? invalidations : false; | ||
return keys(invalidations).length ? invalidations : false; | ||
}; | ||
@@ -282,0 +359,0 @@ Model.hydrate = function(payload) { |
@@ -1,2 +0,2 @@ | ||
/*! SmartModel v0.3.2 */ | ||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).SmartModel=t()}(this,(function(){"use strict";class e extends Error{constructor(e){super(e.message),Object.assign(this,e)}}function t(e){return Array.isArray(e)}function r(e){return void 0===e}function n(e){return""===e||null===e||r(e)}function o(e){return"function"==typeof e}function s(e){return e.toString().startsWith("class")}function c(e){return[].concat([],e)}function i(e,r,o){const i=[];return e.required&&n(o)?(i.push({message:`Invalid value 'required' on property '${r}'`,code:"required"}),i):(void 0===o||(!e.type||!e.required&&n(o)||c(e.type).some((e=>function(e,r){const n=r&&r.toString().match(/^\s*function (\w+)/),o=(n?n[1]:"object").toLowerCase();if("date"===o&&e instanceof r)return!0;if("array"===o&&t(e))return!0;if("object"===o){if(s(r)&&e instanceof r)return!0;if(!s(r)&&typeof e===o)return!0}else if(typeof e===o)return!0;return!1}(o,e)))||i.push({message:`Invalid type '${typeof o}' on property '${r}'`,code:"type"}),e.rule&&Object.keys(e.rule).forEach((t=>{(0,e.rule[t])(o)&&i.push({message:`Invalid value '${t}' on property '${r}'`,code:t})}))),i)}function u(e={},t,r){if(!e.type)return!1;const n=e.type.prototype instanceof f&&e.type,o="[object Object]"===e.type.toString()&&e.type;return!(!n&&!o)&&(n||f.create(t.normalize("NFD").replace(/[\u0300-\u036f]/g,"").match(/[a-z]+/gi).map((e=>e.charAt(0).toUpperCase()+e.substr(1).toLowerCase())).join(""),o,r))}class f extends class{constructor(t,r){return new Proxy(this,{set(n,s,c){let f=t[s];const a=n[s],p=(l=a,!(JSON.stringify(c)===JSON.stringify(l)));var l;const d=u(f,s,r);function y(e,r){return Reflect.apply(e,n,r||[s,c,a,t])}if(!f){if(r.strict)return!0;f={}}if(y(n.onBeforeSet),p&&y(n.onBeforeUpdate),o(f.transform)&&(c=y(f.transform,[c,t])),r.exceptions){const t=i(f,s,c);if(t.length)throw new e({message:t[0].message,property:s,code:t[0].code,source:n.constructor.name})}return d&&(c=new d(c instanceof Object?c:{})),n[s]=c,y(n.onSet),p&&y(n.onUpdate),!0},get(e,r){const n=t[r];let s=e[r];if(!n)return e[r];function c(n,o){return Reflect.apply(n,e,o||[r,s,t])}return c(e.onBeforeGet),o(n)&&(s=c(n,[e,t])),o(n.format)&&(s=c(n.format,[s,t])),c(e.onGet),s},deleteProperty(r,n){const o=r[n];function s(e,s){return Reflect.apply(e,r,s||[n,o,t])}if(t[n].required)throw new e({message:`Invalid delete on required propery ${n}`,property:n,code:"required"});return s(r.onBeforeDelete),Reflect.deleteProperty(r,n),s(r.onDelete),s(r.onUpdate),!0}})}}{constructor(e={},t={},n){super(e,n),Object.keys(e).forEach((n=>{r(t[n])&&(this[n]=r(e[n].default)?t[n]:e[n].default)})),this.feed(t)}feed(e){Object.keys(e).forEach((t=>{this[t]=e[t]}))}onBeforeGet(){}onBeforeSet(){}onBeforeUpdate(){}onDelete(){}onGet(){}onBeforeDelete(){}onSet(){}onUpdate(){}}return f.settings={strict:!1,exceptions:!0},f.create=function(e,r,n,o){n=Object.assign({},f.settings,n);const s={[e]:class extends f{constructor(e){super(r,e,n)}}}[e];return s.checkErrors=function(e,t){const o={};return Object.keys(r).forEach((s=>{let f;const a=e[s],p=r[s],l=u(p,s,n);l&&(f=l.checkErrors(a,t));let d=i(p,s,a);f?o[s]=f:d.length&&(t&&(d=d.filter((e=>!c(t).includes(e.code)))),d.length&&(o[s]=d))})),!!Object.keys(o).length&&o},s.hydrate=function(e){return t(e)?e.map((e=>new s(e))):new s(e)},Object.assign(s.prototype,o),s.schema=r,s},f})); | ||
/*! SmartModel v0.3.3 */ | ||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).SmartModel=t()}(this,(function(){"use strict";function e(e){return Array.isArray(e)}function t(e){return void 0===e}function r(e){return"function"==typeof e}function n(e){return e&&e.toString().startsWith("class")}function o(e){return e.prototype instanceof l||e instanceof l}function s(e){return e&&"[object Object]"===e.toString()}function c(e,t=function(){}){return Object.keys(e).map(t)}function i(e){return[].concat([],e)}function u(e,t){return t=Object.assign({},e,t),c(e,(r=>{s(e[r])&&s(t[r])&&(t[r]=Object.assign({},e[r],u(e[r],t[r])))})),t}class f extends Error{constructor(e){super(e.message),Object.assign(this,e)}}function a(t,r,u,f,a){const p=[];return!a.strict||t&&c(t).length||p.push({message:`Property "${r}" can't be set in strict mode`,code:"strict"}),t.required&&a.empty(u)?(p.push({message:`Property "${r}" is "required"`,code:"required"}),p):t.readonly&&!f?(p.push({message:`Property '${r}' is 'readonly'`,code:"readonly"}),p):(void 0===u||(!t.type||!t.required&&a.empty(u)||o(t.type)&&s(u)||i(t.type).some((t=>function(t,r){const o=r&&r.toString().match(/^\s*function (\w+)/),s=(o?o[1]:"object").toLowerCase();if("date"===s&&t instanceof r)return!0;if("array"===s&&e(t))return!0;if("object"===s){if(n(r)&&t instanceof r)return!0;if(!n(r)&&typeof t===s)return!0}else if(typeof t===s)return!0;return!1}(u,t)))||p.push({message:`Property "${r}" has an invalid type "${typeof u}"`,code:"type"}),t.rule&&c(t.rule,(e=>{(0,t.rule[e])(u)&&p.push({message:`Property "${r}" breaks the "${e}" rule`,code:"rule:"+e})}))),p)}function p(e={},t,r){if(!e.type)return!1;const n=!!o(e.type)&&e.type,c=!!s(e.type)&&e.type;if(n||c){const o=n||l.create(t.normalize("NFD").replace(/[\u0300-\u036f]/g,"").match(/[a-z1-9]+/gi).map((e=>e.charAt(0).toUpperCase()+e.substr(1).toLowerCase())).join(""),c,r);return e.type=o,o}return!1}f.throw=function(e,t,r,n,o){const c=t.split(":")[0];if(!0===e.exceptions||s(e.exceptions)&&e.exceptions[c])throw new f({message:r,property:n,code:t,source:o&&o.constructor.name})};class l extends class{constructor(e,n){return new Proxy(this,{set(o,s,i){const u=e[s]||{},l=o[s],d=t(l),y=!(d||(h=i,$=l,JSON.stringify(h)===JSON.stringify($)));var h,$;const g=p(u,s,n);function m(r,n){const c=Reflect.apply(r,o,n||[s,i,l,e]);return t(c)?i:c}i=m(o.$onBeforeSet),y&&(i=m(o.$onBeforeUpdate)),r(u.transform)&&(i=m(u.transform,[i,e]));const b=a(u,s,i,d,n);if(b.length){if(!n.exceptions)return!0;f.throw(n,b[0].code,b[0].message,s,o)}return n.strict&&!c(u).length||(g&&(i=new g(i)),o[s]=i,m(o.$onSet),y&&m(o.$onUpdate)),!0},get(n,s){const i=e[s];let u=n[s];if("$eject"===s)return function(){return function(e){return c(e=Object.assign({},e),(t=>{o(e[t])&&(e[t]=e[t].$eject())})),e}(n)};if(!i)return n[s];function f(r,o){const c=Reflect.apply(r,n,o||[s,u,e]);return t(c)?u:c}return u=f(n.$onBeforeGet),r(i)&&(u=f(i,[n,e])),r(i.format)&&(u=f(i.format,[u,e])),u=f(n.$onGet),u},deleteProperty(t,r){const o=t[r];function s(n,s){return Reflect.apply(n,t,s||[r,o,e])}return(e[r]||{}).required&&f.throw(n,"required",`Property "${r}" is "required"`,r,t),s(t.$onBeforeDelete),Reflect.deleteProperty(t,r),s(t.$onDelete),s(t.$onUpdate),!0}})}}{constructor(e={},r={},n){super(e,n),c(e,(n=>{t(r[n])&&(this[n]=t(e[n].default)?r[n]:e[n].default)})),this.$patch(r)}$patch(e){c(e,(t=>{this[t]=e[t]}))}$put(e){c(this,(t=>{e[t]?o(this[t])?this[t].$put(e[t]):this[t]=e[t]:this.$delete(t)})),c(e,(t=>{this[t]||(this[t]=e[t])}))}$delete(e){i(e).forEach((e=>{Reflect.deleteProperty(this,e)}))}$onBeforeGet(){}$onBeforeSet(){}$onBeforeUpdate(){}$onDelete(){}$onGet(){}$onBeforeDelete(){}$onSet(){}$onUpdate(){}}return l.settings={empty:e=>""===e||null===e||t(e),strict:!1,exceptions:{readonly:!1,required:!0,rule:!0,strict:!1,type:!0}},l.create=function(t,r,n,o){n=u(l.settings,n);const s={[t]:class extends l{constructor(e){super(r,e,n)}}}[t];return s.checkErrors=function(e,t){const o={};return c(r,(s=>{let c;const u=e[s],f=r[s],l=p(f,s,n);l&&(c=l.checkErrors(u,t));let d=a(f,s,u,!1,n);c?o[s]=c:d.length&&(t&&(d=d.filter((e=>!i(t).includes(e.code)))),d.length&&(o[s]=d))})),!!c(o).length&&o},s.hydrate=function(t){return e(t)?t.map((e=>new s(e))):new s(t)},Object.assign(s.prototype,o),s.schema=r,s},l})); |
{ | ||
"name": "@jaysalvat/smart-model", | ||
"version": "0.3.2", | ||
"version": "0.3.3", | ||
"description": "Javascript object model", | ||
@@ -5,0 +5,0 @@ "main": "./build/smart-model.cjs.min.cjs", |
210
README.md
@@ -0,9 +1,20 @@ | ||
``` | ||
________ _____ ______ ________ ________ _________ _____ ______ ________ ________ _______ ___ | ||
|\ ____\|\ _ \ _ \|\ __ \|\ __ \|\___ ___\\ _ \ _ \|\ __ \|\ ___ \|\ ___ \ |\ \ | ||
\ \ \___|\ \ \\\__\ \ \ \ \|\ \ \ \|\ \|___ \ \_\ \ \\\__\ \ \ \ \|\ \ \ \_|\ \ \ __/|\ \ \ | ||
\ \_____ \ \ \\|__| \ \ \ __ \ \ _ _\ \ \ \ \ \ \\|__| \ \ \ \\\ \ \ \ \\ \ \ \_|/_\ \ \ | ||
\|____|\ \ \ \ \ \ \ \ \ \ \ \ \\ \| \ \ \ \ \ \ \ \ \ \ \\\ \ \ \_\\ \ \ \_|\ \ \ \____ | ||
____\_\ \ \__\ \ \__\ \__\ \__\ \__\\ _\ \ \__\ \ \__\ \ \__\ \_______\ \_______\ \_______\ \_______\ | ||
|\_________\|__| \|__|\|__|\|__|\|__|\|__| \|__| \|__| \|__|\|_______|\|_______|\|_______|\|_______| | ||
\|_________| | ||
``` | ||
[![npm version](https://badge.fury.io/js/%40jaysalvat%2Fsmart-model.svg)](https://badge.fury.io/js/%40jaysalvat%2Fsmart-model) | ||
SmartModel | ||
========== | ||
Model | ||
============= | ||
Javascript object model. | ||
- [x] ~1Kb gzipped | ||
- [x] 1Kb+ gzipped | ||
- [x] Value transformation | ||
@@ -14,2 +25,3 @@ - [x] Value format | ||
- [x] Default value | ||
- [x] Readonly properties | ||
- [x] Virtual property | ||
@@ -26,2 +38,4 @@ - [x] Throw exception (or not) if invalid | ||
[Provided formats](https://github.com/jaysalvat/smart-model/tree/master/build) are IFFE, ESM, CJS and UMD, all minified. | ||
### NPM | ||
@@ -114,7 +128,191 @@ | ||
const post = new Post({ | ||
title: 'my new post', | ||
body: 'lorem ipsum...' | ||
title: 'My new post', | ||
body: 'Lorem ipsum...', | ||
author: { | ||
firstname: 'Brad', | ||
lastname: 'Pitt' | ||
} | ||
}) | ||
``` | ||
## Documentation | ||
### Schema | ||
Options for property: | ||
| Option | Type | Description | ||
| ----------- | ------- | --- | ||
| type | any | The required type (*) of a value. You can set a schema or another model (*) in order to nest models | ||
| required | bool | The value is required. See `settings.empty` for the empty check function | ||
| readonly | bool | The value can't be overwritten | ||
| default | any | The default value if the property is undefined | ||
| transform | fn | A function to transform the value to set | ||
| format | fn | A function to format the value to get | ||
| rule | object | An object which contains the validation rules (**) | ||
#### [*] Type | ||
Type can be `String`, `Boolean`, `Number`, `Date`, `Function` or a class. | ||
If a schema is set as a type, a nested model will be created. | ||
```javascript | ||
const Post = SmartModel.create('Post', { | ||
title: { | ||
type: String | ||
}, | ||
body: { | ||
type: String | ||
}, | ||
Date: { | ||
type: Date | ||
}, | ||
author: { | ||
type { | ||
firstname: { | ||
type: String | ||
}, | ||
lastnaame: { | ||
type: String | ||
} | ||
} | ||
} | ||
}) | ||
``` | ||
An existing Model can be set as a type in order to nest this Model. | ||
```javascript | ||
const Author = SmartModel.create('Author', { | ||
firstname: { | ||
type: String | ||
}, | ||
lastnaame: { | ||
type: String | ||
} | ||
}) | ||
const Post = SmartModel.create('Post', { | ||
title: { | ||
type: String | ||
}, | ||
body: { | ||
type: String | ||
}, | ||
Date: { | ||
type: Date | ||
}, | ||
body: { | ||
type: string | ||
}, | ||
author: { | ||
type: Author | ||
} | ||
}) | ||
``` | ||
#### [**]s Rule | ||
Multiple rules of validation can be set on a property. | ||
```javascript | ||
const Discount = SmartModel.create('Discount', { | ||
percent: { | ||
type: Number, | ||
rule: { | ||
'min': {value) => value < 0, | ||
'max': {value) => value > 100 | ||
} | ||
} | ||
}) | ||
``` | ||
### Settings | ||
| Option | Type | Default | Description | ||
| ----------- | ----------- | ------------- | --- | ||
| strict | bool | false | Allow to set property not present in the schema | ||
| empty | fn | fn (***) | Function to check if a value is empty if required | ||
| exceptions | bool/object | object (****) | Throw exceptions on errors. can be `boolean` or òbject` for advanced settings | ||
#### [***] Empty check function | ||
The default function to check if a value is empty is: | ||
```javascript | ||
(value) => value === '' || value === null || value === undefined | ||
``` | ||
#### [****] Exceptions object | ||
| Option | Type | Default | ||
| --------- | ---- | ------- | ||
| readonly | bool | false | ||
| required | bool | true | ||
| rule | bool | true | ||
| strict | bool | false | ||
| type | bool | true | ||
### Methods | ||
#### $put | ||
```javascript | ||
const article = new Article() | ||
article.put({ | ||
title: 'My article', | ||
}) | ||
``` | ||
#### $patch | ||
Same as $put, but only passed property are updated. | ||
#### $eject | ||
```javascript | ||
const article = new Article() | ||
const json = article.eject() | ||
``` | ||
### Custom methods | ||
Methods can be added to models. | ||
```javascript | ||
const Article = SmartModel.create('Article', { | ||
body: { | ||
type: String | ||
} | ||
}, {}, { | ||
excerpt(limit = 10) { | ||
return this.body.substr(0, limit) + '…' | ||
} | ||
}) | ||
``` | ||
## Callbacks | ||
Models have some callbacks methods that are called when properties are set, get, updated or deleted. | ||
```javascript | ||
const User = SmartModel.create('User', { | ||
username: { | ||
type: String | ||
} | ||
}, {}, { | ||
$onBeforeGet() {} | ||
$onBeforeSet() {} | ||
$onBeforeUpdate() {} | ||
$onDelete() {} | ||
$onGet() {} | ||
$onBeforeDelete() {} | ||
$onSet() {} | ||
$onUpdate() {} | ||
}) | ||
``` | ||
## Dev | ||
@@ -121,0 +319,0 @@ |
@@ -1,9 +0,16 @@ | ||
import { isEmpty, toArray, isType } from './utils.js' | ||
import { keys, isSmartModel, toArray, checkType, isPlainObject } from './utils.js' | ||
export default function checkErrors(entry, property, value) { | ||
export default function checkErrors(entry, property, value, first, settings) { | ||
const errors = [] | ||
if (entry.required && isEmpty(value)) { | ||
if (settings.strict && (!entry || !keys(entry).length)) { | ||
errors.push({ | ||
message: `Invalid value 'required' on property '${property}'`, | ||
message: `Property "${property}" can't be set in strict mode`, | ||
code: 'strict' | ||
}) | ||
} | ||
if (entry.required && settings.empty(value)) { | ||
errors.push({ | ||
message: `Property "${property}" is "required"`, | ||
code: 'required' | ||
@@ -15,2 +22,11 @@ }) | ||
if (entry.readonly && !first) { | ||
errors.push({ | ||
message: `Property '${property}' is 'readonly'`, | ||
code: 'readonly' | ||
}) | ||
return errors | ||
} | ||
if (typeof value === 'undefined') { | ||
@@ -20,8 +36,10 @@ return errors | ||
if (entry.type && (entry.required || !isEmpty(value))) { | ||
if (!toArray(entry.type).some((type) => isType(value, type))) { | ||
errors.push({ | ||
message: `Invalid type '${typeof value}' on property '${property}'`, | ||
code: 'type' | ||
}) | ||
if (entry.type && (entry.required || !settings.empty(value))) { | ||
if (!(isSmartModel(entry.type) && isPlainObject(value))) { | ||
if (!toArray(entry.type).some((type) => checkType(value, type))) { | ||
errors.push({ | ||
message: `Property "${property}" has an invalid type "${typeof value}"`, | ||
code: 'type' | ||
}) | ||
} | ||
} | ||
@@ -31,3 +49,3 @@ } | ||
if (entry.rule) { | ||
Object.keys(entry.rule).forEach((key) => { | ||
keys(entry.rule, (key) => { | ||
const rule = entry.rule[key] | ||
@@ -37,4 +55,4 @@ | ||
errors.push({ | ||
message: `Invalid value '${key}' on property '${property}'`, | ||
code: key | ||
message: `Property "${property}" breaks the "${key}" rule`, | ||
code: 'rule:' + key | ||
}) | ||
@@ -41,0 +59,0 @@ } |
@@ -1,2 +0,2 @@ | ||
import { pascalCase, isPlainObject } from './utils.js' | ||
import { pascalCase, isSmartModel, isPlainObject } from './utils.js' | ||
import SmartModel from './SmartModel.js' | ||
@@ -9,7 +9,11 @@ | ||
const Child = entry.type.prototype instanceof SmartModel ? entry.type : false | ||
const Child = isSmartModel(entry.type) ? entry.type : false | ||
const schema = isPlainObject(entry.type) ? entry.type : false | ||
if (Child || schema) { | ||
return Child ? Child : SmartModel.create(pascalCase(property), schema, settings) | ||
const Model = Child ? Child : SmartModel.create(pascalCase(property), schema, settings) | ||
entry.type = Model | ||
return Model | ||
} | ||
@@ -16,0 +20,0 @@ |
@@ -5,3 +5,3 @@ | ||
import checkErrors from './checkErrors.js' | ||
import { toArray, isArray, isUndef } from './utils.js' | ||
import { toArray, isSmartModel, keys, merge, isArray, isUndef } from './utils.js' | ||
@@ -12,3 +12,3 @@ class SmartModel extends SmartModelProxy { | ||
Object.keys(schema).forEach((key) => { | ||
keys(schema, (key) => { | ||
if (isUndef(data[key])) { | ||
@@ -23,7 +23,7 @@ if (!isUndef(schema[key].default)) { | ||
this.feed(data) | ||
this.$patch(data) | ||
} | ||
feed(data) { | ||
Object.keys(data).forEach((key) => { | ||
$patch(data) { | ||
keys(data, (key) => { | ||
this[key] = data[key] | ||
@@ -33,19 +33,52 @@ }) | ||
onBeforeGet() {} | ||
onBeforeSet() {} | ||
onBeforeUpdate() {} | ||
onDelete() {} | ||
onGet() {} | ||
onBeforeDelete() {} | ||
onSet() {} | ||
onUpdate() {} | ||
$put(data) { | ||
keys(this, (key) => { | ||
if (data[key]) { | ||
if (isSmartModel(this[key])) { | ||
this[key].$put(data[key]) | ||
} else { | ||
this[key] = data[key] | ||
} | ||
} else { | ||
this.$delete(key) | ||
} | ||
}) | ||
keys(data, (key) => { | ||
if (!this[key]) { | ||
this[key] = data[key] | ||
} | ||
}) | ||
} | ||
$delete(properties) { | ||
toArray(properties).forEach((key) => { | ||
Reflect.deleteProperty(this, key) | ||
}) | ||
} | ||
$onBeforeGet() {} | ||
$onBeforeSet() {} | ||
$onBeforeUpdate() {} | ||
$onDelete() {} | ||
$onGet() {} | ||
$onBeforeDelete() {} | ||
$onSet() {} | ||
$onUpdate() {} | ||
} | ||
SmartModel.settings = { | ||
empty: (value) => value === '' || value === null || isUndef(value), | ||
strict: false, | ||
exceptions: true | ||
exceptions: { | ||
readonly: false, | ||
required: true, | ||
rule: true, | ||
strict: false, | ||
type: true | ||
} | ||
} | ||
SmartModel.create = function (name, schema, settings, prototype) { | ||
settings = Object.assign({}, SmartModel.settings, settings) | ||
settings = merge(SmartModel.settings, settings) | ||
@@ -61,3 +94,3 @@ const Model = { [name]: class extends SmartModel { | ||
Object.keys(schema).forEach((property) => { | ||
keys(schema, (property) => { | ||
let subErrors | ||
@@ -72,3 +105,3 @@ const value = payload[property] | ||
let errors = checkErrors(entry, property, value) | ||
let errors = checkErrors(entry, property, value, false, settings) | ||
@@ -88,3 +121,3 @@ if (subErrors) { | ||
return Object.keys(invalidations).length ? invalidations : false | ||
return keys(invalidations).length ? invalidations : false | ||
} | ||
@@ -91,0 +124,0 @@ |
@@ -0,1 +1,3 @@ | ||
import { isPlainObject } from './utils.js' | ||
class SmartModelError extends Error { | ||
@@ -8,2 +10,15 @@ constructor(data) { | ||
SmartModelError.throw = function (settings, code, message, property, source) { | ||
const shortCode = code.split(':')[0] | ||
if (settings.exceptions === true || (isPlainObject(settings.exceptions) && settings.exceptions[shortCode])) { | ||
throw new SmartModelError({ | ||
message, | ||
property, | ||
code, | ||
source: source && source.constructor.name | ||
}) | ||
} | ||
} | ||
export default SmartModelError |
import SmartModelError from './SmartModelError.js' | ||
import checkErrors from './checkErrors.js' | ||
import createNested from './createNested.js' | ||
import { isFn, isEqual } from './utils.js' | ||
import { keys, eject, isFn, isEqual, isUndef } from './utils.js' | ||
@@ -9,24 +9,19 @@ class SmartModelProxy { | ||
return new Proxy(this, { | ||
set(target, property, value) { | ||
let entry = schema[property] | ||
const entry = schema[property] || {} | ||
const old = target[property] | ||
const updated = !isEqual(value, old) | ||
const first = isUndef(old) | ||
const updated = !first && !isEqual(value, old) | ||
const Nested = createNested(entry, property, settings) | ||
function trigger(method, args) { | ||
return Reflect.apply(method, target, args ? args : [ property, value, old, schema ]) | ||
} | ||
const returned = Reflect.apply(method, target, args ? args : [ property, value, old, schema ]) | ||
if (!entry) { | ||
if (settings.strict) { | ||
return true | ||
} | ||
entry = {} | ||
return !isUndef(returned) ? returned : value | ||
} | ||
trigger(target.onBeforeSet) | ||
value = trigger(target.$onBeforeSet) | ||
if (updated) { | ||
trigger(target.onBeforeUpdate) | ||
value = trigger(target.$onBeforeUpdate) | ||
} | ||
@@ -38,17 +33,18 @@ | ||
if (settings.exceptions) { | ||
const errors = checkErrors(entry, property, value) | ||
const errors = checkErrors(entry, property, value, first, settings) | ||
if (errors.length) { | ||
throw new SmartModelError({ | ||
message: errors[0].message, | ||
property: property, | ||
code: errors[0].code, | ||
source: target.constructor.name | ||
}) | ||
if (errors.length) { | ||
if (settings.exceptions) { | ||
SmartModelError.throw(settings, errors[0].code, errors[0].message, property, target) | ||
} else { | ||
return true | ||
} | ||
} | ||
if (settings.strict && !keys(entry).length) { | ||
return true | ||
} | ||
if (Nested) { | ||
value = new Nested(value instanceof Object ? value : {}) | ||
value = new Nested(value) | ||
} | ||
@@ -58,6 +54,6 @@ | ||
trigger(target.onSet) | ||
trigger(target.$onSet) | ||
if (updated) { | ||
trigger(target.onUpdate) | ||
trigger(target.$onUpdate) | ||
} | ||
@@ -72,2 +68,8 @@ | ||
if (property === '$eject') { | ||
return function () { | ||
return eject(target) | ||
} | ||
} | ||
if (!entry) { | ||
@@ -78,6 +80,8 @@ return target[property] | ||
function trigger(method, args) { | ||
return Reflect.apply(method, target, args ? args : [ property, value, schema ]) | ||
const returned = Reflect.apply(method, target, args ? args : [ property, value, schema ]) | ||
return !isUndef(returned) ? returned : value | ||
} | ||
trigger(target.onBeforeGet) | ||
value = trigger(target.$onBeforeGet) | ||
@@ -92,3 +96,3 @@ if (isFn(entry)) { | ||
trigger(target.onGet) | ||
value = trigger(target.$onGet) | ||
@@ -100,3 +104,3 @@ return value | ||
const value = target[property] | ||
const entry = schema[property] | ||
const entry = schema[property] || {} | ||
@@ -108,15 +112,11 @@ function trigger(method, args) { | ||
if (entry.required) { | ||
throw new SmartModelError({ | ||
message: `Invalid delete on required propery ${property}`, | ||
property: property, | ||
code: 'required' | ||
}) | ||
SmartModelError.throw(settings, 'required', `Property "${property}" is "required"`, property, target) | ||
} | ||
trigger(target.onBeforeDelete) | ||
trigger(target.$onBeforeDelete) | ||
Reflect.deleteProperty(target, property) | ||
trigger(target.onDelete) | ||
trigger(target.onUpdate) | ||
trigger(target.$onDelete) | ||
trigger(target.$onUpdate) | ||
@@ -123,0 +123,0 @@ return true |
/* eslint-disable new-cap */ | ||
import SmartModel from './SmartModel.js' | ||
export function isArray(value) { | ||
@@ -11,6 +13,2 @@ return Array.isArray(value) | ||
export function isEmpty(value) { | ||
return value === '' || value === null || isUndef(value) | ||
} | ||
export function isFn(value) { | ||
@@ -25,10 +23,55 @@ return typeof value === 'function' | ||
export function isClass(value) { | ||
return value.toString().startsWith('class') | ||
return value && value.toString().startsWith('class') | ||
} | ||
export function isSmartModel(value) { | ||
return value.prototype instanceof SmartModel || value instanceof SmartModel | ||
} | ||
export function isPlainObject(value) { | ||
return value.toString() === '[object Object]' | ||
return value && value.toString() === '[object Object]' | ||
} | ||
export function isType(value, Type) { | ||
export function keys(obj, cb = function () { }) { | ||
return Object.keys(obj).map(cb) | ||
} | ||
export function toArray(value) { | ||
return [].concat([], value) | ||
} | ||
export function merge(source, target) { | ||
target = Object.assign({}, source, target) | ||
keys(source, (key) => { | ||
if (isPlainObject(source[key]) && isPlainObject(target[key])) { | ||
target[key] = Object.assign({}, source[key], merge(source[key], target[key])) | ||
} | ||
}) | ||
return target | ||
} | ||
export function eject(target) { | ||
target = Object.assign({}, target) | ||
keys(target, (key) => { | ||
if (isSmartModel(target[key])) { | ||
target[key] = target[key].$eject() | ||
} | ||
}) | ||
return target | ||
} | ||
export function pascalCase(string) { | ||
return string | ||
.normalize('NFD') | ||
.replace(/[\u0300-\u036f]/g, '') | ||
.match(/[a-z1-9]+/gi) | ||
.map((word) => word.charAt(0).toUpperCase() + word.substr(1).toLowerCase()) | ||
.join('') | ||
} | ||
export function checkType(value, Type) { | ||
const match = Type && Type.toString().match(/^\s*function (\w+)/) | ||
@@ -58,14 +101,1 @@ const type = (match ? match[1] : 'object').toLowerCase() | ||
} | ||
export function toArray(value) { | ||
return [].concat([], value) | ||
} | ||
export function pascalCase(string) { | ||
return string | ||
.normalize('NFD') | ||
.replace(/[\u0300-\u036f]/g, '') | ||
.match(/[a-z]+/gi) | ||
.map((word) => word.charAt(0).toUpperCase() + word.substr(1).toLowerCase()) | ||
.join('') | ||
} |
@@ -34,3 +34,3 @@ /* eslint-disable prefer-reflect */ | ||
const Model = SmartModel.create('Model', { | ||
prop1: { default: 'Default prop1' }, | ||
prop1: { default: 'Default value1' }, | ||
prop2: { } | ||
@@ -40,3 +40,3 @@ }) | ||
expect(model.prop1).to.be.equal('Default prop1') | ||
expect(model.prop1).to.be.equal('Default value1') | ||
expect(model.prop2).to.be.equal(undef) | ||
@@ -46,216 +46,224 @@ }) | ||
// Required | ||
// Validations | ||
describe('Required', function () { | ||
it('should throw an error if a required prop is not set on init', function () { | ||
const Model = SmartModel.create('Model', { | ||
prop: { required: true } | ||
}) | ||
describe('Validation', function () { | ||
const err = checkExceptions(() => { | ||
new Model() | ||
}) | ||
// Required | ||
expect(err.source).to.be.equal('Model') | ||
expect(err.property).to.be.equal('prop') | ||
expect(err.code).to.be.equal('required') | ||
}) | ||
describe('Required', function () { | ||
it('should throw an error if a required prop is not set on init', function () { | ||
const Model = SmartModel.create('Model', { | ||
prop: { required: true } | ||
}) | ||
it('should not throw an error if a required prop is set on init', function () { | ||
const Model = SmartModel.create('Model', { | ||
prop: { required: true } | ||
}) | ||
const err = checkExceptions(() => { | ||
new Model() | ||
}) | ||
const err = checkExceptions(() => { | ||
new Model({ prop: 'string' }) | ||
expect(err.source).to.be.equal('Model') | ||
expect(err.property).to.be.equal('prop') | ||
expect(err.code).to.be.equal('required') | ||
}) | ||
expect(err.property).to.be.equal(undef) | ||
expect(err.code).to.be.equal(undef) | ||
}) | ||
it('should not throw an error if a required prop is set on init', function () { | ||
const Model = SmartModel.create('Model', { | ||
prop: { required: true } | ||
}) | ||
it('should not throw an error if a required prop is not set but have default value on init', function () { | ||
const Model = SmartModel.create('Model', { | ||
prop: { required: true, default: 'string' } | ||
}) | ||
const err = checkExceptions(() => { | ||
new Model({ prop: 'string' }) | ||
}) | ||
const err = checkExceptions(() => { | ||
new Model() | ||
expect(err.property).to.be.equal(undef) | ||
expect(err.code).to.be.equal(undef) | ||
}) | ||
expect(err.property).to.be.equal(undef) | ||
expect(err.code).to.be.equal(undef) | ||
}) | ||
it('should not throw an error if a required prop is not set but have default value on init', function () { | ||
const Model = SmartModel.create('Model', { | ||
prop: { required: true, default: 'string' } | ||
}) | ||
it('should throw an error if a required prop is set to empty', function () { | ||
const Model = SmartModel.create('Model', { | ||
prop: { required: true, default: 'string' } | ||
}) | ||
const model = new Model() | ||
const err = checkExceptions(() => { | ||
new Model() | ||
}) | ||
const err = checkExceptions(() => { | ||
model.prop = null | ||
expect(err.property).to.be.equal(undef) | ||
expect(err.code).to.be.equal(undef) | ||
}) | ||
expect(err.property).to.be.equal('prop') | ||
expect(err.code).to.be.equal('required') | ||
}) | ||
it('should throw an error if a required prop is set to empty', function () { | ||
const Model = SmartModel.create('Model', { | ||
prop: { required: true, default: 'string' } | ||
}) | ||
const model = new Model() | ||
it('should throw an error if a required prop is deleted', function () { | ||
const Model = SmartModel.create('Model', { | ||
prop: { required: true, default: 'string' } | ||
}) | ||
const model = new Model() | ||
const err = checkExceptions(() => { | ||
model.prop = null | ||
}) | ||
const err = checkExceptions(() => { | ||
delete model.prop | ||
expect(err.property).to.be.equal('prop') | ||
expect(err.code).to.be.equal('required') | ||
}) | ||
expect(err.property).to.be.equal('prop') | ||
expect(err.code).to.be.equal('required') | ||
}) | ||
}) | ||
it('should throw an error if a required prop is deleted', function () { | ||
const Model = SmartModel.create('Model', { | ||
prop: { required: true, default: 'string' } | ||
}) | ||
const model = new Model() | ||
// Type | ||
const err = checkExceptions(() => { | ||
delete model.prop | ||
}) | ||
describe('Type', function () { | ||
it('should throw an error if a typed prop is not set properly', function () { | ||
const Model = SmartModel.create('Model', { | ||
prop: { required: true, type: String } | ||
expect(err.property).to.be.equal('prop') | ||
expect(err.code).to.be.equal('required') | ||
}) | ||
}) | ||
const err = checkExceptions(() => { | ||
new Model({ prop: 0 }) | ||
}) | ||
// Type | ||
expect(err.property).to.be.equal('prop') | ||
expect(err.code).to.be.equal('type') | ||
}) | ||
describe('Type', function () { | ||
it('should throw an error if a typed prop is not set properly', function () { | ||
const Model = SmartModel.create('Model', { | ||
prop: { required: true, type: String } | ||
}) | ||
it('should not throw an error if a typed prop is set properly', function () { | ||
const Model = SmartModel.create('Model', { | ||
prop: { required: true, type: String } | ||
}) | ||
const err = checkExceptions(() => { | ||
new Model({ prop: 0 }) | ||
}) | ||
const err = checkExceptions(() => { | ||
new Model({ prop: 'string' }) | ||
expect(err.property).to.be.equal('prop') | ||
expect(err.code).to.be.equal('type') | ||
}) | ||
expect(err.property).to.be.equal(undef) | ||
expect(err.code).to.be.equal(undef) | ||
}) | ||
it('should not throw an error if a typed prop is set properly', function () { | ||
const Model = SmartModel.create('Model', { | ||
prop: { required: true, type: String } | ||
}) | ||
it('should not throw an error if multiple typed props is set properly', function () { | ||
const Model = SmartModel.create('Model', { | ||
prop: { type: [ String, Array ] } | ||
}) | ||
const model = new Model() | ||
const err = checkExceptions(() => { | ||
new Model({ prop: 'string' }) | ||
}) | ||
const err = checkExceptions(() => { | ||
model.prop = 'string' | ||
model.prop = [ 'string' ] | ||
expect(err.property).to.be.equal(undef) | ||
expect(err.code).to.be.equal(undef) | ||
}) | ||
expect(err.property).to.be.equal(undef) | ||
expect(err.code).to.be.equal(undef) | ||
}) | ||
it('should not throw an error if multiple typed props is set properly', function () { | ||
const Model = SmartModel.create('Model', { | ||
prop: { type: [ String, Array ] } | ||
}) | ||
const model = new Model() | ||
it('should not throw an error if a typed prop is set on a not required property', function () { | ||
const Model = SmartModel.create('Model', { | ||
prop: { type: String } | ||
const err = checkExceptions(() => { | ||
model.prop = 'string' | ||
model.prop = [ 'string' ] | ||
}) | ||
expect(err.property).to.be.equal(undef) | ||
expect(err.code).to.be.equal(undef) | ||
}) | ||
const err = checkExceptions(() => { | ||
new Model() | ||
it('should not throw an error if a typed prop is set on a not required property', function () { | ||
const Model = SmartModel.create('Model', { | ||
prop: { type: String } | ||
}) | ||
const err = checkExceptions(() => { | ||
new Model() | ||
}) | ||
expect(err.property).to.be.equal(undef) | ||
expect(err.code).to.be.equal(undef) | ||
}) | ||
expect(err.property).to.be.equal(undef) | ||
expect(err.code).to.be.equal(undef) | ||
}) | ||
it('should be alright with all those type checks', function () { | ||
const TestModel = SmartModel.create('Test', {}) | ||
const testModel = new TestModel() | ||
it('should be alright with all those type checks', function () { | ||
const TestModel = SmartModel.create('Test', {}) | ||
const testModel = new TestModel() | ||
const types = [ | ||
{ type: Date, fail: 100, success: new Date() }, | ||
{ type: String, fail: 0, success: 'string' }, | ||
{ type: Object, fail: 'string', success: {} }, | ||
{ type: Number, fail: 'string', success: 100 }, | ||
{ type: Boolean, fail: 'string', success: true }, | ||
{ type: Function, fail: 'string', success: function () { } }, | ||
{ type: TestModel, fail: 'string', success: testModel }, | ||
{ type: SmartModel, fail: {}, success: testModel }, | ||
{ type: Array, fail: {}, success: [] } | ||
] | ||
const types = [ | ||
{ type: Date, fail: 100, success: new Date() }, | ||
{ type: Array, fail: {}, success: [] }, | ||
{ type: String, fail: 0, success: 'string' }, | ||
{ type: Object, fail: 'string', success: {} }, | ||
{ type: Number, fail: 'string', success: 100 }, | ||
{ type: Boolean, fail: 'string', success: true }, | ||
{ type: Function, fail: 'string', success: function () { } }, | ||
{ type: SmartModel, fail: {}, success: testModel }, | ||
{ type: TestModel, fail: 'string', success: testModel } | ||
] | ||
types.forEach((type) => { | ||
expect(() => { | ||
const Model = SmartModel.create('Model', { | ||
prop: { type: type.type, default: type.fail } | ||
}) | ||
types.forEach((type) => { | ||
expect(() => { | ||
const Model = SmartModel.create('Model', { | ||
prop: { type: type.type, default: type.fail } | ||
}) | ||
new Model() | ||
}).to.throw('type') | ||
new Model() | ||
}).to.throw('type') | ||
expect(() => { | ||
const Model = SmartModel.create('Model', { | ||
prop: { type: type.type, default: type.success } | ||
}) | ||
expect(() => { | ||
const Model = SmartModel.create('Model', { | ||
prop: { type: type.type, default: type.success } | ||
}) | ||
new Model() | ||
}).to.not.throw('type') | ||
new Model() | ||
}).to.not.throw('type') | ||
}) | ||
}) | ||
}) | ||
}) | ||
// // Rule | ||
// // Rule | ||
describe('Rule', function () { | ||
it('should throw an error if a ruled prop is not set properly', function () { | ||
const Model = SmartModel.create('Model', { | ||
prop: { required: true, rule: { | ||
min: (value) => value < 5 | ||
} } | ||
}) | ||
describe('Rule', function () { | ||
it('should throw an error if a ruled prop is not set properly', function () { | ||
const Model = SmartModel.create('Model', { | ||
prop: { | ||
required: true, rule: { | ||
min: (value) => value < 5 | ||
} | ||
} | ||
}) | ||
const err = checkExceptions(() => { | ||
new Model({ prop: 0 }) | ||
const err = checkExceptions(() => { | ||
new Model({ prop: 0 }) | ||
}) | ||
expect(err.property).to.be.equal('prop') | ||
expect(err.code).to.be.equal('rule:min') | ||
}) | ||
expect(err.property).to.be.equal('prop') | ||
expect(err.code).to.be.equal('min') | ||
}) | ||
it('should not throw an error if a ruled prop is set properly', function () { | ||
const Model = SmartModel.create('Model', { | ||
prop: { | ||
required: true, rule: { | ||
min: (value) => value < 5 | ||
} | ||
} | ||
}) | ||
it('should not throw an error if a ruled prop is set properly', function () { | ||
const Model = SmartModel.create('Model', { | ||
prop: { required: true, rule: { | ||
min: (value) => value < 5 | ||
} } | ||
}) | ||
const err = checkExceptions(() => { | ||
new Model({ prop: 10 }) | ||
}) | ||
const err = checkExceptions(() => { | ||
new Model({ prop: 10 }) | ||
expect(err.property).to.be.equal(undef) | ||
expect(err.code).to.be.equal(undef) | ||
}) | ||
expect(err.property).to.be.equal(undef) | ||
expect(err.code).to.be.equal(undef) | ||
}) | ||
it('should not throw an error if a ruled prop is set on an empty not required property', function () { | ||
const Model = SmartModel.create('Model', { | ||
prop: { | ||
rule: { | ||
min: (value) => value < 5 | ||
} | ||
} | ||
}) | ||
it('should not throw an error if a ruled prop is set on an empty not required property', function () { | ||
let err = {} | ||
const Model = SmartModel.create('Model', { | ||
prop: { rule: { | ||
min: (value) => value < 5 | ||
} } | ||
const err = checkExceptions(() => { | ||
new Model() | ||
}) | ||
expect(err.property).to.be.equal(undef) | ||
expect(err.code).to.be.equal(undef) | ||
}) | ||
try { | ||
new Model() | ||
} catch (e) { | ||
err = e | ||
} | ||
expect(err.property).to.be.equal(undef) | ||
expect(err.code).to.be.equal(undef) | ||
}) | ||
@@ -266,3 +274,3 @@ }) | ||
describe('Format / Transform', function () { | ||
describe('Format and transform', function () { | ||
it('should transform a value', function () { | ||
@@ -302,3 +310,3 @@ const Model = SmartModel.create('Model', { | ||
// Virtual property | ||
// Virtual / Readonly property | ||
@@ -327,2 +335,19 @@ describe('Virtual property', function () { | ||
}) | ||
it('should throw an exception if property is readonly', function () { | ||
SmartModel.settings.exceptions.readonly = true | ||
const Model = SmartModel.create('Model', { | ||
prop: { default: 'string', readonly: true } | ||
}) | ||
const model = new Model() | ||
const err = checkExceptions(() => { | ||
model.prop = 'another string' | ||
}) | ||
expect(err.code).to.be.equal('readonly') | ||
expect(err.property).to.be.equal('prop') | ||
}) | ||
}) | ||
@@ -333,10 +358,72 @@ | ||
describe('Events', function () { | ||
it('should trigger onBeforeDelete', testTrigger('onBeforeDelete')) | ||
it('should trigger onBeforeGet', testTrigger('onBeforeGet')) | ||
it('should trigger onBeforeSet', testTrigger('onBeforeSet')) | ||
it('should trigger onBeforeUpdate', testTrigger('onBeforeUpdate')) | ||
it('should trigger onDelete', testTrigger('onDelete')) | ||
it('should trigger onGet', testTrigger('onGet')) | ||
it('should trigger onSet', testTrigger('onSet')) | ||
it('should trigger onUpdate', testTrigger('onUpdate')) | ||
it('should trigger $onBeforeDelete', testTrigger('$onBeforeDelete')) | ||
it('should trigger $onBeforeGet', testTrigger('$onBeforeGet')) | ||
it('should trigger $onBeforeSet', testTrigger('$onBeforeSet')) | ||
it('should trigger $onBeforeUpdate', testTrigger('$onBeforeUpdate')) | ||
it('should trigger $onDelete', testTrigger('$onDelete')) | ||
it('should trigger $onGet', testTrigger('$onGet')) | ||
it('should trigger $onSet', testTrigger('$onSet')) | ||
it('should trigger $onUpdate', testTrigger('$onUpdate')) | ||
it('should intercept set', () => { | ||
let isIntercepted | ||
const Model = SmartModel.create('Model', { | ||
prop: { default: 'default string' } | ||
}, {}, { | ||
$onBeforeSet() { | ||
return 'INTERCEPTED' | ||
}, | ||
$onSet(_, val) { | ||
isIntercepted = val === 'INTERCEPTED' | ||
} | ||
}) | ||
const model = new Model() | ||
expect(model.prop).to.be.equal('INTERCEPTED') | ||
expect(isIntercepted).to.be.equal(true) | ||
}) | ||
it('should intercept update', () => { | ||
let isIntercepted | ||
const Model = SmartModel.create('Model', { | ||
prop: { default: 'default string' } | ||
}, {}, { | ||
$onBeforeUpdate() { | ||
return 'INTERCEPTED' | ||
}, | ||
$onUpdate(_, val) { | ||
isIntercepted = val === 'INTERCEPTED' | ||
} | ||
}) | ||
const model = new Model() | ||
model.prop = 'updated' | ||
expect(model.prop).to.be.equal('INTERCEPTED') | ||
expect(isIntercepted).to.be.equal(true) | ||
}) | ||
it('should intercept get', () => { | ||
let isIntercepted | ||
const Model = SmartModel.create('Model', { | ||
prop: { default: 'default string' } | ||
}, {}, { | ||
$onBeforeGet() { | ||
return 'INTERCEPTED' | ||
}, | ||
$onGet(_, val) { | ||
isIntercepted = val === 'INTERCEPTED' | ||
} | ||
}) | ||
const model = new Model() | ||
expect(model.prop).to.be.equal('INTERCEPTED') | ||
expect(isIntercepted).to.be.equal(true) | ||
}) | ||
}) | ||
@@ -346,2 +433,124 @@ | ||
describe('Methods', function () { | ||
describe('$put', function () { | ||
it('should replace model data', function () { | ||
const Model = SmartModel.create('Model', { | ||
prop1: { default: 'string1' }, | ||
prop2: { default: 'string2' }, | ||
prop3: { | ||
type: { | ||
nestedProp1: { default: 'string3' }, | ||
nestedProp2: { default: 'string4' } | ||
} | ||
} | ||
}) | ||
const model = new Model() | ||
model.$put({ | ||
prop1: 'newString1', | ||
prop3: { | ||
nestedProp1: 'newString2', | ||
new: 'string' | ||
}, | ||
new: 'string' | ||
}) | ||
expect(model.prop1).to.be.equal('newString1') | ||
expect(model.prop2).to.be.equal(undef) | ||
expect(model.new).to.be.equal('string') | ||
expect(model.prop3.nestedProp1).to.be.equal('newString2') | ||
expect(model.prop3.nestedProp2).to.be.equal(undef) | ||
expect(model.prop3.new).to.be.equal('string') | ||
}) | ||
}) | ||
describe('$patch', function () { | ||
it('should replace model data', function () { | ||
const Model = SmartModel.create('Model', { | ||
prop1: { default: 'string1' }, | ||
prop2: { default: 'string2' }, | ||
prop3: { | ||
type: { | ||
nestedProp1: { default: 'string3' }, | ||
nestedProp2: { default: 'string4' } | ||
} | ||
} | ||
}) | ||
const model = new Model() | ||
model.$patch({ | ||
prop1: 'newString1', | ||
prop3: { | ||
nestedProp1: 'newString2', | ||
new: 'string' | ||
}, | ||
new: 'string' | ||
}) | ||
expect(model.prop1).to.be.equal('newString1') | ||
expect(model.prop2).to.be.equal('string2') | ||
expect(model.new).to.be.equal('string') | ||
expect(model.prop3.nestedProp1).to.be.equal('newString2') | ||
expect(model.prop3.nestedProp2).to.be.equal('string4') | ||
expect(model.prop3.new).to.be.equal('string') | ||
}) | ||
}) | ||
describe('$delete', function () { | ||
it('should delete one property', function () { | ||
const Model = SmartModel.create('Model', { | ||
prop1: { default: 'string1' }, | ||
prop2: { default: 'string2' } | ||
}) | ||
const model = new Model() | ||
model.$delete('prop2') | ||
expect(model.prop1).to.be.equal('string1') | ||
expect(model.prop2).to.be.equal(undef) | ||
}) | ||
it('should delete an array of properties', function () { | ||
const Model = SmartModel.create('Model', { | ||
prop1: { default: 'string1' }, | ||
prop2: { default: 'string2' }, | ||
prop3: { default: 'string3' } | ||
}) | ||
const model = new Model() | ||
model.$delete([ 'prop2', 'prop3' ]) | ||
expect(model).have.own.property('prop1') | ||
expect(model).not.have.own.property('prop2') | ||
expect(model).not.have.own.property('prop3') | ||
}) | ||
}) | ||
describe('$eject', function () { | ||
it('should works', function () { | ||
const Model = SmartModel.create('Model', { | ||
prop1: { default: 'string1' }, | ||
prop2: { | ||
type: { | ||
nestedProp: { default: 'string2' } | ||
} | ||
} | ||
}) | ||
const model = new Model() | ||
const obj = model.$eject() | ||
expect(obj).to.be.deep.equal(model) | ||
expect(model).to.be.instanceOf(SmartModel) | ||
expect(model.prop2).to.be.instanceOf(SmartModel) | ||
expect(obj).to.not.be.instanceOf(SmartModel) | ||
expect(obj.prop2).to.not.be.instanceOf(SmartModel) | ||
}) | ||
}) | ||
}) | ||
describe('Statics methods', function () { | ||
@@ -373,4 +582,4 @@ describe('CheckErrors', function () { | ||
expect(errrors.prop2[0].code).to.be.equal('type') | ||
expect(errrors.prop4[0].code).to.be.equal('min') | ||
expect(errrors.prop4[1].code).to.be.equal('pos') | ||
expect(errrors.prop4[0].code).to.be.equal('rule:min') | ||
expect(errrors.prop4[1].code).to.be.equal('rule:pos') | ||
}) | ||
@@ -413,2 +622,14 @@ | ||
}) | ||
it('should return an array of model errors with readonly error', function () { | ||
const Model = SmartModel.create('Model', { | ||
prop: { type: String, readonly: true } | ||
}) | ||
const errrors = Model.checkErrors({ | ||
prop: 'string' | ||
}) | ||
expect(errrors.prop[0].code).to.be.equal('readonly') | ||
}) | ||
}) | ||
@@ -459,2 +680,3 @@ | ||
SmartModel.settings.strict = true | ||
SmartModel.settings.exceptions.strict = false | ||
@@ -535,3 +757,3 @@ const Model1 = SmartModel.create('Model1', { | ||
const Model = SmartModel.create('Model', { | ||
prop1: { required: true } | ||
prop: { required: true } | ||
}, { | ||
@@ -548,3 +770,3 @@ exceptions: false | ||
const Model = SmartModel.create('Model', { | ||
prop1: { required: true } | ||
prop: { required: true } | ||
}, { | ||
@@ -558,2 +780,58 @@ exceptions: true | ||
}) | ||
it('should not throw errors if exceptions.required:false', function () { | ||
const Model = SmartModel.create('Model', { | ||
prop: { required: true } | ||
}, { | ||
exceptions: { | ||
required: false | ||
} | ||
}) | ||
expect(() => { | ||
new Model() | ||
}).to.not.throw(Error) | ||
}) | ||
it('should not throw errors if exceptions.type:false', function () { | ||
const Model = SmartModel.create('Model', { | ||
prop: { type: String } | ||
}, { | ||
exceptions: { | ||
type: false | ||
} | ||
}) | ||
expect(() => { | ||
new Model({ prop: 0 }) | ||
}).to.not.throw(Error) | ||
}) | ||
it('should not throw errors if exceptions.strict:false', function () { | ||
const Model = SmartModel.create('Model', { | ||
prop1: { type: String } | ||
}, { | ||
exceptions: { | ||
strict: false | ||
} | ||
}) | ||
expect(() => { | ||
new Model({ prop2: 'ok' }) | ||
}).to.not.throw(Error) | ||
}) | ||
it('should not throw errors if exceptions.rule:false', function () { | ||
const Model = SmartModel.create('Model', { | ||
prop: { rule: { min: (value) => value < 2 } } | ||
}, { | ||
exceptions: { | ||
rule: false | ||
} | ||
}) | ||
expect(() => { | ||
new Model({ prop: 1 }) | ||
}).to.not.throw(Error) | ||
}) | ||
}) | ||
@@ -629,2 +907,20 @@ }) | ||
it('should nest a child model and init it properly', function () { | ||
const Model = SmartModel.create('Model', { | ||
prop: { | ||
type: { | ||
nestedProp: { | ||
default: 'string' | ||
} | ||
} | ||
} | ||
}) | ||
const model = new Model({ | ||
prop: {} | ||
}) | ||
expect(model.prop.nestedProp).to.be.equal('string') | ||
}) | ||
it('should throw an error if a required parent prop is not set on init', function () { | ||
@@ -744,3 +1040,3 @@ const Model = SmartModel.create('Model', { | ||
if ([ 'onBeforeDelete', 'onDelete' ].includes(name)) { | ||
if ([ '$onBeforeDelete', '$onDelete' ].includes(name)) { | ||
delete model.prop | ||
@@ -750,3 +1046,3 @@ | ||
} else if ([ 'onBeforeGet', 'onGet' ].includes(name)) { | ||
} else if ([ '$onBeforeGet', '$onGet' ].includes(name)) { | ||
model.prop = model.prop | ||
@@ -753,0 +1049,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
114153
2842
372
26