@coko/server
Version:
Reusable server for use by Coko's projects
377 lines (306 loc) • 8.73 kB
JavaScript
const { Model, AjvValidator } = require('objection')
const config = require('config')
const merge = require('lodash/merge')
const uuid = require('uuid')
const addFormats = require('ajv-formats')
const { db } = require('../db')
const logger = require('../logger')
const useTransaction = require('./useTransaction')
const { dateNotNullable } = require('./_helpers/types')
Model.knex(db)
class BaseModel extends Model {
static createValidator() {
return new AjvValidator({
onCreateAjv: ajv => {
addFormats(ajv)
},
})
}
static get jsonSchema() {
let schema
const mergeSchema = additionalSchema => {
if (additionalSchema) {
schema = merge(schema, additionalSchema)
}
}
// Crawls up the prototype chain to collect schema
// information from models and extended models
const getSchemasRecursively = object => {
mergeSchema(object.schema)
if (config.has('schema')) {
mergeSchema(config.schema[object.name])
}
const proto = Object.getPrototypeOf(object)
if (proto.name !== 'BaseModel') {
getSchemasRecursively(proto)
}
}
getSchemasRecursively(this)
const baseSchema = {
type: 'object',
properties: {
type: { type: 'string' },
id: { type: 'string', format: 'uuid' },
created: dateNotNullable,
updated: dateNotNullable,
},
additionalProperties: false,
}
if (schema) {
return merge(baseSchema, schema)
}
return baseSchema
}
$beforeInsert() {
this.id = this.id || uuid.v4()
this.created = new Date().toISOString()
this.updated = this.created
}
$beforeUpdate() {
this.updated = new Date().toISOString()
}
static async find(data, options = {}) {
try {
const { trx, related, orderBy, page, pageSize } = options
return useTransaction(
async tr => {
let queryBuilder = this.query(tr)
if (orderBy) {
queryBuilder = queryBuilder.orderBy(orderBy)
}
if (
(Number.isInteger(page) && !Number.isInteger(pageSize)) ||
(!Number.isInteger(page) && Number.isInteger(pageSize))
) {
throw new Error(
'both page and pageSize integers needed for paginated results',
)
}
if (Number.isInteger(page) && Number.isInteger(pageSize)) {
if (page < 0) {
throw new Error(
'invalid index for page (page should be an integer and greater than or equal to 0)',
)
}
if (pageSize <= 0) {
throw new Error(
'invalid size for pageSize (pageSize should be an integer and greater than 0)',
)
}
queryBuilder = queryBuilder.page(page, pageSize)
}
if (related) {
queryBuilder = queryBuilder.withGraphFetched(related)
}
const result = await queryBuilder.where(data)
const { results, total } = result
return {
result: page !== undefined ? results : result,
totalCount: total || result.length,
}
},
{
trx,
passedTrxOnly: true,
},
)
} catch (e) {
logger.error('Base model: find failed', e)
throw new Error(e)
}
}
static async findByIds(ids, options = {}) {
try {
const { trx, related } = options
return useTransaction(
async tr => {
let queryBuilder = this.query(tr)
if (related) {
queryBuilder = queryBuilder.withGraphFetched(related)
}
const result = await queryBuilder.findByIds(ids)
if (result.length < ids.length) {
const delta = ids.filter(
id => !result.map(res => res.id).includes(id),
)
throw new Error(`id ${delta} not found`)
}
return result
},
{
trx,
passedTrxOnly: true,
},
)
} catch (e) {
logger.error('Base model: findByIds failed', e)
throw new Error(e)
}
}
static async findById(id, options = {}) {
try {
const { trx, related } = options
return useTransaction(
async tr => {
let queryBuilder = this.query(tr)
if (related) {
queryBuilder = queryBuilder.withGraphFetched(related)
}
return queryBuilder.findById(id).throwIfNotFound()
},
{
trx,
passedTrxOnly: true,
},
)
} catch (e) {
logger.error('Base model: findById failed', e)
throw new Error(e)
}
}
static async findOne(data, options = {}) {
try {
const { trx, related } = options
return useTransaction(
async tr => {
let queryBuilder = this.query(tr)
if (related) {
queryBuilder = queryBuilder.withGraphFetched(related)
}
return queryBuilder.findOne(data)
},
{
trx,
passedTrxOnly: true,
},
)
} catch (e) {
logger.error('Base model: findOne failed', e)
throw new Error(e)
}
}
static async insert(data, options = {}) {
try {
const { trx, related } = options
return useTransaction(
async tr => {
let queryBuilder = this.query(tr)
if (related) {
queryBuilder = queryBuilder.withGraphFetched(related)
}
return queryBuilder.insert(data)
},
{
trx,
passedTrxOnly: true,
},
)
} catch (e) {
logger.error('Base model: insert failed', e)
throw new Error(e)
}
}
// INSTANCE METHOD
async patch(data, options = {}) {
try {
const { trx } = options
if (!data) {
throw new Error('Patch is empty')
}
return useTransaction(async tr => this.$query(tr).patch(data), {
trx,
passedTrxOnly: true,
})
} catch (e) {
logger.error('Base model: patch failed', e)
throw new Error(e)
}
}
static async patchAndFetchById(id, data, options = {}) {
try {
const { trx, related } = options
return useTransaction(
async tr => {
let queryBuilder = this.query(tr)
if (related) {
queryBuilder = queryBuilder.withGraphFetched(related)
}
return queryBuilder.patchAndFetchById(id, data).throwIfNotFound()
},
{
trx,
passedTrxOnly: true,
},
)
} catch (e) {
logger.error('Base model: patchAndFetchById failed', e)
throw new Error(e)
}
}
// INSTANCE METHOD
async update(data, options = {}) {
try {
const { trx } = options
if (!data) {
throw new Error('Patch is empty')
}
return useTransaction(async tr => this.$query(tr).update(data), {
trx,
passedTrxOnly: true,
})
} catch (e) {
logger.error('Base model: update failed', e)
throw new Error(e)
}
}
static async updateAndFetchById(id, data, options = {}) {
try {
const { trx, related } = options
return useTransaction(
async tr => {
let queryBuilder = this.query(tr)
if (related) {
queryBuilder = queryBuilder.withGraphFetched(related)
}
return queryBuilder.updateAndFetchById(id, data).throwIfNotFound()
},
{
trx,
passedTrxOnly: true,
},
)
} catch (e) {
logger.error('Base model: updateAndFetchById failed', e)
throw new Error(e)
}
}
static async deleteById(id, options = {}) {
try {
return this.query(options.trx).deleteById(id).throwIfNotFound()
} catch (e) {
logger.error(`${this.name} model: deleteById failed.`, e)
throw e
}
}
static async deleteByIds(ids, options = {}) {
try {
const rows = await this.query(options.trx).findByIds(ids)
if (rows.length < ids.length) {
const diff = ids.filter(id => !rows.map(res => res.id).includes(id))
throw new Error(
`id${diff.length > 1 ? 's' : ''} ${diff.join(', ')} not found`,
)
}
const result = await this.query(options.trx)
.delete()
.whereIn('id', ids)
.returning('id')
return result.length
} catch (e) {
logger.error(`${this.name} model: deleteByIds failed`, e)
throw new Error(e)
}
}
}
BaseModel.pickJsonSchemaProperties = false
module.exports = BaseModel