dobo
Version:
Database ORM/ODM for Bajo Framework
197 lines (190 loc) • 9.12 kB
JavaScript
import genericPropSanitizer from './generic-prop-sanitizer.js'
async function sanitizeFeature (item) {
const { get, isPlainObject, mergeWith, isArray } = this.lib._
for (const f of item.feature) {
const feature = get(this.feature, f.name) // source from collectFeature
if (!feature) this.fatal('unknownFeature%s%s', f.name, item.name)
let [ns, path] = f.name.split('.')
if (!path) ns = this.name
const input = await feature.call(this.app[ns], f)
let props = input.properties
if (isPlainObject(props)) props = [props]
if (props.length > 0) item.properties.push(...props)
item.globalRules = item.globalRules ?? []
if (input.globalRules) {
item.globalRules = mergeWith(item.globalRules, input.globalRules, (oval, sval) => {
if (isArray(oval)) return oval.concat(sval)
})
}
}
}
async function sanitizeIndexes (item) {
const { generateId } = this.app.bajo
for (const idx of item.indexes) {
if (!idx.name) idx.name = `${(item.collName ?? item.name).toUpperCase()}_${generateId()}`
if (!(typeof idx.fields === 'string' || Array.isArray(idx.fields))) this.fatal('onlyAcceptFields%s%s', 'indexes', item.name)
}
}
async function sanitizeFullText (item) {
for (const f of item.fullText.fields ?? []) {
const prop = item.properties.find(p => p.name === f)
if (!prop) this.fatal('invalidFieldName%s%s', f, item.name)
// if (prop.type !== 'text') fatal.call(this, 'Fulltext index only available for field type \'text\' in \'%s@%s\'', f, item.name)
}
}
async function sanitizeSchema (items) {
const { defaultsDeep } = this.lib.aneka
const { freeze, fatal, importModule, join, breakNsPath, runHook } = this.app.bajo
const { isEmpty, orderBy, map, keys, findIndex, find, each, isString, get, isPlainObject, camelCase, uniq, filter } = this.lib._
const propTypes = keys(this.propType)
const schemas = []
this.log.debug('loadingDbSchemas')
for (const k in items) {
this.log.trace('- %s (%d)', k, keys(items[k]).length)
for (const f in items[k]) {
const item = items[k][f]
item.cacheable = item.cacheable ?? true
await runHook(`dobo.${camelCase(item.name)}:beforeSanitizeSchema`, item)
const conn = find(this.connections, { name: item.connection })
if (!conn) fatal.call(this, 'Unknown connection \'%s@%s\'', item.name, item.connection)
item.fullText = item.fullText ?? { fields: [] }
item.indexes = item.indexes ?? []
const { ns, path: type } = breakNsPath(conn.type)
const driver = find(this.drivers, { type, ns, driver: conn.driver })
if (driver.lowerCaseColl) item.name = item.name.toLowerCase()
let file = `${ns}:/${this.name}/lib/${type}/prop-sanitizer.js`
let propSanitizer = await importModule(file)
if (!propSanitizer) propSanitizer = genericPropSanitizer
for (const idx in item.properties) {
let prop = item.properties[idx]
if (isString(prop)) {
let [name, type, maxLength, index, required] = prop.split(':')
if (!type) type = 'string'
maxLength = maxLength ?? 255
prop = { name, type }
if (type === 'string') prop.maxLength = parseInt(maxLength) || undefined
if (!isEmpty(index)) prop.index = { type: index === 'true' ? 'default' : index }
prop.required = required === 'true'
item.properties[idx] = prop
} else {
if (isString(prop.index)) prop.index = { type: prop.index }
else if (prop.index === true) prop.index = { type: 'default' }
}
}
const idx = findIndex(item.properties, { name: 'id' })
if (idx === -1) item.properties.unshift(driver.idField)
item.feature = item.feature ?? []
await sanitizeFeature.call(this, item)
item.disabled = item.disabled ?? []
if (item.disabled === 'all') item.disabled = ['find', 'get', 'create', 'update', 'remove']
else if (item.disabled === 'readonly') item.disabled = ['create', 'update', 'remove']
for (const idx in item.properties) {
let prop = item.properties[idx]
if (!prop.type) {
prop.type = 'string'
prop.maxLength = 255
}
if (!propTypes.includes(prop.type)) {
let success = false
const feature = get(this.feature, isPlainObject(prop.type) ? prop.type.name : prop.type)
if (feature) {
let opts = { fieldName: prop.name }
if (isPlainObject(prop.type)) opts = defaultsDeep(opts, prop.type)
else opts.name = prop.type
const feat = find(item.feature, opts)
if (!feat) item.feature.push(opts)
const input = await feature.call(this, opts)
if (input.properties && input.properties.length > 0) {
prop = input.properties[0]
success = true
}
}
if (!success) fatal.call(this, 'Unsupported property type \'%s@%s\' in \'%s\'', prop.type, prop.name, item.name)
}
if (prop.index) {
if (prop.index === 'unique') prop.index = { type: 'unique' }
else if (prop.index === 'fulltext') prop.index = { type: 'fulltext' }
else if (prop.index === 'primary') prop.index = { type: 'primary' }
else if (prop.index === true) prop.index = { type: 'default' }
}
await propSanitizer.call(this, { prop, schema: item, connection: conn, driver })
if (prop.index && prop.index.type === 'primary' && prop.name !== 'id') fatal.call(this, 'Primary index should only be used for \'id\' field')
if (prop.index && prop.index.type === 'fulltext') {
item.fullText.fields.push(prop.name)
prop.index.type = 'default'
}
item.properties[idx] = prop
}
await sanitizeIndexes.call(this, item)
await sanitizeFullText.call(this, item)
const all = []
each(item.properties, p => {
if (!all.includes(p.name)) all.push(p.name)
else fatal.call(this, 'Field \'%s@%s\' should be used only once', p.name, item.name)
})
file = `${ns}:/${this.name}/lib/${type}/schema-sanitizer.js`
const schemaSanitizer = await importModule(file)
if (schemaSanitizer) await schemaSanitizer.call(this, { schema: item, connection: conn, driver })
schemas.push(item)
}
}
for (const i in schemas) {
const schema = schemas[i]
const sortables = []
const hidden = []
for (const i2 in schema.properties) {
const prop = schema.properties[i2]
if (prop.type !== 'string') delete prop.maxLength
if (prop.rel) {
for (const key in prop.rel) {
let def = prop.rel[key]
if (isString(def)) {
const [rschema, rfield] = def.split(':')
def = { schema: rschema, propName: rfield }
}
def.type = def.type ?? 'one-to-one'
const rel = find(schemas, { name: def.schema })
if (!rel) {
fatal.call(this, 'No schema found for relationship \'%s@%s:%s\'', `${def.schema}:${def.propName}`, schema.name, prop.name)
}
const rprop = find(rel.properties, { name: def.propName })
if (!rprop) fatal.call(this, 'No property found for relationship \'%s@%s:%s\'', `${def.schema}:${def.propName}`, schema.name, prop.name)
def.fields = def.fields ?? '*'
if (['*', 'all'].includes(def.fields)) {
const relschema = find(schemas, { name: def.schema })
def.fields = relschema.properties.map(p => p.name)
}
if (def.fields.length > 0 && !def.fields.includes('id')) def.fields.push('id')
for (const f of def.fields) {
const p = find(rel.properties, { name: f })
if (!p) fatal.call(this, 'Unknown property for field \'%s\' in relationship \'%s@%s:%s\'', p, `${def.schema}:${def.propName}`, schema.name, prop.name)
}
prop.type = rprop.type
if (rprop.type === 'string') prop.maxLength = rprop.maxLength
else {
delete prop.maxLength
delete prop.minLength
}
prop.rel[key] = def
}
// TODO: propSanitizer must be called again
}
if (prop.hidden) hidden.push(prop.name)
if (prop.index) sortables.push(prop.name)
delete prop.hidden
schema.properties[i2] = prop
}
// sortables
each(schema.indexes, item => {
sortables.push(...item.fields)
})
// hidden
schema.hidden = uniq(filter((schema.hidden ?? []).concat(hidden), item => map(schema.properties, 'name').includes(item)))
schema.sortables = sortables
await runHook(`dobo.${camelCase(schema.name)}:afterSanitizeSchema`, schema)
}
this.schemas = orderBy(schemas, ['buildLevel', 'name'])
freeze(this.schemas)
this.log.debug('loadedSchemas%s', join(map(this.schemas, 'name')))
}
export default sanitizeSchema