api/BaseModel.js

import first from "lodash/first"
import keys from "lodash/keys"
import getValues from "lodash/values"
import get from "lodash/get"

import Database from "../configure/Database"

/**
 * Initialize your model with the table name
 *
 * @class BaseModel
 * @param {string} name | table name that will be used in all your queries
 * @extends {Database}
 */
class BaseModel extends Database {
    constructor(name, propTypes) {
        super()

        this.name = name
        this.propTypes = propTypes
        /**
         * Holds the error related to the current instance
         * @type string | object
         * @memberof BaseModel
         */
        this.error = null
        this.isValid = false
        /**
         * Holds whether or not the instance has been initialized and validated
         * @type boolean
         * @memberof BaseModel
         */
        this.initialized = false
        /**
         * Holds the properties of the current instance
         * @type object
         * @memberof BaseModel
         */
        this.props = {}
        this.select = this.select.bind(this)
        this.create = this.create.bind(this)
    }

    /**
     * Validates the properties values
     * @param {object} props | Props used to initialize the instance
     * @returns {string} | returns the error while validating. It will return null if no error
     */
    validate(props) {
        if (!get(keys(props), "length", 0)) {
            this.error = "props_must_have_at_least_one_attr"
            this.isValid = false

            return Promise.reject(this.error)
        }

        const columns = keys(this.propTypes)

        for (let idx = 0; idx <= columns.length - 1; idx += 1) {
            const propName = columns[idx]
            const propValue = get(props, propName)
            const propType = get(this.propTypes, propName)
            const isRequired = propType.required

            if (isRequired) {
                if (!get(props, propName)) {
                    this.error = `${propName}_is_required`
                    break
                }

                const isValid = propType.isValid(propValue)

                if (!isValid) {
                    this.error = `${propName}_is_not_${propType.type.toLowerCase()}`
                    break
                }
            }
        }

        this.isValid = !this.error
        return this.error
    }

    /**
     * Select entries from the table you initialized your class with
     * @param {object} params | The paramerters of your search in the current entity table
     * @returns {promise}
     */
    select(params) {
        const columns = keys(params)

        if (!params || !columns.length) {
            return this.query(`
                SELECT *
                FROM "${this.name}";
            `)
        }

        const values = getValues(params)
        let query = `"${first(columns)}"=$1`

        for (let idx = 1; idx < columns.length; idx += 1) {
            query = `${query} AND "${columns[idx]}" = $${idx + 1}`
        }

        return this.query(
            `
                SELECT *
                FROM "${this.name}"
                WHERE (${query});
            `,
            values,
        )
    }

    /**
     * Insert entries in the database table
     * @param {object} props | The proprities your entity is composed from
     * @returns {promise}
     */
    insert(props) {
        if (!this.isValid) {
            if (!this.error) {
                throw new Error(`ERROR: ${this.name} wasn't validated before inserting. Run ${this.name}.validate(props) and then try again.`)
            } else {
                return Promise.reject(this.error)
            }
        }

        const columns = keys(props)
        const values = getValues(props)
        let query = `("${first(columns)}"`
        let valuesPlaceholders = "$1"

        for (let idx = 1; idx < columns.length; idx += 1) {
            query = `${query}, "${columns[idx]}"`
            valuesPlaceholders = `${valuesPlaceholders}, $${idx + 1}`
        }

        query = `${query})`

        return this.query(
            `
            INSERT INTO "${this.name}" ${query}
            VALUES (${valuesPlaceholders})
            RETURNING *;
        `,
            values,
        )
    }

    /**
     * Update entries in the table you initialized your class with
     * @param {object} props | The proprities your entity is composed from
     * @returns {promise}
     */
    updateById(id, props) {
        if (!this.isValid) {
            if (!this.error) {
                throw new Error(`ERROR: ${this.name} wasn't validated before inserting. Run ${this.name}.validate(props) and then try again.`)
            } else {
                return Promise.reject(this.error)
            }
        }

        if (!id) throw new Error("ERROR: {id} is required to perform UPDATE. Maybe you forgot to add your id?")

        const columns = keys(props)
        const values = getValues(props)
        let query = `"${first(columns)}" = $1`

        for (let idx = 1; idx < columns.length; idx += 1) {
            query = `${query}, "${columns[idx]}" = $${idx + 1}`
        }

        query = `${query}`

        return this.query(
            `
            UPDATE "${this.name}"
            SET ${query}
            WHERE id = ${id}
            RETURNING *;
        `,
            values,
        )
    }

    /**
     * Deletes entries in the table you initialized your class with
     * @param {int} id | The identifier of the instance your want to delete
     */
    deleteById(id) {
        if (!this.isValid) {
            if (!this.error) {
                throw new Error(`ERROR: ${this.name} wasn't validated before inserting. Run ${this.name}.validate(props) and then try again.`)
            } else {
                return Promise.reject(this.error)
            }
        }

        if (!id) throw new Error("ERROR: {id} is required to perform DELETE. Maybe you forgot to add your id?")

        return this.query(`
            DELETE
            FROM "${this.name}"
            WHERE id = ${id}
            RETURNING *;
        `)
    }
}

export default BaseModel