openrecord
Version:
Active record like ORM for nodejs
350 lines (290 loc) • 9.34 kB
JavaScript
const Definition = require('../../definition')
const TYPES = {
int: 'integer',
integer: 'integer',
smallint: 'integer',
bigint: 'integer',
oid: 'integer',
smallserial: 'integer',
serial: 'integer',
bigserial: 'integer',
money: 'float',
real: 'float',
'double precision': 'float',
float: 'float',
double: 'float',
numeric: 'float',
decimal: 'float',
number: 'float',
bytea: 'binary',
blob: 'binary',
binary: 'binary',
hstore: 'hstore',
tsvector: 'tsvector',
boolean: 'boolean',
date: 'date',
timestamp: 'datetime',
'timestamp with time zone': 'datetime',
'timestamp without time zone': 'datetime',
time: 'time',
'time without time zone': 'time',
'time with time zone': 'time',
interval: 'interval',
json: 'json',
jsonb: 'json',
uuid: 'uuid',
text: 'string',
varchar: 'string',
'character varying': 'string',
char: 'string',
character: 'string',
point: 'point',
line: 'line',
lseg: 'path',
box: 'box',
path: 'path',
polygon: 'polygon',
circle: 'circle'
}
/*
* DEFINITION
*/
exports.definition = {
setTableAttributes: function(attributes) {
var self = this
return self.store.utils.series(
attributes.map(function(attr) {
return function() {
self.attribute(attr.name, attr.type, attr.options)
attr.validations.forEach(function(validation) {
self[validation.name].apply(
self,
[attr.name].concat(validation.args)
)
})
if (attr.type === 'composite') {
// add custom validation to call composite type validation
self.validates(function() {
var parent = this
var child = this[attr.name]
if (
parent[attr.name] &&
typeof parent[attr.name].isValid === 'function'
) {
return child.isValid(function(valid) {
if (!valid) {
child.errors.each(function(field, error) {
parent.errors.add(attr.name + '.' + field, error)
})
throw parent.errors
}
})
}
})
// create a new "trimmed" model for the composite type
return self.createCompositeType(
attr.type_name,
attr.type_attributes,
attr
)
}
}
})
)
},
createCompositeType: function(name, attributes, attr) {
var store = this.store
var setAttrOptions = function(Model) {
store.dynamicAttributeTypes[name] = Model
attr.options.dynamicType = Model
attr.options.default = attr.options.default || {}
attr.options.use = function(fn) {
return fn.call(Model.definition)
}
attr.options.attributes = attributes.map(function(a) {
return a.name
})
}
if (store.dynamicAttributeTypes[name]) {
// is shared between all attributes with the same type
var Model = store.dynamicAttributeTypes[name]
return setAttrOptions(Model)
}
// create a new definition (like a model, but without all the save stuff)
var definition = new Definition(store, name)
// disable cache, because the parent model will cach it via it's attributes...
definition.cacheDisabled = true
// include only important parts
definition.include(
require('../../base').concat(
require('../../stores/sql/data_types'),
require('../../stores/sql/utils'),
require('../../stores/postgres/attributes'),
require('../../stores/postgres/data_types')
)
)
// add attributes and validations
definition.use(function() {
return this.setTableAttributes(attributes)
})
return definition.define().then(function(result) {
setAttrOptions(result.Model)
})
}
}
exports.store = {
loadTableAttributes: function(name) {
var self = this
var tableInfoSql = [
'SELECT a.attname, format_type(a.atttypid, a.atttypmod), pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, t.typoutput, t.typanalyze, c.description',
'FROM pg_attribute a',
'LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum',
'LEFT JOIN pg_type t ON a.atttypid = t.oid',
'LEFT JOIN pg_description c ON a.attrelid = c.objoid AND a.attnum = c.objsubid',
"WHERE a.attrelid = '" +
name +
"'::regclass AND a.attnum > 0 AND NOT a.attisdropped",
'ORDER BY a.attnum'
].join(' ')
var tablePrimaryKeysSql =
"SELECT pg_attribute.attname FROM pg_index, pg_class, pg_attribute WHERE pg_class.oid = '" +
name +
"'::regclass AND indrelid = pg_class.oid AND pg_attribute.attrelid = pg_class.oid AND pg_attribute.attnum = any(pg_index.indkey) AND indisprimary"
return Promise.all([
self.connection.raw(tableInfoSql),
self.connection.raw(tablePrimaryKeysSql)
]).then(function(data) {
const result = data[0].rows
const pkeys = data[1].rows.map(function(r) {
return r.attname
})
return Promise.all(
result.map(function(attribute) {
var type = simplifiedType(attribute.format_type)
var primary = pkeys.indexOf(attribute.attname) !== -1
var hasDefaultValue = !!attribute.pg_get_expr
var attrDef = {
name: attribute.attname,
type: type,
options: {
description: attribute.description,
persistent: true,
primary: primary,
notnull: attribute.attnotnull,
default: extractValueFromDefault(attribute.pg_get_expr),
length: getMaxLength(attribute.format_type),
writable: !(primary && hasDefaultValue) // set to false if primary with default value
},
validations: extractValidations(attribute)
}
// IF CUSTOM TYPE!
if (!type) {
if (attribute.typoutput === 'enum_out') {
// get enum types
return self.connection
.raw(
'select enumlabel from pg_enum where enumtypid = ? ORDER BY enumsortorder',
attribute.atttypid
)
.then(function(enums) {
var values = enums.rows.map(function(r) {
return r.enumlabel
})
attrDef.type = 'string'
attrDef.validations.push({
name: 'validatesInclusionOf',
args: [values]
})
return attrDef
})
}
// set to composite type
var typeName = attribute.format_type
attrDef.type = 'composite'
// and load the type
return self
.loadTableAttributes(typeName)
.then(function(attributes) {
attrDef.type_name = typeName
attrDef.type_attributes = attributes
return attrDef
})
.catch(function() {
// ignore error and use default type `string`
attrDef.type = 'string'
return attrDef
})
}
return Promise.resolve(attrDef)
})
)
})
}
}
function getMaxLength(type) {
var len = type.match(/\((\d+)\)/)
if (len) {
len = parseInt(len[1])
if (len > 1) {
return len
}
}
return null
}
function simplifiedType(type, attribute) {
type = type.replace(/\((.+)\)/, '')
var array = !!type.match(/.+\[\]$/)
type = type.replace(/\[\]$/, '')
var simpleType = TYPES[type]
if (!simpleType) {
return null
}
if (array) return simpleType + '_array'
return simpleType
}
function extractValidations(attribute) {
var validations = []
// max length validation
var len = getMaxLength(attribute.format_type)
if (len) {
validations.push({ name: 'validatesLengthOf', args: [len] })
}
// not null
if (attribute.attnotnull && !attribute.pg_get_expr) {
validations.push({ name: 'validatesPresenceOf', args: [] })
}
return validations
}
function extractValueFromDefault(value) {
if (value === null || value.match(/uuid_generate/)) {
return null
}
var m
m = value.match(/\(?(-?\d+(\.\d*)?\)?(::bigint)?)/)
if (m) return m[1]
m = value.match(/\(?'(.*)'::.*\b(?:character varying|bpchar|text)/m)
if (m) return m[1]
m = value.match(/'(.*)'::bytea/m)
if (m) return m[1]
m = value.match(/'(.+)'::(?:time(?:stamp)? with(?:out)? time zone|date)/)
if (m) return m[1]
m = value.match(/'(.*)'::interval/)
if (m) return m[1]
m = value === 'true'
if (m) return true
m = value === 'false'
if (m) return false
m = value.match(/'(.*)'::(?:point|line|lseg|box|"?path"?|polygon|circle)/)
if (m) return m[1]
m = value.match(/'(.*)'::(?:cidr|inet|macaddr)/)
if (m) return m[1]
m = value.match(/'(.*)'::"?bit(?: varying)?"?/)
if (m) return m[1]
m = value.match(/'(.*)'::xml/m)
if (m) return m[1]
m = value.match(/'(.*)'::"?\D+"?\[\]/)
if (m) return m[1]
m = value.match(/-?\d+/)
if (m) return m[1]
return null
}