ngn-data
Version:
Data modeling, stores, proxies, and utilities for NGN
1,244 lines (1,017 loc) • 32.4 kB
JavaScript
/**
* v0.1.11 generated on: Sat Jun 24 2017 01:20:07 GMT+0000 (UTC)
* Copyright (c) 2014-2017, Ecor Ventures LLC. All Rights Reserved. See LICENSE (BSD3).
*/
'use strict'
class NgnDataModel extends NGN.EventEmitter {
constructor (config) {
config = config || {}
super()
Object.defineProperties(this, {
idAttribute: NGN.privateconst(config.idAttribute || 'id'),
fields: NGN.private(config.fields ||
{
id: {
required: true,
type: String,
'default': config.id || null
}
}
),
hiddenFields: NGN.private(NGN.coalesce(config.metaFields)),
joins: NGN.private(config.relationships || {}),
virtuals: NGN.private(config.virtuals || {}),
validators: NGN.private({}),
validation: NGN.public(NGN.coalesce(config.validation, true)),
isNew: NGN.private(true),
isRecordDestroyed: NGN.private(false),
oid: NGN.private(config[this.idAttribute] || null),
autoid: NGN.public(NGN.coalesce(config.autoid, false)),
autoidPrefix: NGN.public(NGN.coalesce(config.autoidPrefix)),
autoidPostfix: NGN.public(NGN.coalesce(config.autoidPostfix)),
benchmark: NGN.private(null),
expiration: NGN.private(null),
expirationTimeout: NGN.private(null),
hasExpired: NGN.private(false),
ignoreTTL: NGN.private(false),
createDate: NGN.privateconst(Date.now()),
setUnmodified: NGN.privateconst(function () {
this.benchmark = this.checksum
this.changelog = []
}),
allowInvalidSave: NGN.private(NGN.coalesce(config.allowInvalidSave, false)),
disableDataValidation: NGN.private(NGN.coalesce(config.disableDataValidation, false)),
invalidDataAttributes: NGN.private([]),
initialDataAttributes: NGN.private([]),
changelog: NGN.private([]),
_nativeValidators: NGN.privateconst({
min: function (minimum, value) {
if (NGN.typeof(value) === 'array') {
return value.length >= minimum
}
if (NGN.typeof(value) === 'number') {
return value >= minimum
}
if (NGN.typeof(value) === 'string') {
return value.trim().length >= minimum
}
if (NGN.typeof(value) === 'date') {
return value.parse() >= minimum.parse()
}
return false
},
max: function (maximum, value) {
if (NGN.typeof(value) === 'array') {
return value.length <= maximum
}
if (NGN.typeof(value) === 'number') {
return value <= maximum
}
if (NGN.typeof(value) === 'string') {
return value.trim().length <= maximum
}
if (NGN.typeof(value) === 'date') {
return value.parse() <= maximum.parse()
}
return false
},
enum: function (valid, value) {
return valid.indexOf(value) >= 0
},
required: (field, value) => {
return this.hasOwnProperty(field) && this[value] !== null
}
}),
_dataMap: NGN.private(config.dataMap || null),
_reverseDataMap: NGN.public(null),
raw: NGN.private({}),
rawjoins: NGN.private({}),
_store: NGN.private(null),
_proxy: NGN.private(config.proxy || null),
proxyignore: NGN.private(NGN.coalesce(config.proxyignore, false))
})
if (NGN.coalesce(this.idAttribute, '') !== 'id' && !this.fields.hasOwnProperty(this.idAttribute)) {
console.warn(this.idAttribute + ' is specified as the ID, but it is not defined in the fields.')
}
if (config.proxy) {
if (this._proxy instanceof NGN.DATA.Proxy) {
this._proxy.init(this)
} else {
throw new Error('Invalid proxy configuration.')
}
}
let allfields = this.datafields.concat(this.virtualdatafields).concat(this.relationships).filter(function (key, i, a) {
return a.indexOf(key) !== i
})
if (allfields.length > 0) {
throw new Error('Duplicate field names exist: ' + allfields.join(', ') + '. Unique fieldnames are required for data fields, virtuals, and relationship fields.')
}
if (!this.fields.hasOwnProperty('id')) {
config.fields.id = {
required: true,
type: String,
'default': config.id || null
}
}
Object.keys(this.fields).forEach((field) => {
if (typeof this.fields[field] !== 'object' && this.fields[field] !== null) {
this.fields[field] = {
required: true,
type: this.fields[field],
default: null,
name: field
}
}
this.addField(field, true)
})
if (this.hiddenFields) {
Object.keys(this.hiddenFields).forEach((field) => {
this.addMetaField(field, NGN.coalesce(this.hiddenFields[field], null), true)
})
}
Object.keys(this.virtuals).forEach((v) => {
Object.defineProperty(this, v, NGN.get(() => {
return this.virtuals[v].apply(this)
}))
})
Object.keys(this.joins).forEach((field) => {
this.addRelationshipField(field, this.joins[field], true)
})
let events = [
'field.update',
'field.create',
'field.remove',
'field.invalid',
'validator.add',
'validator.remove',
'relationship.create',
'relationship.remove',
'expired',
'deleted',
'reset',
'load'
]
if (NGN.BUS) {
events.forEach((eventName) => {
this.on(eventName, function () {
let args = NGN.slice(arguments)
args.push(this)
args.unshift(eventName)
NGN.BUS.emit.apply(NGN.BUS, args)
})
})
}
if (config.hasOwnProperty('expires')) {
this.expires = config.expires
}
this.on('changelog.append', (delta) => {
delta.id = NGN.DATA.util.GUID()
this.changelog.push(delta)
this.emit('append.changelog', delta)
})
this.on('changelog.remove', (idList) => {
idList = Array.isArray(idList) ? idList : [idList]
this.emit('remove.changelog', idList)
})
}
get proxy () {
return this._proxy
}
set proxy (value) {
if (!this._proxy && value instanceof NGN.DATA.Proxy) {
this._proxy = value
this._proxy.init(this)
}
}
get deleted () {
return this.isRecordDestroyed
}
set isDestroyed (value) {
if (typeof value !== 'boolean') {
console.warn(NGN.stack)
throw new Error('Invalid data type. isDestroyed must be a boolean. Received ' + (typeof value))
}
this.isRecordDestroyed = value
if (value) {
this.emit('deleted')
}
}
get expires () {
return this.expiration
}
set expires (value) {
if (NGN.typeof(value) !== 'date' && NGN.typeof(value) !== 'number') {
try {
const source = NGN.stack.pop()
console.warn('Expiration could not be set at %c' + source.path + '%c (Invalid data type. Must be a Date or number).', NGN.css, '')
} catch (e) {
console.warn('Expiration could not be set (Invalid data type. Must be a Date or number).')
}
return
}
clearTimeout(this.expirationTimeout)
if (NGN.typeof(value) === 'number') {
if (value < 0) {
this.ignoreTTL = true
return
}
const currentDate = new Date()
value = new Date (
currentDate.getFullYear(),
currentDate.getMonth(),
currentDate.getDate(),
currentDate.getHours(),
currentDate.getMinutes(),
currentDate.getSeconds(),
currentDate.getMilliseconds() + value
)
}
this.ignoreTTL = false
this.expiration = value
if (Date.now() >= this.expiration.getTime()) {
this.expire()
return
}
this.hasExpired = false
let waitPeriod = this.expiration.getTime() - Date.now()
this.expirationTimeout = setTimeout(() => {
this.expire()
}, waitPeriod)
}
get expired () {
if (this.ignoreTTL) {
return false
}
return this.hasExpired
}
get modified () {
return this.checksum !== this.benchmark
}
get id () {
return this.oid
}
set id (value) {
this.oid = value
}
get checksum () {
return NGN.DATA.util.checksum(JSON.stringify(this.data))
}
get dataMap () {
return this._dataMap
}
set dataMap (value) {
this._dataMap = value
this._reverseDataMap = null
}
get datastore () {
return this._store
}
get valid () {
this.validate()
return this.invalidDataAttributes.length === 0
}
get datafields () {
if (!this.hiddenFields) {
return Object.keys(this.fields)
}
let hidden = NGN.coalesce(this.hiddenFields, {})
return Object.keys(this.fields).filter((fieldname) => {
return !hidden.hasOwnProperty(fieldname)
})
}
get relationships () {
return Object.keys(this.joins)
}
get virtualdatafields () {
return Object.keys(this.virtuals)
}
get reverseMap () {
if (this.dataMap !== null) {
if (this._reverseDataMap !== null) {
return this._reverseDataMap
}
let rmap = {}
const me = this
Object.keys(this._dataMap).forEach(function (attr) {
rmap[me._dataMap[attr]] = attr
})
this._reverseDataMap = rmap
return rmap
}
return null
}
get data () {
let d = this.serialize()
if (!d.hasOwnProperty(this.idAttribute) && this.autoid) {
d[this.idAttribute] = this[this.idAttribute]
}
if (this.dataMap) {
const me = this
Object.keys(this.dataMap).forEach(function (key) {
if (d.hasOwnProperty(key)) {
if (d[key] instanceof NGN.DATA.Model) {
d[me.dataMap[key]] = d[key].data
} else {
d[me.dataMap[key]] = d[key]
}
delete d[key]
}
})
}
return d
}
get representation () {
let data = this.data
Object.keys(this.rawjoins).forEach((join) => {
data[join] = this.rawjoins[join].representation
})
Object.keys(this.virtuals).forEach((attribute) => {
let val = this[attribute]
if (val instanceof NGN.DATA.Entity || val instanceof NGN.DATA.Store) {
data[attribute] = this[attribute].representation
} else if (typeof val !== 'function') {
data[attribute] = this[attribute]
}
})
return data
}
get history () {
return this.changelog.reverse()
}
expire (duration) {
if (this.expired) {
return
}
if (duration) {
this.expires = duration
return
}
if (this.ignoreTTL) {
return
}
this.hasExpired = true
clearTimeout(this.expirationTimeout)
this.emit('expired', this)
}
disableExpiration () {
this.expires = -1
}
addValidator (property, validator) {
if (!this.hasOwnProperty(property)) {
console.warn('No validator could be create for %c' + property + '%c. It is not an attribute of %c' + this.type + '%c.', NGN.css, '', NGN.css, '')
return
}
switch (typeof validator) {
case 'function':
this.validators[property] = this.validators[property] || []
this.validators[property].push(validator)
this.emit('validator.add', property)
break
case 'object':
if (Array.isArray(validator)) {
this.validators[property] = this.validators[property] || []
this.validators[property].push(function (value) {
return validator.indexOf(value) >= 0
})
this.emit('validator.add', property)
} else if (validator.test) {
this.validators[property] = this.validators[property] || []
this.validators[property].push(function (value) {
return validator.test(value)
})
this.emit('validator.add', property)
} else {
console.warn('No validator could be created for %c' + property + '%c. The validator appears to be invalid.', NGN.css, '')
}
break
case 'string':
case 'number':
case 'date':
this.validators[property] = this.validators[property] || []
this.validators[property].push(function (value) {
return value === validator
})
this.emit('validator.add', property)
break
default:
console.warn('No validator could be create for %c' + property + '%c. The validator appears to be invalid.', NGN.css, '')
}
}
removeValidator (attribute) {
if (this.validators.hasOwnProperty(attribute)) {
delete this.validators[attribute]
this.emit('validator.remove', attribute)
}
}
validate (attribute) {
if (!this.validation) {
return true
}
const me = this
if (attribute) {
if (this.validators.hasOwnProperty(attribute)) {
for (let i = 0; i < this.validators[attribute].length; i++) {
if (!me.validators[attribute][i].apply(me, [me[attribute]])) {
me.invalidDataAttributes.indexOf(attribute) < 0 && me.invalidDataAttributes.push(attribute)
return false
} else {
me.invalidDataAttributes = me.invalidDataAttributes.filter(function (attr) {
return attribute !== attr
})
}
}
if (!this.validateDataType(attribute)) {
this.invalidDataAttributes.push(attribute)
return false
}
}
return true
}
this.datafields.forEach(function (field) {
me.validate(field)
})
}
validateDataType (field) {
const fieldType = NGN.typeof(this[field])
const expectedType = NGN.typeof(this.fields[field].type)
if (fieldType !== 'null') {
return fieldType === expectedType
}
if (this[field] === null && this.fields[field].required) {
if (this.autoid && field === this.idAttribute) {
return true
}
return false
}
return true
}
getRelationshipField (fieldname) {
return this.joins[fieldname]
}
hasRelationship (fieldname) {
return this.joins.hasOwnProperty(fieldname)
}
getDataField (fieldname) {
return this.fields[fieldname]
}
getMappedFieldName (fieldname) {
if (!this.dataMap || !this.dataMap.hasOwnProperty(fieldname)) {
return fieldname
}
return this.dataMap[fieldname]
}
getReverseMappedFieldName (fieldname) {
if (!this.reverseMap || !this.reverseMap.hasOwnProperty(fieldname)) {
return fieldname
}
return this.reverseMap[fieldname]
}
hasDataField (fieldname) {
return this.fields.hasOwnProperty(fieldname)
}
hasMetaField (fieldname) {
if (!this.has(fieldname)) {
return false
}
return this.getDataField(fieldname).hidden
}
has (attribute, includeVirtuals = true) {
if (this.idAttribute === attribute || this.hasDataField(attribute) || this.hasRelationship(attribute)) {
return true
}
if (includeVirtuals && this.hasVirtualField(attribute)) {
return true
}
return false
}
serialize (obj, ignoreID = false) {
let _obj = obj || this.raw
let rtn = {}
for (let key in _obj) {
_obj.nonEnumerableProperties = _obj.nonEnumerableProperties || ''
if (this.fields.hasOwnProperty(key) && !this.hasMetaField(key)) {
key = key === 'id' ? this.idAttribute : key
if ((_obj.hasOwnProperty(key) && (_obj.nonEnumerableProperties.indexOf(key) < 0 && /^[a-z0-9 ]$/.test(key.substr(0, 1)))) || (_obj[key] !== undefined && _obj.enumerableProperties.indexOf(key) >= 0)) {
let dsc = Object.getOwnPropertyDescriptor(_obj, key)
if (!dsc.set) {
switch (typeof dsc.value) {
case 'function':
if (dsc.value.name === 'Date') {
rtn[key] = _obj[key].refs.toJSON()
} else if (dsc.value.name === 'RegExp') {
rtn[key] = dsc.value()
}
break
case 'object':
if (_obj[key] instanceof Array && !Array.isArray(_obj[key])) {
_obj[key] = _obj[key].slice(0)
}
rtn[key] = _obj[key]
break
default:
rtn[key] = NGN.coalesce(_obj[key], this.fields[key].default, null)
break
}
}
}
}
}
const me = this
this.relationships.forEach(function (r) {
rtn[r] = me.rawjoins[r].data
})
if ((rtn.hasOwnProperty(this.idAttribute) && ignoreID) || (rtn.hasOwnProperty(this.idAttribute) && rtn[this.idAttribute] === undefined)) {
delete rtn[this.idAttribute]
}
return rtn
}
addField (field, fieldcfg = null, suppressEvents = false) {
if (typeof fieldcfg === 'boolean') {
suppressEvents = fieldcfg
fieldcfg = null
}
const me = this
let cfg = null
if (field.toLowerCase() !== 'id') {
if (typeof field === 'object') {
if (!field.name) {
throw new Error('Cannot create data field. The supplied configuration does not contain a unique data field name.')
}
cfg = field
field = cfg.name
delete cfg.name
}
if (me[field] !== undefined) {
try {
const source = NGN.stack.pop()
console.warn('%c' + field + '%c data field defined multiple times (at %c' + source.path + '%c). Only the last defintion will be used.', NGN.css, '', NGN.css, '')
} catch (e) {
console.warn('%c' + field + '%c data field defined multiple times. Only the last definition will be used.', NGN.css, '', NGN.css, '')
}
delete me[field]
}
me.fields[field] = NGN.coalesce(cfg, fieldcfg, me.fields[field], {})
me.fields[field].required = NGN.coalesce(me.fields[field].required, false)
if (!me.fields[field].hasOwnProperty('type')) {
if (me.fields[field].hasOwnProperty('default')) {
let type = NGN.typeof(me.fields[field].default)
type = type.charAt(0).toUpperCase() + type.slice(1)
me.fields[field].type = eval(type)
}
}
me.fields[field].type = NGN.coalesce(me.fields[field].type, String)
if (field === me.idAttribute && me.autoid === true) {
me.fields[field].type = String
me.fields[field]['default'] = NGN.coalesce(me.autoidPrefix, '') + NGN.DATA.util.GUID() + NGN.coalesce(me.autoidPostfix, '')
} else {
me.fields[field]['default'] = NGN.coalesce(me.fields[field]['default'])
}
me.fields[field].hidden = NGN.coalesce(me.fields[field].hidden, false)
me.raw[field] = me.fields[field]['default']
me[field] = me.raw[field]
Object.defineProperty(me, field, {
get: function () {
return NGN.coalesce(me.raw[field], me.fields[field].default, null)
},
set: function (value) {
let old = me.raw[field]
if (old === value) {
return
}
const wasInvalid = !me.validate(field)
me.raw[field] = value
let c = {
action: 'update',
field: field,
old: old,
new: me.raw[field]
}
this.emit('changelog.append', c)
this.emit('field.update', c)
this.emit('field.update.' + field, c)
if (!me.validate(field)) {
me.emit('field.invalid', {
field: field
})
} else if (wasInvalid) {
me.emit('field.valid', {
field: field
})
}
}
})
if (!suppressEvents) {
let c = {
action: 'create',
field: field
}
this.emit('changelog.append', c)
this.emit('field.create', c)
}
if (me.fields.hasOwnProperty(field)) {
if (me.fields[field].hasOwnProperty('pattern')) {
me.addValidator(field, me.fields[field].pattern)
}
['min', 'max', 'enum'].forEach(function (v) {
if (me.fields[field].hasOwnProperty(v)) {
me.addValidator(field, function (val) {
return me._nativeValidators[v](me.fields[field][v], val)
})
}
})
if (me.fields[field].hasOwnProperty('required')) {
if (me.fields[field].required) {
me.addValidator(field, function (val) {
return me._nativeValidators.required(field, val)
})
}
}
if (me.fields[field].hasOwnProperty('validate')) {
if (typeof me.fields[field].validate === 'function') {
me.addValidator(field, function (val) {
return me.fields[field].validate.apply(me, [val])
})
} else {
const source = NGN.stack.pop()
console.warn('Invalid custom validation function (in %c' + source.path + '%c). The value passed to the validate attribute must be a function.', NGN.css, '')
}
}
}
} else if (me.id === null && me.autoid) {
me.id = NGN.coalesce(me.autoidPrefix, '') + NGN.DATA.util.GUID() + NGN.coalesce(me.autoidPostfix, '')
}
}
addMetaField (fieldname, fieldcfg = null) {
if (!this.has(fieldname)) {
this.hiddenFields = NGN.coalesce(this.hiddenFields, {})
this.hiddenFields[fieldname] = fieldcfg
this.addField.apply(this, arguments)
this.fields[fieldname].hidden = true
}
}
addVirtual (name, fn) {
const me = this
Object.defineProperty(this, name, {
get: function () {
return fn.apply(me)
}
})
}
addRelationshipField (name, cfg, suppressEvents) {
suppressEvents = suppressEvents !== undefined ? suppressEvents : false
if (this.rawjoins.hasOwnProperty(name) || this.fields.hasOwnProperty(name) || this.hasOwnProperty(name)) {
throw new Error(name + ' already exists. It cannot be added to the model again.')
}
if (typeof cfg === 'function' || typeof cfg === 'object' && !cfg.hasOwnProperty('type')) {
cfg = {
type: cfg
}
}
if (!cfg.type) {
throw new Error('Configuration has no reference! The reference must be an NGN.DATA.Model or NGN.DATA.Store.')
}
cfg.required = NGN.coalesce(cfg.required, true)
cfg.default = cfg.default || null
if (!this.joins.hasOwnProperty(name)) {
this.joins[name] = cfg
}
const me = this
let entityType = 'model'
if (cfg.type instanceof NGN.DATA.Store) {
entityType = 'store'
} else if (NGN.typeof(cfg.type) === 'array') {
if (cfg.type.length === 0) {
throw new Error(name + ' cannot be an empty store. A model must be provided.')
}
entityType = 'collection'
} else if (typeof cfg.type === 'object') {
if (cfg.type.model) {
entityType = 'store'
}
}
if (entityType === 'store') {
let storeCfg = {}
if (cfg.type instanceof NGN.DATA.Store) {
this.rawjoins[name] = cfg.type
storeCfg = null
} else if (cfg.type.model) {
storeCfg = cfg.type
} else {
throw new Error('Nested store configuration is invalid or was not recognized.')
}
if (storeCfg !== null) {
this.rawjoins[name] = new NGN.DATA.Store(storeCfg)
}
this.applyStoreMonitor(name)
} else if (entityType === 'collection') {
this.rawjoins[name] = new NGN.DATA.Store({
model: cfg.type[0]
})
this.applyStoreMonitor(name)
} else if (!cfg.type.data) {
this.rawjoins[name] = cfg.default !== null ? new cfg.type(cfg.default) : new cfg.type()
this.applyModelMonitor(name)
} else if (cfg.type.data) {
this.rawjoins[name] = cfg.type
this.applyStoreMonitor(name)
} else {
throw new Error('Nested store configuration is invalid or was not recognized.')
}
Object.defineProperty(this, name, {
enumerable: true,
get: function () {
return me.rawjoins[name]
}
})
if (!suppressEvents) {
let c = {
action: 'create',
field: name
}
this.emit('changelog.append', c)
this.emit('relationship.create', c)
}
}
applyModelMonitor (name) {
const model = this.rawjoins[name]
const me = this
let oldData = model.data
model.on('load', () => {
let payload = {
action: 'update',
field: name,
old: NGN.coalesce(oldData),
new: model.data,
join: true,
originalEvent: {
event: 'load',
record: model
}
}
oldData = model.data
me.emit('field.update', payload)
me.emit('field.update.' + name, payload)
})
model.on('field.update', function (delta) {
let payload = {
action: 'update',
field: name + '.' + delta.field,
old: delta.old,
new: delta.new,
join: true,
originalEvent: {
event: 'field.update',
record: model
}
}
me.emit('field.update', payload)
me.emit('field.update.' + name + '.' + delta.field, payload)
})
model.on('field.create', function (delta) {
let payload = {
action: 'update',
field: name + '.' + delta.field,
old: null,
new: null,
join: true,
originalEvent: {
event: 'field.create',
record: model
}
}
me.emit('field.update', payload)
me.emit('field.update.' + name + '.' + delta.field, payload)
})
model.on('field.remove', function (delta) {
let payload = {
action: 'update',
field: name + '.' + delta.field,
old: delta.value,
new: null,
join: true,
originalEvent: {
event: 'field.remove',
record: model
}
}
me.emit('field.update', payload)
me.emit('field.update.' + name + '.' + delta.field, payload)
})
model.on('field.invalid', function (data) {
me.emit('field.invalid')
me.emit('field.invalid.' + name + '.' + data.field)
})
model.on('field.valid', function (data) {
me.emit('field.valid')
me.emit('field.valid.' + name + '.' + data.field)
})
}
applyStoreMonitor (name) {
if (!this.rawjoins.hasOwnProperty(name)) {
return
}
if (this.rawjoins[name] instanceof NGN.DATA.Store) {
const me = this
this.rawjoins[name].on('record.create', function (record) {
let old = me[name].data
old.pop()
let c = {
action: 'update',
field: name,
join: true,
old: old,
new: me[name].data,
originalEvent: {
event: 'record.create',
record: record
}
}
me.emit('field.update', c)
me.emit('field.update.' + name, c)
})
this.rawjoins[name].on('record.update', function (record, delta) {
if (!delta) {
return
}
let c = {
action: 'update',
field: name + '.' + delta.field,
join: true,
old: delta.old,
new: delta.new,
originalEvent: {
event: 'record.update',
record: record
}
}
me.emit('field.update', c)
me.emit('field.update.' + name + '.' + delta.field, c)
})
this.rawjoins[name].on('record.delete', function (record) {
let old = me[name].data
old.push(record.data)
let c = {
action: 'update',
field: name,
join: true,
old: old,
new: me[name].data,
originalEvent: {
event: 'record.delete',
record: record
}
}
me.emit('field.update', c)
me.emit('field.update.' + name, c)
})
this.rawjoins[name].on('record.invalid', function (data) {
me.emit('field.invalid', data.field)
me.emit('field.invalid.' + name, data.field)
})
this.rawjoins[name].on('record.valid', function (data) {
me.emit('field.valid', data.field)
me.emit('field.valid.' + name, data.field)
})
}
}
removeField (name) {
if (this.raw.hasOwnProperty(name)) {
let val = this.raw[name]
delete this[name]
delete this.fields[name]
delete this.raw[name]
if (this.invalidDataAttributes.indexOf(name) >= 0) {
this.invalidDataAttributes.splice(this.invalidDataAttributes.indexOf(name), 1)
}
let c = {
action: 'delete',
field: name,
value: val
}
this.emit('field.remove', c)
this.emit('changelog.append', c)
}
}
removeVirtual (name) {
delete this[name]
}
removeRelationshipField (name, suppressEvents) {
suppressEvents = suppressEvents !== undefined ? suppressEvents : false
if (this.joins.hasOwnProperty(name)) {
let val = this.rawjoins[name]
delete this.rawjoins[name]
delete this[name]
delete this.joins[name]
if (!suppressEvents) {
let c = {
action: 'delete',
field: name,
old: val,
join: true
}
this.emit('changelog.append', c)
this.emit('relationship.remove', c)
}
}
}
hasVirtualField (fieldname) {
return this.virtuals.hasOwnProperty(fieldname)
}
setSilent (fieldname, value) {
if (fieldname === this.idAttribute) {
this.id = value
return
}
if (this.hasRelationship(fieldname)) {
if (this[fieldname] instanceof NGN.DATA.Store) {
this[fieldname].clear()
this[fieldname].bulk(null, value)
return
}
this[fieldname].load(value)
return
}
this.raw[fieldname] = value
}
undo (back = 1) {
let old = this.changelog.splice(this.changelog.length - back, back)
const me = this
old.reverse().forEach(function (change) {
if (!(typeof change.join === 'boolean' ? change.join : false)) {
switch (change.action) {
case 'update':
me[change.field] = change.old
break
case 'create':
me.removeField(change.field)
break
case 'delete':
me.addField(change.field)
me[change.field] = me.old
break
}
} else {
switch (change.action) {
case 'create':
me.removeRelationshipField(change.field)
break
case 'delete':
me.addRelationshipField(change.field)
me[change.field] = change.old
break
}
}
})
this.emit('changelog.remove', old.map((item) => {
return item.id
}))
}
load (data) {
data = data || {}
if (this._dataMap !== null) {
Object.keys(this.reverseMap).forEach((key) => {
if (data.hasOwnProperty(key)) {
data[this.reverseMap[key]] = data[key]
delete data[key]
}
})
}
Object.keys(data).forEach((key) => {
if (this.hasDataField(key)) {
if (this.raw.hasOwnProperty(key)) {
this.raw[key] = data[key]
} else if (key === this.idAttribute) {
this.id = data[key]
}
} else if (this.hasRelationship(key)) {
this.rawjoins[key].load(data[key])
} else if (key !== this.idAttribute && !this.hasMetaField(key)) {
try {
const source = NGN.stack.pop()
console.warn('%c' + key + '%c specified in %c' + source.path + '%c as a data field but is not defined in the model.', NGN.css, '', NGN.css, '')
} catch (e) {
console.warn('%c' + key + '%c specified as a data field but is not defined in the model.', NGN.css, '')
}
}
})
this.setUnmodified()
this.emit('load')
}
}
NGN.DATA = NGN.DATA || {}
Object.defineProperties(NGN.DATA, {
Model: NGN.const(function (cfg) {
const ModelLoader = function (data) {
let model = new NgnDataModel(cfg)
if (data) {
model.load(data)
}
return model
}
return ModelLoader
}),
Entity: NGN.private(NgnDataModel)
})
if (NGN.nodelike) {
module.exports = NGN.DATA
}