sql-dao
Version:
database access objects for sql databases
688 lines (648 loc) • 22.9 kB
JavaScript
const Model = require('./Model')
const WhereClause = require('./WhereClause')
const Join = require('./Join')
const RelationBelongsTo = require('./relation/RelationBelongsTo')
const RelationHasOne = require('./relation/RelationHasOne')
const RelationHasMany = require('./relation/RelationHasMany')
const RelationManyMany = require('./relation/RelationManyMany')
/* eslint-disable no-unused-vars */
const DatabaseConnection = require('./DatabaseConnection')
const Relation = require('./relation/Relation')
/* eslint-enable no-unused-vars */
class DatabaseAccessObject extends Model {
/**
* @returns {DatabaseConnection}
* @abstract
*/
static getDatabaseConnection () { }
/**
* @abstract
* @returns {string}
*/
static getTableName () {}
/**
* @abstract
* @returns {string}
*/
static getPrimaryKey () {}
/**
* Overwrite to addRelation's
* @returns {{Relation[]}}
*/
static getRelations () {
return []
}
/**
* @param {*} transaction
*/
async insert (transaction = undefined) {
await this.beforeInsert(transaction)
for (const relation of this.constructor.getRelations()) {
await this._insertRefObjectBefore(relation, transaction)
}
const connection = this.constructor.getDatabaseConnection()
const query = connection.createInsertQuery(this.constructor.getTableName(), this.createAttributeMap())
const result = await connection.sendQuery(query, transaction)
const primaryKey = this.constructor.getPrimaryKey()
if (typeof primaryKey === 'string') {
this[primaryKey] = connection.parsePrimaryKeyFromResult(result)
}
for (const relation of this.constructor.getRelations()) {
await this._insertRefObjectAfter(relation, transaction)
}
await this.afterInsert(transaction)
}
/**
* @param {Relation} relation
* @param {*} transaction
*/
async _insertRefObjectBefore (relation, transaction = undefined) {
switch (true) {
case relation instanceof RelationHasMany:
// must inserted after, this is inserted
return
case relation instanceof RelationHasOne:
case relation instanceof RelationBelongsTo:
this[relation.thisKey] = await this._insertRefObj(this[relation.thisAttribute], relation.refKey, transaction)
return
case relation instanceof RelationManyMany:
return this._insertRefObjList(this[relation.thisAttribute], relation.refKey, transaction)
default:
throw new Error('unsupported relation type')
}
}
/**
* @param {Relation} relation
* @param {*} transaction
*/
async _insertRefObjectAfter (relation, transaction = undefined) {
switch (true) {
case relation instanceof RelationManyMany:
return this._insertRelationEntry(relation, transaction)
case relation instanceof RelationHasMany:
for (const refObj of this[relation.thisAttribute]) {
refObj[relation.refKey] = this[relation.thisKey]
await this._insertRefObj(refObj, relation.refKey, transaction)
}
break
case relation instanceof RelationHasOne:
case relation instanceof RelationBelongsTo:
return
default:
throw new Error('unsupported relation type')
}
}
/**
* Inserts referenced object and set key to this
* @param {DatabaseAccessObject} refObj
* @param {string} refKey
* @param {*} transaction
* @returns {Promise<number|string>} new inserted id
*/
async _insertRefObj (refObj, refKey, transaction = undefined) {
if (typeof refObj !== 'object' || refObj === null) {
return
}
if (typeof refObj[refObj.constructor.getPrimaryKey()] === 'undefined') {
await refObj.insert(transaction)
} else {
await refObj.update(transaction)
}
if (typeof refObj[refObj.constructor.getPrimaryKey()] === 'undefined') {
throw new Error('cant insert new DAO')
}
return refObj[refKey]
}
/**
* @param {DatabaseAccessObject[]} refObjList
* @param {string} refKey
* @param {*} transaction
*/
async _insertRefObjList (refObjList, refKey, transaction = undefined) {
if (!Array.isArray(refObjList) || refObjList.length === 0) {
return
}
for (const refObj of refObjList) {
await this._insertRefObj(refObj, refKey, transaction)
}
}
/**
* Creates entry in relationship table
* @param {RelationManyMany} relation
* @param {*} transaction
*/
async _insertRelationEntry (relation, transaction = undefined) {
if (!Array.isArray(this[relation.thisAttribute]) || this[relation.thisAttribute].length === 0) {
return
}
const connection = this.constructor.getDatabaseConnection()
for (const refModel of this[relation.thisAttribute]) {
const attributeMap = new Map()
attributeMap.set(relation.relKeyThis, this[relation.thisKey])
attributeMap.set(relation.relKeyRef, refModel[relation.refKey])
const query = connection.createInsertQuery(relation.relTableName, attributeMap)
await connection.sendQuery(query, transaction)
}
}
/**
* find models with equal attributes like this model
* @param {*} transaction
* @returns {DatabaseAccessObject}
*/
async search (transaction = undefined) {
const attributes = this.constructor.getAttributeNames()
const clauseParts = []
const values = []
for (const attr of attributes) {
if (typeof this[attr] !== 'undefined' && this[attr] !== null) {
clauseParts.push('?? = ?')
values.push(attr)
values.push(this[attr])
}
}
const whereClause = new WhereClause(clauseParts.join(' AND '), values)
return this.constructor.find(whereClause, [], transaction)
}
/**
* @param {WhereClause} whereClause
* @param {Join} joins
* @param {*} transaction
* @returns {DatabaseAccessObject[]}
*/
static async find (whereClause = undefined, joins = [], transaction = undefined) {
const connection = this.getDatabaseConnection()
const query = connection.createFindQuery(this.getTableName(), whereClause, joins)
const result = await connection.sendQuery(query, transaction)
const attributeMaps = connection.parseAttributeMapsFromResult(result, this.getAttributeNames())
const models = []
for (const attributeMap of attributeMaps) {
const model = new this()
model.setAttributes(attributeMap)
for (const relation of this.getRelations()) {
await this._findRefObjAfter(model, relation)
}
models.push(model)
}
return models
}
/**
* @param {DatabaseAccessObject} model
* @param {Relation} relation
*/
static async _findRefObjAfter (model, relation) {
switch (true) {
case relation instanceof RelationHasOne:
case relation instanceof RelationBelongsTo:
const refObjList = await this._findRefObjList(model, relation) // eslint-disable-line no-case-declarations
if (refObjList.length > 0) {
model[relation.thisAttribute] = refObjList[0]
}
break
case relation instanceof RelationHasMany:
model[relation.thisAttribute] = await this._findRefObjList(model, relation)
break
case relation instanceof RelationManyMany:
model[relation.thisAttribute] = await this._findRefObjListManyMany(model, relation)
break
default:
throw new Error('unsupported relation type')
}
}
/**
* @param {DatabaseAccessObject} model
* @param {Relation} relation
* @param {boolean} hasOne
*/
static async _findRefObjList (model, relation) {
if (typeof model[relation.thisKey] === 'undefined' || model[relation.thisKey] === null) {
return []
}
const whereClause = new WhereClause(
'?? = ?',
[
relation.refKey,
model[relation.thisKey]
]
)
return relation.refClass.find(whereClause)
}
/**
* @param {DatabaseAccessObject} model
* @param {RelationManyMany} relation
*/
static async _findRefObjListManyMany (model, relation) {
const joins = []
joins.push(new Join(
relation.relTableName,
'??.?? = ??.??',
[
relation.relTableName,
relation.relKeyRef,
relation.refClass.getTableName(),
relation.refKey
]
))
joins.push(new Join(
this.getTableName(),
'??.?? = ??.??',
[
this.getTableName(),
relation.thisKey,
relation.relTableName,
relation.relKeyThis
]
))
const whereClause = new WhereClause(
'??.?? = ?',
[
this.getTableName(),
relation.refKey,
model[relation.thisKey]
]
)
return relation.refClass.find(whereClause, joins)
}
/**
* @param {*} transaction
* @returns {number}
*/
async update (transaction = undefined) {
await this.beforeUpdate(transaction)
for (const relation of this.constructor.getRelations()) {
await this._updateRefObjectBefore(relation, transaction)
}
const connection = this.constructor.getDatabaseConnection()
const primaryKey = this.constructor.getPrimaryKey()
let whereClause
if (typeof primaryKey === 'string' &&
typeof this[primaryKey] !== 'number' &&
typeof this[primaryKey] !== 'string') {
const attributeMap = this.createAttributeMap()
const values = []
for (const [attr, value] of attributeMap) {
values.push(attr)
values.push(value)
}
whereClause = new WhereClause(Array(attributeMap.size).fill('?? = ?').join(' AND '), values)
} else {
whereClause = new WhereClause('?? = ?', [primaryKey, this[primaryKey]])
}
const query = connection.createUpdateQuery(this.constructor.getTableName(), this.createAttributeMap(), whereClause)
const result = await connection.sendQuery(query, transaction)
const rowsAffected = connection.parseUpdatedRowsFromResult(result)
await this.afterUpdate(transaction)
return rowsAffected
}
/**
* @param {Relation} relation
* @param {*} transaction
*/
async _updateRefObjectBefore (relation, transaction = undefined) {
switch (true) {
case relation instanceof RelationHasMany:
await this._deleteMissingRefObjHasMany(relation, transaction)
for (const refObj of this[relation.thisAttribute]) {
if (typeof refObj[refObj.constructor.getPrimaryKey()] !== 'undefined') {
await refObj.update(transaction)
} else {
await refObj.insert(transaction)
}
}
break
case relation instanceof RelationManyMany:
await this._deleteMissingRelationEntries(relation, transaction)
for (const refObj of this[relation.thisAttribute]) {
if (typeof refObj[refObj.constructor.getPrimaryKey()] !== 'undefined') {
await refObj.update(transaction)
} else {
await refObj.insert(transaction)
}
}
break
case relation instanceof RelationHasOne:
case relation instanceof RelationBelongsTo:
const refObj = this[relation.thisAttribute] // eslint-disable-line no-case-declarations
if (typeof refObj[refObj.constructor.getPrimaryKey()] !== 'undefined') {
await refObj.update(transaction)
} else {
await refObj.insert(transaction)
}
break
default:
throw new Error('unsupported relation type')
}
}
/**
* @param {*} transaction
* @returns {number}
*/
async delete (transaction = undefined) {
await this.beforeDelete(transaction)
let rowsAffected = 0
for (const relation of this.constructor.getRelations()) {
rowsAffected += await this._deleteRefObjectBefore(relation, transaction)
}
const primaryKey = this.constructor.getPrimaryKey()
let whereClause
if (typeof primaryKey === 'string' &&
typeof this[primaryKey] !== 'number' &&
typeof this[primaryKey] !== 'string') {
const attributeMap = this.createAttributeMap()
const values = []
for (const [attr, value] of attributeMap) {
values.push(attr)
values.push(value)
}
whereClause = new WhereClause(Array(attributeMap.size).fill('?? = ?').join(' AND '), values)
} else {
whereClause = new WhereClause('?? = ?', [primaryKey, this[primaryKey]])
}
const connection = this.constructor.getDatabaseConnection()
const query = connection.createDeleteQuery(this.constructor.getTableName(), whereClause)
const result = await connection.sendQuery(query, transaction)
rowsAffected += connection.parseDeletedRowsFromResult(result)
this[primaryKey] = undefined
this.afterDelete(transaction)
return rowsAffected
}
/**
* @param {Relation} relation
* @param {*} transaction
* @returns {Promise<number>}
*/
async _deleteRefObjectBefore (relation, transaction = undefined) {
let rowsAffected = 0
switch (true) {
case relation instanceof RelationHasOne:
case relation instanceof RelationBelongsTo:
// do not delete, may used elsewhere
this[relation.thisKey] = undefined
break
case relation instanceof RelationHasMany:
if (Array.isArray(this[relation.thisAttribute])) {
for (const refObj of this[relation.thisAttribute]) {
rowsAffected += await refObj.delete(transaction)
}
}
break
case relation instanceof RelationManyMany:
// only delete relation entries, referenced may used elsewhere
rowsAffected = await this._deleteRelationEntry(relation, transaction)
break
default:
throw new Error('unsupported relation type')
}
this[relation.thisAttribute] = undefined
return rowsAffected
}
/**
* @param {RelationManyMany} relation
* @param {*} transaction
*/
async _deleteRelationEntry (relation, transaction = undefined) {
if (!Array.isArray(this[relation.thisAttribute]) || this[relation.thisAttribute].length === 0) {
return
}
const connection = this.constructor.getDatabaseConnection()
const whereClause = new WhereClause('?? = ?', [relation.relKeyThis, this[relation.thisKey]])
const query = connection.createDeleteQuery(relation.relTableName, whereClause)
const result = await connection.sendQuery(query, transaction)
return connection.parseDeletedRowsFromResult(result)
}
/**
* @param {RelationManyMany} relation
* @param {*} transaction
*/
async _deleteMissingRelationEntries (relation, transaction = undefined) {
if (!Array.isArray(this[relation.thisAttribute]) || this[relation.thisAttribute].length === 0) {
return
}
const execptedIds = this[relation.thisAttribute].map((i) => i[relation.refKey])
const connection = this.constructor.getDatabaseConnection()
let whereSql = '?? = ?'
let whereValues = [relation.relKeyThis, this[relation.thisKey]]
if (execptedIds.length > 0) {
whereSql += ` AND ?? NOT IN (${new Array(execptedIds.length).fill('?').join(', ')})`
whereValues.push(relation.relKeyRef)
whereValues = whereValues.concat(execptedIds)
}
const whereClause = new WhereClause(whereSql, whereValues)
const query = connection.createDeleteQuery(relation.relTableName, whereClause)
const result = await connection.sendQuery(query, transaction)
return connection.parseDeletedRowsFromResult(result)
}
/**
* @param {RelationHasMany} relation
* @param {*} transaction
*/
async _deleteMissingRefObjHasMany (relation, transaction) {
if (!Array.isArray(this[relation.thisAttribute]) || this[relation.thisAttribute].length === 0) {
return
}
const primaryKey = relation.refClass.getPrimaryKey()
const execptedIds = this[relation.thisAttribute].map((i) => i[primaryKey])
let whereSql = '?? = ?'
let whereValues = [relation.refKey, this[relation.thisKey]]
if (execptedIds.length > 0) {
whereSql += ` AND ?? NOT IN (${new Array(execptedIds.length).fill('?').join(', ')})`
whereValues.push(primaryKey)
whereValues = whereValues.concat(execptedIds)
}
const connection = this.constructor.getDatabaseConnection()
const whereClause = new WhereClause(whereSql, whereValues)
const query = connection.createDeleteQuery(relation.refClass.getTableName(), whereClause)
const result = await connection.sendQuery(query, transaction)
return connection.parseDeletedRowsFromResult(result)
}
/**
* Insert on duplicate update
* @param {*} transaction
*/
async save (transaction = undefined) {
await this.beforeSave(transaction)
for (const relation of this.constructor.getRelations()) {
await this._saveRefObjectBefore(relation, transaction)
}
const connection = this.constructor.getDatabaseConnection()
const query = connection.createSaveQuery(this.constructor.getTableName(), this.createAttributeMap())
const result = await connection.sendQuery(query, transaction)
const primaryKey = this.constructor.getPrimaryKey()
if (typeof primaryKey === 'string' &&
typeof this[primaryKey] !== 'number' &&
typeof this[primaryKey] !== 'string') {
try {
this[primaryKey] = connection.parsePrimaryKeyFromResult(result)
} catch (e) {
// should happen on duplicate update
const findings = await this.search(transaction)
if (findings.length === 0) {
throw new Error('cant parse inserted id')
}
this[primaryKey] = findings[0][primaryKey]
}
}
for (const relation of this.constructor.getRelations()) {
await this._saveRefObjectAfter(relation, transaction)
}
await this.afterSave(transaction)
}
/**
* @param {Relation} relation
* @param {*} transaction
*/
async _saveRefObjectBefore (relation, transaction = undefined) {
switch (true) {
case relation instanceof RelationHasMany:
this._deleteMissingRefObjHasMany(relation, transaction)
// must saved after, this is saved
return
case relation instanceof RelationHasOne:
case relation instanceof RelationBelongsTo:
if (typeof this[relation.thisAttribute] !== 'undefined') {
this[relation.thisKey] = await this._saveRefObj(this[relation.thisAttribute], relation.refKey, transaction)
}
return
case relation instanceof RelationManyMany:
await this._deleteMissingRelationEntries(relation, transaction)
return this._saveRefObjList(this[relation.thisAttribute], relation.refKey, transaction)
default:
throw new Error('unsupported relation type')
}
}
/**
* @param {Relation} relation
* @param {*} transaction
*/
async _saveRefObjectAfter (relation, transaction = undefined) {
switch (true) {
case relation instanceof RelationManyMany:
return this._saveRelationEntry(relation, transaction)
case relation instanceof RelationHasMany:
if (Array.isArray(this[relation.thisAttribute])) {
for (const refObj of this[relation.thisAttribute]) {
refObj[relation.refKey] = this[relation.thisKey]
await this._saveRefObj(refObj, relation.refKey, transaction)
}
}
break
case relation instanceof RelationHasOne:
case relation instanceof RelationBelongsTo:
return
default:
throw new Error('unsupported relation type')
}
}
/**
* @param {DatabaseAccessObject} refObj
* @param {string} refKey
* @param {*} transaction
* @returns {Promise<number|string>} new inserted id, or existing
*/
async _saveRefObj (refObj, refKey, transaction = undefined) {
if (typeof refObj !== 'object' || refObj === null) {
return
}
await refObj.save(transaction)
if (typeof refObj[refObj.constructor.getPrimaryKey()] === 'undefined') {
throw new Error('cant insert new DAO')
}
return refObj[refKey]
}
/**
* @param {DatabaseAccessObject[]} refObjList
* @param {string} refKey
* @param {*} transaction
*/
async _saveRefObjList (refObjList, refKey, transaction = undefined) {
if (!Array.isArray(refObjList) || refObjList.length === 0) {
return
}
for (const refObj of refObjList) {
await this._saveRefObj(refObj, refKey, transaction)
}
}
/**
* Creates entry in relationship table
* @param {RelationManyMany} relation
* @param {*} transaction
*/
async _saveRelationEntry (relation, transaction = undefined) {
if (!Array.isArray(this[relation.thisAttribute]) || this[relation.thisAttribute].length === 0) {
return
}
const connection = this.constructor.getDatabaseConnection()
for (const refModel of this[relation.thisAttribute]) {
const attributeMap = new Map()
attributeMap.set(relation.relKeyThis, this[relation.thisKey])
attributeMap.set(relation.relKeyRef, refModel[relation.refKey])
const query = connection.createSaveQuery(relation.relTableName, attributeMap)
await connection.sendQuery(query, transaction)
}
}
/**
* Creates object with only attributes
* (removes all DAO specific properties)
* @param {number} maxDepth for recursion
*/
toPlainObject (maxDepth = 1000) {
const obj = {}
const attributes = this.constructor.getAttributeNames()
this.constructor.getRelations().forEach(rel => attributes.push(rel.thisAttribute))
for (const attr of attributes) {
obj[attr] = this._plainValue(this[attr], maxDepth)
}
return obj
}
/**
* recursive function!
* @param {*} val
* @param {number} maxDepth
* @param {number} depth
* @returns {*}
*/
_plainValue (val, maxDepth, depth = 1) {
if (depth === maxDepth) {
throw new Error('max depth of recursion reached')
}
if (val instanceof DatabaseAccessObject) {
return val.toPlainObject()
} else if (Array.isArray(val)) {
return val.map(v => this._plainValue(v, depth + 1, maxDepth))
} else {
return val
}
}
/**
* @param {*} transaction
*/
async beforeInsert (transaction = undefined) {}
/**
* @param {*} transaction
*/
async afterInsert (transaction = undefined) {}
/**
* @param {*} transaction
*/
async beforeUpdate (transaction = undefined) {}
/**
* @param {*} transaction
*/
async afterUpdate (transaction = undefined) {}
/**
* @param {*} transaction
*/
async beforeDelete (transaction = undefined) {}
/**
* @param {*} transaction
*/
async afterDelete (transaction = undefined) {}
/**
* @param {*} transaction
*/
async beforeSave (transaction = undefined) {}
/**
* @param {*} transaction
*/
async afterSave (transaction = undefined) {}
}
module.exports = DatabaseAccessObject