dobo-knex
Version:
Knex for Dobo
332 lines (304 loc) • 12.8 kB
JavaScript
import knex from 'knex'
const propertyKeys = ['specificType', 'precision', 'textType', 'scale', 'unsigned', 'comment', 'autoInc']
async function knexFactory () {
const { DoboDriver } = this.app.baseClass
const { getPluginFile, importPkg } = this.app.bajo
const { fs } = this.app.lib
const { omit, has, forOwn, cloneDeep, isEmpty, isArray } = this.app.lib._
const { defaultsDeep } = this.app.lib.aneka
const mongoKnex = await importPkg('dobo:@tryghost/mongo-knex')
class DoboKnexDriver extends DoboDriver {
static propertyKeys = propertyKeys
constructor (plugin, name, options = {}) {
super(plugin, name, options)
this.idField = {
name: 'id',
type: 'integer',
required: true,
autoInc: true,
index: 'primary'
}
this.support.truncate = false
this.support.returning = false
this.support.uniqueIndex = true
this.support.transaction = true
this.adapter = null
}
async connect (connection, noRebuild) {
const dialectFile = getPluginFile(this.dialectFile ?? `${this.app.doboKnex.ns}:node_modules/knex/lib/dialects/${this.dialect}/index.js`)
if (!fs.existsSync(dialectFile)) this.plugin.fatal('notFound%s%s', this.plugin.t('dialectFile'), dialectFile)
const dbDriver = (await import(dialectFile)).default
const adapter = this.adapter ?? this.dialect
let dbAdapter = await importPkg(`main:${adapter}`)
if (!dbAdapter) dbAdapter = await importPkg(`${this.plugin.ns}:${adapter}`)
if (!dbAdapter) throw this.plugin.fatal('dbAdapterNotInstalled%s', dbAdapter)
dbDriver.prototype._driver = () => dbAdapter
connection.client = knex(defaultsDeep({ connection: connection.options }, { client: dbDriver }, this.options))
}
getClient (model, options = {}) {
const { get } = this.app.lib._
const client = model.connection.client
const key = 'context.client.config.connName'
if (options.trx && get(options, `trx.${key}`) === get(client, key)) return options.trx
return client
}
async modelExists (model, options = {}) {
const client = this.getClient(model, options)
const exists = await client.schema.hasTable(model.collName)
return { data: !!exists }
}
async buildModel (model, options = {}) {
const client = this.getClient(model, options)
await client.schema.createTable(model.collName, table => {
for (const p of model.getProperties({ noVirtual: true })) {
const prop = cloneDeep(p)
if (prop.specificType) {
table.specificType(prop.name, prop.specificType)
continue
}
if (['object', 'array'].includes(prop.type) && !this.support.propType[prop.type]) prop.type = 'text'
const args = []
for (const item of ['maxLength', 'precision', 'textType']) {
if (has(prop, item)) args.push(prop[item])
if (item === 'precision' && has(prop, 'scale')) args.push(prop.scale)
}
let col
if (prop.autoInc && ['smallint', 'integer'].includes(prop.type)) col = table.increments(prop.name)
else if (prop.specificType) table.specificType(prop.name, prop.specificType)
else col = table[prop.type](prop.name, ...args)
if (prop.required) col.notNullable()
if (prop.unsigned && ['integer', 'smallint', 'float', 'double'].indexOf(prop.type)) col.unsigned()
if (prop.comment) col.comment(prop.comment)
if (options.onColumn) options.onColumn.call(this, model, table, col)
}
for (const index of model.getIndexes()) {
let opts = omit(index, ['name', 'type', 'fields'])
switch (index.type) {
case 'primary': {
if (isEmpty(opts)) opts = undefined
// opts.constraintName = index.name
table.primary(index.fields, opts)
break
}
case 'unique': {
opts.indexName = index.name
if (this.support.uniqueIndex) table.unique(index.fields, opts)
else table.index(index.fields, index.name)
break
}
case 'index': {
if (isEmpty(opts)) opts = undefined
table.index(index.fields, index.name, opts)
break
}
}
}
const engine = model.engine ?? this.defaultEngine
if (engine) table.engine(engine)
if (options.onTable) options.onTable.call(this, model, table)
})
return { data: true }
}
async clearRecord (model, options = {}) {
const client = this.getClient(model, options)
const op = this.support.truncate ? 'truncate' : 'del'
await client(model.collName)[op]()
return { data: true }
}
async dropModel (model, options = {}) {
const client = this.getClient(model, options)
await client.schema.dropTable(model.collName)
return { data: true }
}
async bulkCreateRecord (model, bodies = [], options = {}) {
const client = this.getClient(model, options)
await client(model.collName).insert(bodies)
return { data: true }
}
async createRecord (model, body = {}, options = {}) {
const client = this.getClient(model, options)
const result = await client(model.collName).insert(body, this._getReturningFields(model, options))
if (options.noResult) return
if (this.support.returning) return { data: result[0] }
return await this.getCreatedRecord(model, body, result, options)
}
/**
* Get newly created record. This is the case for ```mysql```, other DB that doesn't support returning
* should extend this method
*
* @async
* @param {Object} model
* @param {Object} body
* @param {Object} result
* @param {Object} [options]
* @returns {Object}
*/
async getCreatedRecord (model, body, result, options = {}) {
const id = body[this.idField.name] ?? result[0]
const resp = await this.getRecord(model, id, options)
return { data: resp.data }
}
/**
* Get record
*
* @param {Object} model
* @param {number|string} id
* @param {Object} [options]
* @returns {Object}
*/
async getRecord (model, id, options = {}) {
const client = this.getClient(model, options)
const result = await client(model.collName).where('id', id)
return { data: result[0] }
}
async updateRecord (model, id, body = {}, options = {}) {
const oldData = options._data
const client = this.getClient(model, options)
const result = await client(model.collName).where('id', id).update(body, this._getReturningFields(model, options))
if (options.noResult) return
if (this.support.returning) return { data: result[0], oldData }
const resp = await this.getRecord(model, id, options)
return { data: resp.data, oldData }
}
async removeRecord (model, id, options = {}) {
const client = this.getClient(model, options)
await client(model.collName).where('id', id).del()
if (options.noResult) return
return { oldData: options._data }
}
async findRecord (model, filter = {}, options = {}) {
const { handleLastPage } = this.app.dobo
const client = this.getClient(model, options)
const { hardCap } = this.app.dobo.getDefaultValues(options)
const { limit, skip, sort, page } = filter
const resp = await model.countRecord(filter, { ...options, noCache: true, dataOnly: false })
let count = options.count ? resp.data : 0
const { query } = filter
const result = handleLastPage({ count: resp.orgCount, limit, page }, options)
if (result) return result
const instance = mongoKnex(client(model.collName), query)
if (options.noLimit && options.count) {
instance.limit(hardCap, { skipBinding: true }).offset(skip)
} else instance.limit(limit, { skipBinding: true }).offset(skip)
if (sort) {
const sorts = []
forOwn(sort, (v, k) => {
sorts.push({ column: k, order: v < 0 ? 'desc' : 'asc' })
})
instance.orderBy(sorts)
}
const data = await instance
if (options.noLimit && options.count && count > hardCap) count = hardCap
return { data, count, warnings: resp.warnings, hardCapped: resp.hardCapped }
}
async findAllRecord (model, filter = {}, options = {}) {
options.noLimit = true
return await this.findRecord(model, filter, options)
}
async countRecord (model, filter = {}, options = {}) {
const client = this.getClient(model, options)
const instance = mongoKnex(client(model.collName), filter.query)
const resp = await instance.count('*', { as: 'cnt' })
return { data: resp[0].cnt }
}
async createAggregate (model, filter = {}, params = {}, options = {}) {
const client = this.getClient(model, options)
const { generateId } = this.app.lib.aneka
const { limit, skip, sort, page } = filter
const { query } = filter
const { group, aggregates = [], field } = params
const instance = mongoKnex(client(model.collName), query)
if (!options.noLimit) instance.limit(limit, { skipBinding: true }).offset(skip)
instance.select(group).groupBy(group)
if (sort) {
const f = Object.keys(sort)[0]
let d = sort[f]
d = d <= 0 ? 'desc' : 'asc'
instance.orderBy(group, d)
}
instance.orderBy(group)
for (const a of aggregates) {
instance[a](field, { as: a })
}
const data = ((await instance) ?? []).map(d => {
d[this.idField.name] = generateId()
return d
})
/*
for (const d of data) {
d.id = d[group]
delete d[group]
}
*/
return { data, page, limit, group, field }
}
async createHistogram (model, filter = {}, params, options = {}) {
const client = this.getClient(model, options)
const { generateId } = this.app.lib.aneka
const { limit, skip, sort, page } = filter
const { query } = filter
const { group, type, field, aggregates = [] } = params
const instance = mongoKnex(client(model.collName), query)
if (!options.noLimit) instance.limit(limit, { skipBinding: true }).offset(skip)
if (sort) {
/*
const f = Object.keys(sort)[0]
let d = sort[f]
d = d <= 0 ? 'desc' : 'asc'
instance.orderBy(f, d)
*/
}
const item = instance.toSQL().toNative()
this._reformHistogram({ item, type, group, aggregates, field })
const result = await this.getRawResult(instance, item)
const data = (result ?? []).map(d => {
d[this.idField.name] = generateId()
return d
})
return { data, page, limit, group, field, type, aggregates }
}
_reformHistogram ({ type, item, group, aggregates, field }) {
const aggs = []
for (const agg of aggregates) {
aggs.push(`${agg}(${agg === 'count' ? '*' : field}) as ${agg}`)
}
switch (type) {
case 'daily': {
item.sql = item.sql.replace('*', `date_format(${group}, '%Y-%m-%e') as date, ${aggs.join(', ')}`)
// item.sql = item.sql.replace('limit ', `group by year(${group}), month(${group}), dayofmonth(${group}) limit `)
item.sql = item.sql.replace('limit ', 'group by date limit ')
break
}
case 'monthly': {
item.sql = item.sql.replace('*', `date_format(${group}, '%Y-%m') as month, ${aggs.join(', ')}`)
// item.sql = item.sql.replace('limit ', `group by year(${group}), month(${group}) limit `)
item.sql = item.sql.replace('limit ', 'group by month limit ')
break
}
case 'yearly': {
item.sql = item.sql.replace('*', `year(${group}) as year, ${aggs.join(', ')}`)
// item.sql = item.sql.replace('limit ', `group by year(${group}) limit `)
item.sql = item.sql.replace('limit ', 'group by year limit ')
break
}
}
}
async getRawResult (instance, item) {
item = item ?? instance.toSQL().toNative()
let result = (await instance.client.raw(item.sql, item.bindings)) ?? []
if (isArray(result[0])) result = result[0]
return result
}
async transaction (model, handler, ...args) {
const client = model.connection.client
let result
await client.transaction(async trx => {
result = await handler.call(model, trx, ...args)
})
return result
}
}
this.app.baseClass.DoboKnexDriver = DoboKnexDriver
return DoboKnexDriver
}
export default knexFactory