json-schema-entity
Version:
Manage a group of tables with a parent child relation in SQL that will be seen as a document, or entity, like a no SQL database
1,651 lines (1,592 loc) • 63.3 kB
JavaScript
'use strict'
var _ = require('lodash')
var co = require('@ayk/co')
var assert = require('assert')
var sqlView = require('sql-view')
var jst = require('json-schema-table')
var EntityError = require('./entity-error')
const {
convertToUpdatedAt,
getUpdatedAtColumnName
} = require('./adapters/common')
const getErrorMessage = err => err.precedingErrors ? err.precedingErrors.map(e => e.message).join('; ') : err.message
const isGenerator = obj =>
typeof obj.next === 'function' && typeof obj.throw === 'function'
function toArray(obj) {
return Array.isArray(obj) ? obj : (obj && [obj]) || []
}
const orderAssociations = (record, data) => {
_.forEach(data.associations, function (association) {
const key = association.data.key
const isArray = Array.isArray(record[key])
if (isArray) {
if (association.data.primarySortFields) {
record[key] = _.sortBy(
record[key],
association.data.primarySortFields
)
}
}
if (association.data.associations.length && record[key]) {
const recordset = isArray ? record[key] : [record[key]]
for (let i = 0; i < recordset.length; i++) {
recordset[i] = orderAssociations(
recordset[i],
association.data
)
}
}
})
return record
}
const hasEqualPrimaryKey = function (a, b, data) {
let same = false
_.forEach(data.primaryKeyAttributes, function (name) {
same = a[name] === b[name] || Number(a[name]) === Number(b[name])
return same
})
return same
}
function validateModel(is, was, data) {
var errors = []
function validate(is, was, data) {
return runModelValidations(is, was, data, errors).then(
function () {
return _.reduce(
data.associations,
function (chain, association) {
return chain.then(function () {
var associationKey = association.data.key
var from = is && is[associationKey]
var to = was && was[associationKey]
if (_.isArray(from) || _.isArray(to)) {
from = _.isArray(from)
? from.slice(0)
: from
? [from]
: []
to = _.isArray(to) ? to.slice(0) : to ? [to] : []
var pairs = []
var findAndRemove = function (arr, obj) {
var res = _.remove(arr, function (e) {
if (
hasEqualPrimaryKey(e, obj, association.data)
) {
return true
}
})
assert(
res.length < 2,
'Pair this was for validation should be 1 but found ' +
res.length
)
return res.length === 0 ? void 0 : res[0]
}
from.map(function (record) {
pairs.push({
from: record,
to: findAndRemove(to, record)
})
})
to.map(function (record) {
pairs.push({
to: record
})
})
return _.reduce(
pairs,
function (chain, pair) {
return validate(
pair.from,
pair.to,
association.data
)
},
Promise.resolve()
)
} else {
return validate(from, to, association.data)
}
})
},
Promise.resolve()
)
}
)
}
return validate(is, was, data).then(function () {
if (errors.length > 0) {
throw new EntityError({
message: 'Validation error',
type: 'ValidationError',
errors: errors
})
}
})
}
function decimalPlaces(num) {
var match = ('' + num).match(/(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/)
return (
(match &&
Math.max(
0,
// Number of digits right of decimal point.
(match[1] ? match[1].length : 0) -
// Adjust for scientific notation.
(match[2] ? +match[2] : 0)
)) ||
0
)
}
function validateFields(is, data) {
const errors = []
function validate(is, data) {
return runFieldValidations(is, data, errors).then(() =>
data.associations.reduce(
(chain, association) =>
chain.then(() =>
toArray(is && is[association.data.key]).reduce(
(chain, is) =>
chain.then(() => validate(is, association.data)),
Promise.resolve()
)
),
Promise.resolve()
)
)
}
return validate(is, data).then(() => {
if (errors.length > 0) {
throw new EntityError({
message: 'Validation error',
type: 'ValidationError',
errors: errors
})
}
})
}
function runFieldValidations(is, data, errors) {
var validator = data.validator
return Promise.resolve()
.then(function () {
_.forEach(is && data.properties, function (property, key) {
if (is[key] && is[key] !== null) {
var value = is[key]
if (
property.enum &&
property.enum.indexOf(value) === -1 &&
property.maxLength &&
property.enum
.map(value => value.substr(0, property.maxLength))
.indexOf(value) === -1
) {
errors.push({
path: key,
message:
"Value '" +
value +
"' not valid. Options are: " +
property.enum.join()
})
}
if (
!property.enum &&
property.maxLength &&
String(value).length > property.maxLength
) {
errors.push({
path: key,
message:
"Value '" +
value +
"' exceeds maximum length: " +
property.maxLength
})
}
if (
property.decimals &&
decimalPlaces(value) > property.decimals
) {
errors.push({
path: key,
message:
"Value '" +
value +
"' exceeds maximum decimals length: " +
property.decimals
})
}
}
})
})
.then(function () {
return _.reduce(
is && validator && data.properties,
function (chain, property, key) {
var validations = {}
if (is[key]) {
_.forEach(property.validate, function (validate, name) {
var args = _.map(validate.args, function (arg) {
if (
typeof arg === 'string' &&
arg.substr(0, 5) === 'this.'
) {
return is[arg.substring(5)]
} else {
return arg
}
})
validations[name] = {
id: key,
message: validate.message,
fn: validator[name],
args: [is[key]].concat(args)
}
})
if (
property.format &&
!validations[property.format] &&
validator[property.format]
) {
validations[property.format] = {
id: key,
fn: validator[property.format],
args: [is[key]]
}
}
}
return _.reduce(
validations,
function (chain, validation) {
return chain.then(function () {
var res
try {
res = validation.fn.apply(
validator,
validation.args
)
} catch (err) {
errors.push({
path: validation.id,
message: err.message
})
}
if (res === false) {
errors.push({
path: validation.id,
message:
validation.message || 'Invalid ' + validation.id
})
}
})
},
chain
)
},
Promise.resolve()
)
})
}
function runModelValidations(is, was, data, errors) {
return _.reduce(
data.validate,
function (chain, validation) {
return chain.then(function () {
if (
!(
(is && !was && validation.options.onCreate) ||
(is && was && validation.options.onUpdate) ||
(!is && was && validation.options.onDestroy)
)
) {
return
}
var res
try {
res = validation.fn.call(is || was)
if (res && isGenerator(res)) {
res = co(res)
}
} catch (err) {
errors.push({path: validation.id, message: err.message})
}
if (res && res.then) {
return res.catch(function (err) {
errors.push({path: validation.id, message: err.message})
})
} else {
if (res === false) {
errors.push({
path: validation.id,
message: 'Invalid ' + validation.id
})
}
}
})
},
Promise.resolve()
)
}
const tableRecordInfo = new WeakMap()
const tableRecordData = new WeakMap()
const timeStampsColumns = ['updatedAt']
class TableRecord {
constructor(trs, record, isNew, parent) {
const it = {
isNew,
parent,
info: tableRecordInfo.get(trs),
values: {}
}
tableRecordData.set(this, it)
it.info.data.instanceMethods.map(method =>
Object.defineProperty(this, method.id, {
value: method.fn
})
)
setIs(this, record, it, isNew)
// Object.freeze(this)
}
get entity() {
return tableRecordData.get(this).parent.self
}
validate() {
const it = tableRecordData.get(this)
return validateModel(this, this.was, it.info.data)
}
save(options) {
let entityData = tableRecordData.get(this)
const entity = entityData.parent.tableRecord
entityData =
entity === this ? entityData : tableRecordData.get(entity)
if (entityData.isNew) {
return entityData.parent.self
.create(entity, options)
.then(function () {
entityData.isNew = false
})
}
return entityData.parent.self.update(entity, null, options)
}
destroy(options) {
let entityData = tableRecordData.get(this)
const entity = entityData.parent.tableRecord
entityData =
entity === this ? entityData : tableRecordData.get(entity)
if (entityData.isNew) {
return new Promise(function (resolve, reject) {
reject(
new EntityError({
type: 'InvalidOperation',
message: 'Instance is new'
})
)
})
}
var primaryKey =
entityData.info.data.entity.primaryKeyAttributes[0]
var key = {where: {}}
key.where[primaryKey] = entity[primaryKey]
if (entityData.info.data.entity.timestamps) {
key.where.updatedAt = entity.updatedAt || null
}
return entityData.parent.self
.destroy(key, options, entity)
.then(function () {
entityData.isNew = true
var is = entityData.values
is.updatedAt = void 0
entityData.was = Object.freeze({})
})
}
get was() {
return tableRecordData.get(this).was
}
get db() {
return tableRecordData.get(this).parent.self.db
}
}
function instanceParent(value) {
return tableRecordData.get(value).parent
}
function isSameEntityInstance(value, parent) {
return (
value instanceof TableRecord &&
parent &&
instanceParent(value).tableRecord === parent.tableRecord
)
}
class TableRecordSchema {
constructor(data) {
const columns = new Map()
const methods = {}
const keys = []
tableRecordInfo.set(this, {columns, methods, data, keys})
_.forEach(data.properties, function (property, name) {
columns.set(name, function (value) {
if (value && value !== null) {
if (property.enum) {
let found
_.forEach(property.enum, function (item) {
if (
item === value ||
item.substring(0, property.maxLength) === value
) {
value = item
found = true
return false
}
})
if (!found) {
throw new EntityError({
type: 'InvalidColumnData',
message: 'Invalid value',
errors: [
{
path: name,
message:
"Value '" +
value +
"' not valid. Options are: " +
property.enum.join()
}
]
})
}
} else {
switch (property.type) {
case 'date':
value =
value instanceof Date
? value.toISOString().substr(0, 10)
: value
break
}
}
var validator = data.validator
if (validator) {
var values = this.values
var validations = []
var format = property.format
_.forEach(property.validate, function (validate, key) {
var args = _.map(validate.args, function (arg) {
if (
typeof arg === 'string' &&
arg.substr(0, 5) === 'this.'
) {
return values[arg.substring(5)]
} else {
return arg
}
})
validations.push({
message: validate.message || key,
fn: validator[key],
args: [value].concat(args)
})
if (key === format) {
format = null
}
})
if (format && validator[format]) {
validations.push({
message: 'Invalid format',
fn: validator[format],
args: [value]
})
}
_.forEach(validations, function (validation) {
if (
validation.fn.apply(validator, validation.args) ===
false
) {
throw new EntityError({
message: `Validation for '${name}' failed: ${validation.message}`,
type: 'ValidationError',
errors: [{path: name, message: validation.message}]
})
}
})
}
}
this.values[name] = value
})
})
_.forEach(data.associations, function (association) {
var name = association.data.key
columns.set(name, function (value) {
var build = value =>
isSameEntityInstance(value, this.parent)
? value
: buildEntity(
Object.assign({}, value),
association.data,
this.isNew,
false,
void 0,
void 0,
this.parent
)
this.values[name] = Array.isArray(value)
? value.map(value => build(value))
: value
? build(value)
: value
})
})
if (data.timestamps) {
columns.set('updatedAt', function () {
throw new Error('Column updatedAt cannot be modified')
})
}
for (var key of columns.keys()) {
keys.push(key)
}
}
}
function setIs(instance, record, it, isNew) {
const alreadyBuilt = it === void 0
it = it || tableRecordData.get(instance)
const was = {}
it.info.columns.forEach(function (value, key) {
if (!alreadyBuilt) {
Object.defineProperty(instance, key, {
get: function () {
return it.values[key]
},
set: it.info.columns.get(key).bind(it),
enumerable: true
})
}
const newValue = record[key]
if (newValue !== void 0) {
if (timeStampsColumns.indexOf(key) !== -1) {
it.values[key] = newValue
was[key] = newValue
} else {
try {
instance[key] = newValue
} catch (e) {
it.values[key] = newValue // force
}
if (_.isArray(newValue)) {
was[key] =
'was' in newValue
? newValue.was
: newValue.map(newValue =>
'was' in newValue
? newValue.was
: _.cloneDeep(instance[key])
)
} else if (_.isObject(newValue)) {
was[key] =
'was' in newValue
? newValue.was
: _.cloneDeep(instance[key])
} else {
was[key] = instance[key]
}
}
} else {
it.values[key] = void 0
}
})
function exists(record) {
return (
isNew !== true || record[it.info.data.foreignKey] !== void 0
)
}
if (exists(record)) {
Object.freeze(was)
it.was = was
}
}
function clearNulls(obj) {
Object.keys(obj).forEach(function (key) {
if (obj[key] === null) {
delete obj[key]
}
})
}
const isEmpty = v => v === void 0 || v === null
function buildPlainObject(record, data) {
clearNulls(record)
const props = data.schema.properties
Object.keys(record).forEach(key => {
const prop = props[key]
if (!prop) {
return
}
const value = record[key]
switch (prop.type) {
case 'date':
record[key] =
value instanceof Date
? value.toISOString().substr(0, 10)
: value
break
}
if (prop.enum) {
_.forEach(prop.enum, item => {
if (
item === value ||
item.substring(0, prop.maxLength) === value
) {
record[key] = item
return false
}
})
}
if (prop.mapper?.read) {
record[key] = prop.mapper.read(value, record)
}
})
_.forEach(data.associations, function (association) {
var key = association.data.key
if (!isEmpty(record[key])) {
var recordset = data.adapter.extractRecordset(
record[key],
association.data.coerce
)
for (var i = 0; i < recordset.length; i++) {
recordset[i] = buildPlainObject(
recordset[i],
association.data
)
}
record[key] =
recordset.length === 1 && association.type === 'hasOne'
? recordset[0]
: recordset
}
})
convertToUpdatedAt(record, data)
return record
}
function updateEntity(entity, values, data) {
data.propertiesList
.filter(key => key !== 'updatedAt')
.forEach(key => {
if (values[key] !== undefined) {
entity[key] = values[key]
}
})
_.forEach(data.associations, function (association) {
const extract = function (set, record) {
set = _.isArray(set) ? set : [set].filter(Boolean)
for (const item of set) {
if (hasEqualPrimaryKey(item, record, association.data)) {
return item
}
}
}
const key = association.data.key
if (values[key] === null) {
entity[key] = null
} else if (values[key] !== undefined) {
if (association.type === 'hasMany') {
const valueSet = _.isArray(values[key])
? values[key]
: [values[key]].filter(Boolean)
entity[key] = valueSet.map(value => {
return updateEntity(
extract(entity[key], value) || {},
value,
association.data
)
})
} else {
entity[key] = updateEntity(
entity[key] || {},
values[key],
association.data
)
}
}
})
return entity
}
function buildEntity(
record,
data,
isNew,
fromFetch,
instance,
self,
parent
) {
clearNulls(record)
if (fromFetch && data.readMappers) {
for (const {name, read} of data.readMappers) {
record[name] = read(record[name], record)
}
}
const isParent = !parent
parent = parent || (instance && instanceParent(instance)) || {self}
const associations = isSameEntityInstance(instance, parent)
? record
: {}
_.forEach(data.associations, function (association) {
var key = association.data.key
if (!isEmpty(record[key])) {
var recordset = fromFetch
? data.adapter.extractRecordset(
record[key],
association.data.coerce
)
: _.isArray(record[key])
? record[key]
: [record[key]]
var instanceSet = instance
? _.isArray(instance[key])
? instance[key]
: [instance[key]]
: void 0
for (var i = 0; i < recordset.length; i++) {
recordset[i] = buildEntity(
recordset[i],
association.data,
isNew,
fromFetch,
instanceSet && instanceSet[i],
void 0,
parent
)
}
associations[key] =
recordset.length === 1 && association.type === 'hasOne'
? recordset[0]
: recordset
}
})
if (isSameEntityInstance(instance, parent)) {
setIs(instance, record)
return instance
} else {
const r = _.extend(
_.pick(record, data.propertiesList),
associations
)
convertToUpdatedAt(record, data, r)
const tableRecord = new TableRecord(data.trs, r, isNew, parent)
if (isParent) {
parent.tableRecord = tableRecord
}
return tableRecord
}
}
function runHooks(hooks, model, options, data, validatedInstance) {
var allHooks = []
hooks.map(function (name) {
allHooks = allHooks.concat(data.hooks[name])
})
return _.reduce(
allHooks,
function (chain, hook) {
return chain.then(function () {
var res
try {
res = hook.fn.call(
validatedInstance || model,
options,
validatedInstance ? model : void 0
)
if (res && isGenerator(res)) {
res = co(res)
}
} catch (err) {
throw new EntityError({
type: hook.name + 'HookError',
message: err.message,
errors: [{path: hook.id, message: getErrorMessage(err)}],
err
})
}
if (res && res.then) {
return res.catch(err => {
throw new EntityError({
type: hook.name + 'HookError',
message: err.message,
errors: [{path: hook.id, message: getErrorMessage(err)}],
err
})
})
}
})
},
Promise.resolve()
)
}
function create(entity, options, data, adapter) {
var record
return runHooks(
['beforeCreate', 'beforeSave'],
entity,
options,
data
)
.then(function () {
record = _.pick(entity, data.propertiesList)
return adapter
.create(record, data, options)
.then(function (record) {
var newEntity = _.pick(record, data.propertiesList)
return _.reduce(
data.associations,
function (chain, association) {
const associationKey = association.data.key
var associatedEntity = entity[associationKey]
const recordIsArray = _.isArray(associatedEntity)
var hasMany =
(recordIsArray && associatedEntity.length > 1) ||
association.type === 'hasMany'
if (association.type === 'hasOne' && hasMany) {
throw new EntityError({
type: 'InvalidData',
message:
'Association ' +
associationKey +
' can not be an array'
})
}
associatedEntity =
associatedEntity === void 0 || recordIsArray
? associatedEntity
: [associatedEntity]
return _.reduce(
associatedEntity,
function (chain, entity) {
entity[association.data.foreignKey] =
newEntity[data.primaryKeyAttributes[0]]
return chain.then(function () {
return create(
entity,
options,
association.data,
adapter
).then(function (associationEntity) {
if (hasMany) {
newEntity[associationKey] =
newEntity[associationKey] || []
newEntity[associationKey].push(
associationEntity
)
} else {
newEntity[associationKey] = associationEntity
}
})
})
},
chain
)
},
Promise.resolve()
).then(function () {
return newEntity
})
})
})
.then(function (newRecord) {
return runHooks(
['afterCreate', 'afterSave'],
newRecord,
options,
data,
entity
).then(function () {
return newRecord
})
})
}
function update(entity, was, options, data, adapter) {
var record
return runHooks(
['beforeUpdate', 'beforeSave'],
entity,
options,
data
)
.then(function () {
record = _.pick(entity, data.propertiesList)
options = Object.assign({}, options, {where: {}})
data.primaryKeyAttributes.map(function (field) {
if (
entity[field] !== undefined ||
options.where[field] === undefined
) {
options.where[field] =
entity[field] === undefined ? null : entity[field]
}
})
if (data.timestamps && entity.updatedAt !== undefined) {
options.where.updatedAt = entity.updatedAt || null
}
return adapter
.update(record, data, options)
.then(function (res) {
assert(
res[0] === 1 ||
(typeof res === 'object' && res[0] === void 0),
'Record of ' +
data.key +
' found ' +
res[0] +
' times for update, expected 1.' +
' Check if your entity has two association with the same foreign key'
)
var modifiedEntity = record
return _.reduce(
data.associations,
function (chain, association) {
const associationKey = association.data.key
function exists(a) {
return a[association.data.foreignKey]
}
var find = function (entity, entities) {
if (!_.isArray(entities)) {
return entities
}
for (var i = 0; i < entities.length; i++) {
var obj = entities[i]
if (
hasEqualPrimaryKey(entity, obj, association.data)
) {
return obj
}
}
throw new EntityError({
type: 'InvalidData',
message:
'Record ' +
JSON.stringify(entity) +
' in association ' +
associationKey +
' has no previous data'
})
}
var associatedIsEntity = entity[associationKey]
var hasMany =
(_.isArray(associatedIsEntity) &&
associatedIsEntity.length > 1) ||
association.type === 'hasMany'
if (association.type === 'hasOne' && hasMany) {
throw new EntityError({
type: 'InvalidData',
message:
'Association ' +
associationKey +
' can not be an array'
})
}
var primaryKeyValue =
entity[data.primaryKeyAttributes[0]]
associatedIsEntity = _.isArray(associatedIsEntity)
? associatedIsEntity
: associatedIsEntity
? [associatedIsEntity]
: void 0
var toBeCreated = []
var toBeUpdated = []
_.forEach(associatedIsEntity, function (is) {
if (is[association.data.foreignKey] !== void 0) {
// Should convert to string before comparing
if (
is[association.data.foreignKey] != primaryKeyValue
) {
// eslint-disable-line
throw new EntityError({
type: 'InvalidData',
message:
'Foreign key in ' +
association.data.key +
' does not match primary key of ' +
data.key
})
}
}
if (exists(is)) {
toBeUpdated.push(is)
} else {
toBeCreated.push(is)
}
})
var associatedWasEntity = was[associationKey]
associatedWasEntity = _.isArray(associatedWasEntity)
? associatedWasEntity
: associatedWasEntity
? [associatedWasEntity]
: void 0
var toBeDeleted = []
_.forEach(associatedWasEntity, function (was) {
var hasIs = false
_.forEach(toBeUpdated, function (is) {
if (hasEqualPrimaryKey(was, is, association.data)) {
hasIs = true
return false
}
})
if (!hasIs) {
toBeDeleted.push(was)
}
})
return _.reduce(
toBeDeleted,
function (chain, entity) {
return chain.then(function () {
return destroy(
entity,
options,
association.data,
adapter
)
})
},
chain
)
.then(function () {
return _.reduce(
toBeUpdated,
function (chain, entity) {
return chain.then(function () {
return update(
entity,
find(entity, was[association.data.key]),
options,
association.data,
adapter
).then(function (associationEntity) {
if (hasMany) {
modifiedEntity[associationKey] =
modifiedEntity[associationKey] || []
modifiedEntity[associationKey].push(
associationEntity
)
} else {
modifiedEntity[associationKey] =
associationEntity
}
})
})
},
chain
)
})
.then(function () {
return _.reduce(
toBeCreated,
function (chain, entity) {
entity[association.data.foreignKey] =
modifiedEntity[data.primaryKeyAttributes[0]]
return chain.then(function () {
return create(
entity,
options,
association.data,
adapter
).then(function (associationEntity) {
if (hasMany) {
modifiedEntity[associationKey] =
modifiedEntity[associationKey] || []
modifiedEntity[associationKey].push(
associationEntity
)
} else {
modifiedEntity[associationKey] =
associationEntity
}
})
})
},
chain
)
})
},
Promise.resolve()
).then(function () {
return modifiedEntity
})
})
})
.then(function (updatedRecord) {
return runHooks(
['afterUpdate', 'afterSave'],
updatedRecord,
options,
data,
entity
).then(function () {
return updatedRecord
})
})
}
function destroy(entity, options, data, adapter) {
return runHooks(
['beforeDelete', 'beforeDestroy'],
entity,
options,
data
)
.then(function () {
return _.reduce(
data.associations,
function (chain, association) {
const associationKey = association.data.key
var associatedEntity = entity[associationKey]
const recordIsArray = _.isArray(associatedEntity)
associatedEntity =
associatedEntity === void 0 || recordIsArray
? associatedEntity
: [associatedEntity]
return _.reduce(
associatedEntity,
function (chain, entity) {
return chain.then(function () {
return destroy(
entity,
options,
association.data,
adapter
)
})
},
chain
)
},
Promise.resolve()
).then(function () {
options = Object.assign({}, options, {where: {}})
data.primaryKeyAttributes.map(function (field) {
options.where[field] =
entity[field] === undefined ? null : entity[field]
})
if (data.timestamps) {
options.where.updatedAt = entity.updatedAt || null
}
return adapter.destroy(data, options)
})
})
.then(function (deletedEntity) {
return runHooks(
['afterDestroy', 'afterDelete'],
deletedEntity,
options,
data,
entity
)
})
}
module.exports = function (schemaName, schema, config) {
config = Object.assign({}, config)
if (config.dialect) {
if (config.dialect === 'pg') config.dialect = 'postgres'
if (config.dialect === 'ms') config.dialect = 'mssql'
} else {
config.dialect = 'postgres'
}
var adapter = getAdapter(config.dialect)
var sv = sqlView(config.dialect)
var entity = entityFactory(schemaName, schema, rebuild)
entity.entity = entity
function entityFactory(schemaName, schema, rebuild) {
const publicAssociationMethods = [
'setTitle',
'setDescription',
'setProperties',
'hasMany',
'hasOne',
'validate',
'instanceMethod',
'foreignKey',
'beforeCreate',
'afterCreate',
'beforeUpdate',
'afterUpdate',
'beforeSave',
'afterSave',
'beforeDelete',
'afterDelete',
'beforeDestroy',
'afterDestroy'
]
const reservedInstanceMethodsNames = [
'constructor',
'entity',
'validate',
'save',
'destroy',
'was',
'db'
]
const identity = splitAlias(schemaName)
var data = {
validator: config.validator,
identity: identity,
table: schema || {},
adapter, // Use only when db access is not required
title: schema && schema.title,
description: schema && schema.description,
key: identity.as || identity.name,
associations: [],
requestedProperties: (schema && schema.properties) || {},
reservedInstanceMethodsNames,
propertiesList: [],
schema: {},
primaryKey: schema.primaryKey,
hooks: {},
coerce: [],
public: {
new(db) {
var newEntity = {}
data.entityMethods.map(method =>
Object.defineProperty(newEntity, method.id, {
value: method.fn
})
)
return Object.assign(newEntity, {
get db() {
return db // For db access
},
get adapter() {
return Object.assign({db}, data.adapter) // For db access
},
get id() {
return data.public.id
},
get alias() {
return data.public.alias
},
get dialect() {
return config.dialect
},
get timestampsSuffix() {
return data.timestamps
},
get schema() {
return data.public.schema
},
fetch(criteria, options) {
criteria = criteria || {}
var self = this
return Promise.resolve().then(function () {
options = options || {}
var where = _.extend({}, criteria.where, data.scope)
if (where.updatedAt !== undefined) {
const updatedAtColumnName =
getUpdatedAtColumnName(data)
if (updatedAtColumnName) {
where[updatedAtColumnName] = where.updatedAt
delete where.updatedAt
}
}
criteria = _.extend({}, criteria)
criteria.where = where
var view = sv.build(
data.adapter.buildQuery(entity, options),
criteria
)
return db
.query(view.statement, view.params, options)
.then(function (res) {
const recordset = res.map(function (record) {
return options.toPlainObject === true ||
options.fetchExternalDescription === true
? buildPlainObject(record, data)
: buildEntity(
record,
data,
false,
true,
void 0,
self
)
})
return recordset
})
})
},
create: function (entity, options) {
options = options || {}
var self = this
var isInstance =
entity instanceof TableRecord &&
entity.entity === this
return (
isInstance
? Promise.resolve()
: validateFields(entity, data)
)
.then(function () {
entity = isInstance
? entity
: buildEntity(
entity,
data,
true,
void 0,
void 0,
self
)
})
.then(function () {
return validateModel(entity, void 0, data, options)
.then(function () {
return options.transaction
? create(entity, options, data, self.adapter)
: db.transaction(function (transaction) {
return create(
entity,
Object.assign({}, options, {
transaction
}),
data,
self.adapter
)
}, options)
})
.then(function (record) {
orderAssociations(record, data)
let res =
options.toPlainObject === true && !isInstance
? record
: buildEntity(
record,
data,
false,
false,
entity,
self
)
return res
})
})
},
update: function (entity, key, options) {
var self = this
key = key || entity[data.primaryKeyAttributes[0]]
if (!key) {
return Promise.resolve().then(function () {
throw new EntityError({
type: 'InvalidArgument',
message:
'Entity ' +
data.key +
' need a primary key for update'
})
})
}
options = options || {}
if (typeof key !== 'object') {
var id = key
key = {where: {}}
key.where[data.primaryKeyAttributes[0]] = id
} else if (!key.where) {
return Promise.resolve().then(function () {
throw new EntityError({
type: 'InvalidArgument',
message:
'Where clause not defined for entity ' +
data.key +
' update'
})
})
}
if (data.timestamps) {
const updatedAt =
entity.updatedAt || key.where.updatedAt
if (updatedAt !== undefined) {
key.where.updatedAt = updatedAt || null
}
}
var isInstance =
entity instanceof TableRecord &&
entity.entity === this
return (
isInstance
? Promise.resolve([entity.was])
: this.fetch(key, options)
).then(function (was) {
if (was.length === 0) {
throw new EntityError({
type: 'RecordModifiedOrDeleted',
message:
'Entity {' +
data.key +
'} key ' +
JSON.stringify(key.where) +
' not found for update'
})
}
assert(was.length === 1)
return (
isInstance
? Promise.resolve()
: validateFields(entity, data)
)
.then(function () {
entity = isInstance
? entity
: updateEntity(
buildEntity(
_.cloneDeep(was[0]),
data,
void 0,
void 0,
void 0,
self
),
entity,
data
)
})
.then(function () {
return validateModel(
entity,
was[0],
data,
options
)
})
.then(function () {
return options.transaction
? update(
entity,
was[0],
options,
data,
self.adapter
)
: db.transaction(function (transaction) {
return update(
entity,
was[0],
Object.assign({}, options, {transaction}),
data,
self.adapter
)
}, options)
})
.then(function (record) {
orderAssociations(record, data)
let res =
options.toPlainObject === true && !isInstance
? record
: buildEntity(
record,
data,
false,
false,
entity,
self
)
return res
})
})
},
destroy: function (key, options, entity) {
var self = this
if (!key) {
return Promise.resolve().then(function () {
throw new EntityError({
type: 'InvalidArgument',
message:
'Entity ' +
data.key +
' need a primary key for delete'
})
})
}
options = options || {}
if (typeof key !== 'object') {
var id = key
key = {where: {}}
key.where[data.primaryKeyAttributes[0]] = id
} else if (!key.where) {
return Promise.resolve().then(function () {
throw new EntityError({
type: 'InvalidArgument',
message:
'Where clause not defined for entity ' +
data.key +
' delete'
})
})
}
if (data.timestamps) {
key.where.updatedAt = key.where.updatedAt || null
}
var isInstance =
entity instanceof TableRecord &&
entity.entity === this
return (
isInstance
? Promise.resolve([entity.was])
: this.fetch(key, options)
).then(function (was) {
if (was.length === 0) {
throw new EntityError({
type: 'RecordModifiedOrDeleted',
message:
'Entity {' +
data.key +
'} key ' +
JSON.stringify(key.where) +
' not found for delete'
})
}
assert(was.length === 1)
return (
isInstance
? Promise.resolve()
: validateFields(entity, data)
)
.then(function () {
entity = isInstance ? entity : was[0]
})
.then(function () {
return validateModel(
void 0,
entity,
data,
options
)
})
.then(function () {
return options.transaction
? destroy(entity, options, data, self.adapter)
: db.transaction(function (transaction) {
return destroy(
entity,
Object.assign({}, options, {transaction}),
data,
self.adapter
)
}, options)
})
})
},
createInstance: function (entity) {
return buildEntity(
entity || {},
data,
true,
void 0,
void 0,
this
)
},
createTables: function () {
var self = this
return Promise.resolve().then(function () {
var tables = []
getTables(data, tables)
var jsts = []
tables.map(function (table) {
jsts.push(
jst(table.name, table.schema, {db: self.db})
)
})
return jsts.reduce(function (promise, jst) {
return promise.then(function () {
return jst.create().then(function () {
if (data.timestamps) {
re