elder-orm/core

View on GitHub
src/adapters/postgres.ts

Summary

Maintainability
B
6 hrs
Test Coverage
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())
    })
  }
}