src/adapters/postgres.ts
import Adapter, {
DatabaseConnectionError,
databaseConfig,
optsMultiple,
optsSingle,
where,
props,
pojo
} from '../classes/adapter'
import * as Knex from 'knex'
import { underscore } from 'inflection'
import Model from '../classes/model'
const first = (arr: any[]): any => arr[0] || null
function sanitize(
Ctor: typeof Model,
props: { [key: string]: any }
): { [key: string]: any } {
const validPropKeys: string[] = Object.keys(props).filter(prop =>
Reflect.ownKeys(Ctor.meta.types).includes(prop)
)
const validProps: { [key: string]: any } = {}
for (let prop of validPropKeys) {
validProps[underscore(prop)] = props[prop]
}
return validProps
}
function clone(obj: pojo): pojo {
return JSON.parse(JSON.stringify(obj))
}
export default class PostgresAdapter extends Adapter {
knex: Knex
constructor(config: databaseConfig) {
super(config)
this.knex = Knex({
client: 'pg',
connection: config
})
}
async checkConnection() {
try {
await this.knex.raw('SELECT 1 = 1;')
} catch (e) {
const conf = clone(this.config)
if (conf.password) {
conf.password = '********'
}
throw new DatabaseConnectionError(
`Unable to connect to database '${this.config
.database}' using adapter '${this.constructor
.name}' and config '${JSON.stringify(conf)}'`
)
}
}
paginate(
query: Knex.QueryBuilder,
options: { limit?: number; page?: number }
) {
const page = options.page || 1
const limit = options.limit || 20
return query.limit(limit).offset(page * limit - limit)
}
sort(query: Knex.QueryBuilder, sortString = '') {
if (sortString) {
sortString.split(',').forEach(column => {
const direction = column.includes('-') ? 'DESC' : 'ASC'
column = column.replace('-', '')
query = query.orderBy(column.trim(), direction)
})
}
return query
}
fieldsForModel(Ctor: typeof Model) {
return Object.keys(Ctor.meta.types)
}
databaseFieldsForModel(Ctor: typeof Model) {
return this.fieldsForModel(Ctor).map(field => underscore(field))
}
columnsForModel(Ctor: typeof Model) {
const modelAttrs = this.fieldsForModel(Ctor)
const databaseAttrs = this.databaseFieldsForModel(Ctor)
return databaseAttrs.map((dbAttr, i) => `${dbAttr} as ${modelAttrs[i]}`)
}
limitFieldSet(Ctor: typeof Model, fieldNames: string[] = []) {
// always include id field
if (!fieldNames.includes(Ctor.idField)) {
fieldNames.push(Ctor.idField)
}
// create a field map from model attr name to db column name eg. {myTitle => my_title, etc}
const modelFields = this.fieldsForModel(Ctor)
const dbFields = this.databaseFieldsForModel(Ctor)
const fieldMap = modelFields.map((modelField, i) => ({
nameForModel: modelField,
nameForDb: dbFields[i]
}))
// filter out using fieldNames and return array of strings ['my_title as myTitle', etc]
return fieldMap
.filter(
fieldMapping => fieldNames.indexOf(fieldMapping.nameForModel) !== -1
)
.map(
fieldMapping =>
`${fieldMapping.nameForDb} as ${fieldMapping.nameForModel}`
)
}
// include (Ctor: typeof Model, query: Knex.QueryBuilder, includeString:string = '', fieldOptions) {
// return query.then(data => {
// data = JSON.parse(JSON.stringify(data))
// let single = false
// if (!Array.isArray(data)) {
// single = true
// data = [data]
// }
// return Promise.all(includeString.split(',').map(include => {
// include = include.trim()
// if (include.includes('.')) return
// const definition = get(Model, `definition.relationships.${include}`)
// if (!definition) return
// const RelatedModel = this.store.modelFor(definition.modelTo)
// if (!RelatedModel) return
// let query = this.knex(RelatedModel.tableName)
// let types = this.columnsForModel(Model)
// if (fieldOptions) types = this.limitFieldSet(RelatedModel, types, fieldOptions)
// query = query.column(types)
// let columns = []
// if (fieldOptions && fieldOptions[RelatedModel.type]) {
// columns = this.limitFieldSet(RelatedModel, fieldOptions[RelatedModel.type])
// } else {
// columns = this.columnsForModel(RelatedModel)
// }
// query = query.column(columns)
// let ids = data.map(item => item[definition.keyFrom])
// query = query.whereIn(definition.keyTo, ids)
// return query.then(relatedData => {
// const relatedDataGroups = new Map()
// relatedData.forEach(relData => {
// if (relatedDataGroups.has(relData[definition.keyTo])) {
// relatedDataGroups.get(relData[definition.keyTo]).push(relData)
// } else {
// relatedDataGroups.set(relData[definition.keyTo], [relData])
// }
// })
// return {name: include, definition, groupedData: relatedDataGroups}
// })
// }))
// .then(relationshipData => {
// data.forEach(item => {
// relationshipData.forEach(relationship => {
// const relData = relationship.groupedData.get(item[relationship.definition.keyFrom])
// if (relationship.definition.type === 'belongsTo') {
// item[relationship.name] = relData[0]
// } else {
// item[relationship.name] = relData
// }
// })
// })
// if (single) {
// data = data[0]
// }
// return JSON.parse(JSON.stringify(data))
// })
// })
// }
one(Ctor: typeof Model, where: where, options?: optsSingle): Promise<pojo> {
return Promise.resolve(
this.knex
.table(Ctor.tableName)
.column(this.columnsForModel(Ctor))
.where(where)
.first()
)
}
oneById(Ctor: typeof Model, id: number, options?: optsSingle): Promise<pojo> {
return Promise.resolve(
this.knex
.table(Ctor.tableName)
.column(this.columnsForModel(Ctor))
.where(Ctor.idField, id)
.first()
)
}
async oneBySql(
Ctor: typeof Model,
sql: string,
params?: string[] | number[],
options?: optsSingle
): Promise<pojo> {
const result = await this.knex.raw(sql, params || [])
return first(result.rows)
}
some(
Ctor: typeof Model,
where: where,
options?: optsMultiple
): Promise<pojo[]> {
return Promise.resolve(
this.knex
.table(Ctor.tableName)
.column(this.columnsForModel(Ctor))
.where(where)
)
}
async someBySql(
Ctor: typeof Model,
sql: string,
params?: string[] | number[],
options?: optsMultiple
): Promise<pojo[]> {
const result = await this.knex.raw(sql, params || [])
return result.rows
}
async all(Ctor: typeof Model, options?: optsMultiple): Promise<pojo[]> {
await this.checkConnection()
options = options || {}
let columns = []
if (options.fields) {
columns = this.limitFieldSet(Ctor, options.fields)
} else {
columns = this.columnsForModel(Ctor)
}
let query = this.knex.table(Ctor.tableName)
query = query.column(columns)
query = this.paginate(query, options)
query = this.sort(query, options.sort)
return query
}
async createRecord(Ctor: typeof Model, props: pojo): Promise<pojo> {
const data = sanitize(Ctor, props)
const result = await this.knex(Ctor.tableName)
.insert(data)
.returning(this.databaseFieldsForModel(Ctor))
return clone(result[0])
}
async createSome(Ctor: typeof Model, records: pojo[]): Promise<number> {
const result = await this.knex
.insert(records)
.into(Ctor.tableName)
.returning(Ctor.idField)
return result.length
}
async updateRecord(
Ctor: typeof Model,
key: string | number,
props: { [name: string]: any }
): Promise<pojo> {
const idField: string = Ctor.idField
const result = await this.knex(Ctor.tableName)
.update(sanitize(Ctor, props))
.where(idField, key)
.returning(this.databaseFieldsForModel(Ctor))
return clone(result[0])
}
async deleteRecord(Ctor: typeof Model, key: string | number): Promise<void> {
const idField: string = Ctor.idField
await this.knex(Ctor.tableName).delete().where(idField, key)
}
async deleteAll(Ctor: typeof Model): Promise<number> {
const deleted: number = await this.knex(Ctor.tableName).delete()
return deleted
}
async deleteSome(Ctor: typeof Model, where: where): Promise<number> {
const deleted: number = await this.knex(Ctor.tableName)
.delete()
.where(where)
return deleted
}
async deleteOne(Ctor: typeof Model, where: where): Promise<number> {
const deleted: number = await this.knex(Ctor.tableName)
.delete()
.where(where)
.limit(1)
return deleted
}
async deleteOneById(Ctor: typeof Model, id: number): Promise<number> {
const deleted: number = await this.knex(Ctor.tableName)
.delete()
.where(Ctor.idField, id)
return deleted
}
async updateAll(Ctor: typeof Model, props: props): Promise<number> {
const updated: number = await this.knex(Ctor.tableName).update(props)
return updated
}
async updateSome(
Ctor: typeof Model,
where: where,
props: props
): Promise<number> {
const updated: number = await this.knex(Ctor.tableName)
.update(props)
.where(where)
return updated
}
async updateOneById(
Ctor: typeof Model,
id: number,
props: props
): Promise<number> {
const updated: number = await this.knex(Ctor.tableName)
.update(props)
.where(Ctor.idField, id)
return updated
}
async updateOne(
Ctor: typeof Model,
where: where,
props: props
): Promise<number> {
const updated: number = await this.knex(Ctor.tableName)
.update(props)
.where(where)
.limit(1)
return updated
}
async truncate(Ctor: typeof Model): Promise<void> {
return this.knex(Ctor.tableName).truncate()
}
async countAll(Ctor: typeof Model): Promise<number> {
const result = await this.knex(Ctor.tableName).count()
return Number(result[0].count)
}
async countSome(Ctor: typeof Model, where: where): Promise<number> {
const result = await this.knex(Ctor.tableName).where(where).count()
return Number(result[0].count)
}
destroy(): Promise<void> {
return new Promise(resolve => {
this.knex.destroy().then(() => resolve())
})
}
}