UNPKG

test-easy-psql

Version:

Welcome to the test-easy-psql documentation! test-easy-psql is a simple intermediary for querying data in PostgreSQL databases. Whether you're a beginner or an experienced developer, this documentation will help you get started with test-easy-psql and lev

1,674 lines (1,656 loc) 81.1 kB
"use strict"; const Column = require("./column"); const { WHERE_CLAUSE_OPERATORS, QUERY_BINDER_KEYS, REQUIRE_CAST_TO_NULL, REQUIRE_WILDCARD_TRANSFORMATION, IS_POSTGRES, SupportedAggregations, START_TRANSACTION, COMMIT, ROLLBACK, SELF_UPDATE_OPERATORS, EVENTS, REQUIRE_ARRAY_TRANSFORMATION, forUpdateMapper, IS_POSTGIS_OPERATOR, POSTGIS_DISTANCE_COMPARISON_OPERATORS, } = require("./constants"); const { Pool, types, Client } = require("pg"); const RawSQL = require("./raw"); const ValidationService = require("./validation"); const SQL = require("./sql"); const pg = require("pg"); types.setTypeParser(types.builtins.INT8, (x) => { return x && DB.isString(x) && x.length > 16 ? x : parseInt(x); }); class DB { static models = {}; static modelFactory = {}; static database = "public"; static enableLog = false; static replicas = []; static allowedOrderDirectionsKeys = { ASC: "asc", DESC: "desc", asc: "asc", desc: "desc", ...(IS_POSTGRES && { asc_nulls_first: "asc nulls first", asc_nulls_last: "asc nulls last", desc_nulls_first: "desc nulls first", desc_nulls_last: "desc nulls last", }), }; static connectionConfig = { host: process.env.DB_HOST, port: process.env.DB_PORT, user: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, // max: 50, // min: 0, idleTimeoutMillis: 0, connectionTimeoutMillis: 0, statement_timeout: 20000, }; static logger = console; static logLevel = "info"; static EventNameSpaces = EVENTS; static pool = new Pool(DB.connectionConfig); static postgis = false; static client = new Client(DB.connectionConfig); static clientConnected = false; static replicationIndex = 0; static brokerEvents = {}; static notificationRegistered = false; static events = { [EVENTS.SELECT]: {}, [EVENTS.INSERT]: {}, [EVENTS.UPDATE]: {}, [EVENTS.DELETE]: {}, [EVENTS.ERROR]: {}, }; static asyncEvents = { [EVENTS.SELECT]: {}, [EVENTS.INSERT]: {}, [EVENTS.UPDATE]: {}, [EVENTS.DELETE]: {}, [EVENTS.ERROR]: {}, }; static actions = { [EVENTS.SELECT]: [], [EVENTS.INSERT]: [], [EVENTS.UPDATE]: [], [EVENTS.DELETE]: [], [EVENTS.ERROR]: [], }; constructor(table, connection = null, schema = "public") { this.table = table; this.relations = {}; this.columns = {}; this.isAggregate = false; this.connection = connection; this.connected = !!connection; this.transaction = false; this.database = DB.database; this.driver = null; this.schema = schema; this.savepointCounter = 0; this.savepointStack = []; } static setLogger(logger) { if (logger) { DB.logger = logger; } } static async connectClient() { if (!DB.clientConnected) { try { DB.clientConnected = true; await DB.client.connect(); } catch (error) { DB.clientConnected = false; } } } static async clientDisconnect() { if (DB.clientConnected) { await DB.client.end(); DB.clientConnected = false; } } static hasReplicas() { return DB.replicas?.length > 0; } static roundRobinReplicaPoolRetrieval() { if (!DB.replicas.length) { throw new Error("0 replicas are present"); } const pool = DB.replicas[DB.replicationIndex]; DB.replicationIndex = (DB.replicationIndex + 1) % DB.replicas.length; return pool; } async connect(primary = true) { if (this.connected) { return; } if (primary) { this.connection = await DB.pool.connect(); this.connected = true; } else { this.connection = await DB.roundRobinReplicaPoolRetrieval().connect(); this.connected = true; } } disconnect() { if (!this.connected) { return; } if (this.transaction) { this.rollback() .then((x) => { this.connection.release(); this.connected = false; this.transaction = false; }) .catch((err) => { this.connection.release(); this.connected = false; this.transaction = false; }); } else { this.connection.release(); this.connected = false; this.transaction = false; } } async startTransaction() { await this.connection.query(START_TRANSACTION); this.transaction = true; } async commit() { if (!this.transaction) { return; } await this.connection.query(COMMIT); this.transaction = false; } async rollback() { if (!this.transaction) { return; } await this.connection.query(ROLLBACK); this.transaction = false; } async createSavepoint() { this.savepointCounter++; const spName = `sp_${this.savepointCounter}`; await this.connection.query(`SAVEPOINT ${spName};`); this.savepointStack.push(spName); return spName; } async releaseSavepoint() { const spName = this.savepointStack.pop(); if (spName) { await this.connection.query(`RELEASE SAVEPOINT ${spName};`); } } async rollbackToSavepoint() { const spName = this.savepointStack.pop(); if (spName) { await this.connection.query(`ROLLBACK TO SAVEPOINT ${spName};`); } } async withSavepoint(cb) { await this.createSavepoint(); try { const result = await cb(this.connection); await this.releaseSavepoint(); return result; } catch (err) { await this.rollbackToSavepoint(); throw err; } } async withTransaction(cb) { try { await this.connect(); await this.startTransaction(); const result = await cb(this.connection); await this.commit(); this.disconnect(); return result; } catch (error) { await this.rollback(); this.disconnect(); return error; } } async withConnection(cb) { try { await this.connect(); const result = await cb(this.connection); this.disconnect(); return result; } catch (error) { this.disconnect(); throw error; } } async setCurrentSetting(name, value, isLocal) { return await this.raw( `select set_config('${name}', '${value}', ${isLocal} ? true : false);` ); } async setCurrentSettingParametrized(name, value, isLocal) { return await this.raw( `select set_config($1,$2, ${isLocal} ? true : false);`, [name, value] ); } async withCurrentSetting(name, value, cb) { return await this.withConnection(async (connection) => { await this.setCurrentSetting(name, value, !!this.transaction); return await cb(connection); }); } async selectQueryExec(sql, args = []) { const { rows: [result], } = await this.raw(sql, args, !!this.connection || !DB.hasReplicas()); return result; } async insertQueryExec(sql, args, returning = true) { const result = this.connection ? await this.connection.query(sql, args) : await DB.pool.query(sql, args); return returning ? result?.rows : [result]; } async updateQueryExec(sql, args, returning = true) { const result = this.connection ? await this.connection.query(sql, args) : await DB.pool.query(sql, args); return returning ? result?.rows : result; } async deleteQueryExec(sql, args, returning = true) { const result = this.connection ? await this.connection.query(sql, args) : await DB.pool.query(sql, args); return returning ? result?.rows : result; } async raw(sql, args = [], primary = true) { if (this.connection) { return await this.connection.query(sql, args); } if (primary || !DB.hasReplicas()) { return await DB.pool.query(sql, args); } else { return await DB.roundRobinReplicaPoolRetrieval().query(sql, args); } } buildSelect({ where = {}, include = {}, aggregate = null, orderBy, groupBy, distinct, select, limit, offset, extras, asText, forUpdate, } = {}) { let depth = 0; let index = 1; const alias = this.makeDepthAlias(this.table, depth); const args = []; const modelColumnsStr = this.getModelColumnsCommaSeperatedString( alias, select, extras ); const selectColumnsStr = [modelColumnsStr] .concat( Object.keys(include).map( (alias, idx) => `${this.makeDepthAlias(alias, 1 + idx)}.${alias}` ) ) .join(","); let sql = `select coalesce(json_agg(${alias}),'[]')${ asText ? "::text" : "" } as ${this.table} from ( select row_to_json(( select ${alias} from ( SELECT ${selectColumnsStr}) ${alias} )) ${alias} from ( select ${this.makeDistinctOn( distinct, alias )} ${modelColumnsStr} from "${this.schema}"."${ this.table }" ${alias}`; const self = this; function makeQuery(model, relations, depth, prevAlias) { if (!relations || typeof relations === "boolean") { return ""; } let sql = ``; const _iter = Object.entries(relations); for (let i = 0; i < _iter.length; i++) { const [_alias, config] = _iter[i]; const alias = DB.getRelationNameWithoutAggregate(_alias); const isAggregate = DB.getIsAggregate(_alias); const relation = model.relations[alias]; if (!relation) { throw new Error(`no such relation: ${alias}`); } const currentModel = DB.getRelatedModel(relation); if (!currentModel) { throw new Error(`no such model for table ${relation.to_table}`); } const depthAlias = isAggregate ? self.makeDepthAlias(relation.alias, depth + i) + "_aggregate" : self.makeDepthAlias(relation.alias, depth + i); const coalesceFallback = relation.type === "object" ? "null" : "[]"; const coalesceAppendex = relation.type === "object" ? "->0" : ""; const modelColumnsStr = currentModel.getModelColumnsCommaSeperatedString( depthAlias, config?.select, config?.extras ); const { distinct, groupBy, orderBy, where, include, limit, offset } = DB.isObject(config) ? config : {}; const selectColumnsStr = [modelColumnsStr] .concat( Object.keys(include || {}).map( (alias, idx) => `${self.makeDepthAlias(alias, depth + 1 + idx)}.${alias}` ) ) .join(","); if (isAggregate) { const [agg] = currentModel.aggregateInternal({ ...config, where: currentModel._mergeRelationalWhere( config.where || {}, relation.where || {} ), alias: depthAlias, relationAlias: relation.alias, }); sql += ` left join lateral (${agg} `; } else { sql += ` left outer join lateral ( select coalesce(json_agg(${depthAlias})${coalesceAppendex},'${coalesceFallback}') as ${ relation.alias } from ( select row_to_json(( select ${depthAlias} from ( SELECT ${selectColumnsStr}) ${depthAlias} )) ${depthAlias} from ( select ${currentModel.makeDistinctOn( distinct, depthAlias )} ${modelColumnsStr} from "${currentModel?.schema}"."${ currentModel.table }" ${depthAlias} `; } const appendSql = makeQuery( currentModel, include, depth + 1, depthAlias ); const [whereClauseStr, qArgs, idx] = currentModel.makeWhereClause( currentModel, currentModel._mergeRelationalWhere(where || {}, relation.where || {}), index, depthAlias, false, false ); args.push(...qArgs); index = idx; const groupByStr = currentModel.makeGroupBy(groupBy, depthAlias); const [orderByStr, orderByArgs, orderByIndx] = currentModel.makeOrderBy( orderBy, index, depthAlias ); args.push(...orderByArgs); index = orderByIndx; const [limitStr, limitArgs, idxLimit] = currentModel.makeLimit( limit, index ); index = idxLimit; args.push(...limitArgs); const [offsetStr, offsetArgs, idxOffset] = currentModel.makeOffset( offset, index ); args.push(...offsetArgs); index = idxOffset; if (!isAggregate) { sql += ` where ${currentModel.makeRelationalWhereAliases( prevAlias, depthAlias, relation )} ${whereClauseStr} ${groupByStr} ${orderByStr} ${limitStr} ${offsetStr} ) ${depthAlias} ${appendSql} ) ${depthAlias} ) as ${depthAlias} on true `; } else { sql += ` where ${currentModel.makeRelationalWhereAliases( prevAlias, depthAlias, relation )} ${whereClauseStr} ${groupByStr} ) as ${depthAlias} on true `; } } return sql; } const [whereClauseStr, whereArgs, idx] = this.makeWhereClause( this, where, index, alias, true, true ); index = idx; args.push(...whereArgs); const groupByStr = this.makeGroupBy(groupBy, alias); const [orderByStr, orderByArgs, orderByIndx] = this.makeOrderBy( orderBy, index, alias ); args.push(...orderByArgs); index = orderByIndx; const [limitStr, limitArgs, idxLimit] = this.makeLimit(limit, index); index = idxLimit; args.push(...limitArgs); const [offsetStr, offsetArgs, idxOffset] = this.makeOffset(offset, index); args.push(...offsetArgs); index = idxOffset; sql += ` ${whereClauseStr} ${groupByStr} ${orderByStr} ${limitStr} ${offsetStr} ${this.forUpdateResolve( forUpdate )}) ${alias} `; sql += makeQuery(this, include, depth + 1, alias); sql += ` ) ${alias}`; return [sql, args]; } async findOne({ where = {}, include = {}, aggregate = null, select = [], orderBy, groupBy, distinct, extras, asText, forUpdate, } = {}) { try { const [result] = await this.find({ where, include, aggregate, orderBy, groupBy, distinct, select, limit: 1, extras, asText, forUpdate, }); return result; } catch (error) { throw error; } } async find({ where = {}, include = {}, aggregate = null, orderBy, groupBy, distinct, select, limit, offset, extras, asText, forUpdate, } = {}) { try { const [sql, args] = this.buildSelect({ where, include, aggregate, orderBy, groupBy, distinct, select, limit, offset, extras, asText, forUpdate, }); if (DB.enableLog) { DB.log(sql, args); } const result = (await this.selectQueryExec(sql, args))?.[this.table]; if (DB.eventExists(this.schema, this.table, DB.EventNameSpaces.SELECT)) { DB.executeEvent( this.schema, this.table, DB.EventNameSpaces.SELECT, result, this ); } if ( DB.asyncEventExists(this.schema, this.table, DB.EventNameSpaces.SELECT) ) { await DB.executeAsyncEvent( this.schema, this.table, DB.EventNameSpaces.SELECT, result, this ); } if (DB.asyncActionExists(DB.EventNameSpaces.SELECT)) { await DB.executeAsyncAction(DB.EventNameSpaces.SELECT, result, this); } return result; } catch (error) { if (DB.eventExists(this.schema, this.table, DB.EventNameSpaces.ERROR)) { DB.executeEvent( this.schema, this.table, DB.EventNameSpaces.ERROR, error, this ); } if ( DB.asyncEventExists(this.schema, this.table, DB.EventNameSpaces.ERROR) ) { await DB.executeAsyncEvent( this.schema, this.table, DB.EventNameSpaces.ERROR, error, this ); } if (DB.asyncActionExists(DB.EventNameSpaces.ERROR)) { await DB.executeAsyncAction(DB.EventNameSpaces.ERROR, error, this); } throw error; } } async selectOne({ where = {}, include = {}, select = [], orderBy, distinct, extras, asText, forUpdate, } = {}) { try { const [result] = await this.select({ where, include, orderBy, distinct, select, limit: 1, extras, asText, forUpdate, }); return result; } catch (error) { throw error; } } async select({ where = {}, include = {}, orderBy, select, limit, offset, extras, asText, forUpdate, } = {}) { try { let depth = 0; let index = 1; const alias = this.makeDepthAlias(this.table, depth); const args = []; const modelColumnsStr = this.getModelColumnsCommaSeperatedString( alias, select, extras ); let hasInclude = false; const selectColumnsStr = [modelColumnsStr] .concat( Object.keys(include).map((alias, idx) => { const _alias = DB.getRelationNameWithoutAggregate(alias); const isAggregate = DB.getIsAggregate(alias); const relation = this.relations[_alias]; const coalesceFallback = relation.type === "object" ? "null" : "[]"; const coalesceAppendex = relation.type === "object" ? "->0" : ""; hasInclude = true; return `coalesce(json_agg(${this.makeDepthAlias(alias, 1 + idx)}${ isAggregate ? `.${alias}` : "" })${coalesceAppendex},'${coalesceFallback}') as "${alias}"`; }) ) .join(","); let sql = `select ${selectColumnsStr} from "${this.schema}"."${this.table}" ${alias} `; const self = this; function makeQuery(model, relations, depth, prevAlias) { if (!relations || typeof relations === "boolean") { return ""; } let sql = ``; const _iter = Object.entries(relations); for (let i = 0; i < _iter.length; i++) { const [_alias, config] = _iter[i]; const alias = DB.getRelationNameWithoutAggregate(_alias); const isAggregate = DB.getIsAggregate(_alias); const relation = model.relations[alias]; if (!relation) { throw new Error("no such relation"); } const currentModel = DB.getRelatedModel(relation); if (!currentModel) { throw new Error(`no such model for table ${relation.to_table}`); } const depthAlias = isAggregate ? self.makeDepthAlias(relation.alias, depth + i) + "_aggregate" : self.makeDepthAlias(relation.alias, depth + i); const coalesceFallback = relation.type === "object" ? "null" : "[]"; const coalesceAppendex = relation.type === "object" ? "->0" : ""; const modelColumnsStr = currentModel.getModelColumnsCommaSeperatedString( depthAlias, config?.select, config?.extras ); const { distinct, groupBy, orderBy, where, include, limit, offset } = DB.isObject(config) ? config : {}; let hasInclude = false; const selectColumnsStr = [modelColumnsStr] .concat( Object.keys(include || {}).map((alias, idx) => { hasInclude = true; return `coalesce(json_agg(${self.makeDepthAlias( alias, depth + 1 + idx )})${coalesceAppendex},'${coalesceFallback}') as ${alias}`; }) ) .join(","); if (isAggregate) { const [agg] = currentModel.aggregateInternal({ ...config, alias: depthAlias, relationAlias: relation.alias, }); sql += ` left outer join lateral (${agg} `; } else { sql += ` left outer join lateral ( select ${selectColumnsStr} from "${currentModel?.schema}"."${currentModel.table}" ${depthAlias} `; } const appendSql = makeQuery( currentModel, include, depth + 1, depthAlias ); const [whereClauseStr, qArgs, idx] = currentModel.makeWhereClause( currentModel, where, index, depthAlias, false, false ); args.push(...qArgs); index = idx; // const groupByStr = currentModel.makeGroupBy(groupBy, depthAlias); const [orderByStr, orderByArgs, orderByIndx] = currentModel.makeOrderBy(orderBy, index, depthAlias); args.push(...orderByArgs); index = orderByIndx; const [limitStr, limitArgs, idxLimit] = currentModel.makeLimit( limit, index ); index = idxLimit; args.push(...limitArgs); const [offsetStr, offsetArgs, idxOffset] = currentModel.makeOffset( offset, index ); args.push(...offsetArgs); index = idxOffset; if (!isAggregate) { sql += ` ${appendSql} where ${currentModel.makeRelationalWhereAliases( prevAlias, depthAlias, relation )} ${whereClauseStr} ${ hasInclude ? `group by ${modelColumnsStr}` : "" } ${orderByStr} ${limitStr} ${offsetStr} ) ${depthAlias} on true `; } else { sql += ` ${appendSql} where ${currentModel.makeRelationalWhereAliases( prevAlias, depthAlias, relation )} ${whereClauseStr} ) ${depthAlias} on true `; } } return sql; } const [whereClauseStr, whereArgs, idx] = this.makeWhereClause( this, where, index, alias, true, true ); index = idx; args.push(...whereArgs); // const groupByStr = this.makeGroupBy(groupBy, alias); const [orderByStr, orderByArgs, orderByIndx] = this.makeOrderBy( orderBy, index, alias ); args.push(...orderByArgs); index = orderByIndx; const [limitStr, limitArgs, idxLimit] = this.makeLimit(limit, index); index = idxLimit; args.push(...limitArgs); const [offsetStr, offsetArgs, idxOffset] = this.makeOffset(offset, index); args.push(...offsetArgs); index = idxOffset; sql += makeQuery(this, include, depth + 1, alias); sql += ` ${whereClauseStr} ${ hasInclude ? `group by ${modelColumnsStr}` : "" } ${orderByStr} ${limitStr} ${offsetStr} ${this.forUpdateResolve( forUpdate )} `; sql += ` `; if (DB.enableLog) { DB.log(sql, args); } const { rows: result } = await this.raw( sql, args, !!this.connection || !DB.hasReplicas() ); if (DB.eventExists(this.schema, this.table, DB.EventNameSpaces.SELECT)) { DB.executeEvent( this.schema, this.table, DB.EventNameSpaces.SELECT, result, this ); } if ( DB.asyncEventExists(this.schema, this.table, DB.EventNameSpaces.SELECT) ) { await DB.executeAsyncEvent( this.schema, this.table, DB.EventNameSpaces.SELECT, result, this ); } if (DB.asyncActionExists(DB.EventNameSpaces.SELECT)) { await DB.executeAsyncAction(DB.EventNameSpaces.SELECT, result, this); } return result; } catch (error) { if (DB.eventExists(this.schema, this.table, DB.EventNameSpaces.ERROR)) { DB.executeEvent( this.schema, this.table, DB.EventNameSpaces.ERROR, error, this ); } if ( DB.asyncEventExists(this.schema, this.table, DB.EventNameSpaces.ERROR) ) { await DB.executeAsyncEvent( this.schema, this.table, DB.EventNameSpaces.ERROR, error, this ); } if (DB.asyncActionExists(DB.EventNameSpaces.ERROR)) { await DB.executeAsyncAction(DB.EventNameSpaces.ERROR, error, this); } throw error; } } async insert(args) { if (!DB.isObject(args)) { return null; } try { let { onConflict, returning = true, ...rest } = args; const [modelPayload, relationalPayload, hasRelations] = this.splitRelationalAndModelColumnsInput(rest); if (hasRelations) { returning = true; } const [query, qArgs] = this.buildInsertQuery( modelPayload, onConflict, returning ); if (DB.enableLog) { DB.log(query, qArgs); } const [result] = await this.insertQueryExec(query, qArgs, returning); const self = this; async function insertChildren(model, prevResult, relationalPayload) { const _iter = Object.keys(relationalPayload || {}); const aggResult = {}; if (!_iter.length) { return aggResult; } for (let i = 0; i < _iter.length; i++) { const relationAlias = _iter[i]; const insertInput = relationalPayload[relationAlias]; const relation = model.relations?.[relationAlias]; if (!relation) { throw new Error("no such relation"); } const insertionModel = DB.getRelatedModel(relation); if (!insertionModel) { throw new Error("no such model"); } if (relation.type === "object" && Array.isArray(insertInput)) { throw new Error( "relation type object cannot accept an array as input" ); } if (relation.type === "array" && !Array.isArray(insertInput)) { throw new Error("relation type array can only accept array inputs"); } const isArrayRelationalColumns = Array.isArray(relation.from_column); const dependedColumnValues = isArrayRelationalColumns ? relation.from_column.map((x) => prevResult[x]) : prevResult[relation.from_column]; const valuesIter = relation.type === "object" ? [insertInput] : insertInput; const appendResult = []; for (let value of valuesIter) { if (isArrayRelationalColumns) { for (let j = 0; j < relation.to_column.length; j++) { value[relation.to_column[j]] = dependedColumnValues[j]; } } else { value[relation.to_column] = dependedColumnValues; } const { onConflict, ...rest } = value; const [modelPayload, relationalPayload] = insertionModel.splitRelationalAndModelColumnsInput(rest); const [query, args] = insertionModel.buildInsertQuery( modelPayload, onConflict, returning ); if (DB.enableLog) { DB.log(query, args); } const [result] = await self.insertQueryExec(query, args, returning); const childrenResult = await insertChildren( insertionModel, result, relationalPayload ); if (Object.keys(childrenResult)?.length) { Object.assign(result, childrenResult); } appendResult.push(result); } if (relation.type === "object") { aggResult[relationAlias] = appendResult[0]; } else { aggResult[relationAlias] = appendResult; } } return aggResult; } const childrenResult = await insertChildren( this, result, relationalPayload ); if (Object.keys(childrenResult)?.length) { Object.assign(result, childrenResult); } return result; } catch (error) { throw error; } } async createTX(args) { try { await this.connect(); const result = await this.withTransaction(async (tx) => { return await this.insert(args); }); this.disconnect(); if (DB.eventExists(this.schema, this.table, DB.EventNameSpaces.INSERT)) { DB.executeEvent( this.schema, this.table, DB.EventNameSpaces.INSERT, result, this ); } if ( DB.asyncEventExists(this.schema, this.table, DB.EventNameSpaces.INSERT) ) { await DB.executeAsyncEvent( this.schema, this.table, DB.EventNameSpaces.INSERT, result, this ); } if (DB.asyncActionExists(DB.EventNameSpaces.INSERT)) { await DB.executeAsyncAction(DB.EventNameSpaces.INSERT, result, this); } return result; } catch (error) { this.disconnect(); if (DB.eventExists(this.schema, this.table, DB.EventNameSpaces.ERROR)) { DB.executeEvent( this.schema, this.table, DB.EventNameSpaces.ERROR, error, this ); } if ( DB.asyncEventExists(this.schema, this.table, DB.EventNameSpaces.ERROR) ) { await DB.executeAsyncEvent( this.schema, this.table, DB.EventNameSpaces.ERROR, error, this ); } if (DB.asyncActionExists(DB.EventNameSpaces.ERROR)) { await DB.executeAsyncAction(DB.EventNameSpaces.ERROR, error, this); } throw error; } } async createManyTX(args) { try { if (!Array.isArray(args)) { throw new Error("provided input is not an array"); } await this.connect(); const result = await this.withTransaction(async (tx) => { const results = []; for (const input of args) { const res = await this.insert(input); if (res instanceof Error) { throw res; } results.push(res); } return results; }); this.disconnect(); if (DB.eventExists(this.schema, this.table, DB.EventNameSpaces.INSERT)) { DB.executeEvent( this.schema, this.table, DB.EventNameSpaces.INSERT, result, this ); } if ( DB.asyncEventExists(this.schema, this.table, DB.EventNameSpaces.INSERT) ) { await DB.executeAsyncEvent( this.schema, this.table, DB.EventNameSpaces.INSERT, result, this ); } if (DB.asyncActionExists(DB.EventNameSpaces.INSERT)) { await DB.executeAsyncAction(DB.EventNameSpaces.INSERT, result, this); } return result; } catch (error) { this.disconnect(); if (DB.eventExists(this.schema, this.table, DB.EventNameSpaces.ERROR)) { DB.executeEvent( this.schema, this.table, DB.EventNameSpaces.ERROR, error, this ); } if ( DB.asyncEventExists(this.schema, this.table, DB.EventNameSpaces.ERROR) ) { await DB.executeAsyncEvent( this.schema, this.table, DB.EventNameSpaces.ERROR, error, this ); } if (DB.asyncActionExists(DB.EventNameSpaces.ERROR)) { await DB.executeAsyncAction(DB.EventNameSpaces.ERROR, error, this); } throw error; } } async create(args) { try { const result = await this.insert(args); if (DB.eventExists(this.schema, this.table, DB.EventNameSpaces.INSERT)) { DB.executeEvent( this.schema, this.table, DB.EventNameSpaces.INSERT, result, this ); } if ( DB.asyncEventExists(this.schema, this.table, DB.EventNameSpaces.INSERT) ) { await DB.executeAsyncEvent( this.schema, this.table, DB.EventNameSpaces.INSERT, result, this ); } if (DB.asyncActionExists(DB.EventNameSpaces.INSERT)) { await DB.executeAsyncAction(DB.EventNameSpaces.INSERT, result, this); } return result; } catch (error) { if (DB.eventExists(this.schema, this.table, DB.EventNameSpaces.ERROR)) { DB.executeEvent( this.schema, this.table, DB.EventNameSpaces.ERROR, error, this ); } if ( DB.asyncEventExists(this.schema, this.table, DB.EventNameSpaces.ERROR) ) { await DB.executeAsyncEvent( this.schema, this.table, DB.EventNameSpaces.ERROR, error, this ); } if (DB.asyncActionExists(DB.EventNameSpaces.ERROR)) { await DB.executeAsyncAction(DB.EventNameSpaces.ERROR, error, this); } throw error; } } async createMany(args) { try { if (!Array.isArray(args)) { throw new Error("provided input is not an array"); } const promises = args.map((input) => this.insert(input)); const result = await Promise.all(promises); if (DB.eventExists(this.schema, this.table, DB.EventNameSpaces.INSERT)) { DB.executeEvent( this.schema, this.table, DB.EventNameSpaces.INSERT, result, this ); } if ( DB.asyncEventExists(this.schema, this.table, DB.EventNameSpaces.INSERT) ) { await DB.executeAsyncEvent( this.schema, this.table, DB.EventNameSpaces.INSERT, result, this ); } if (DB.asyncActionExists(DB.EventNameSpaces.INSERT)) { await DB.executeAsyncAction(DB.EventNameSpaces.INSERT, result, this); } return result; } catch (error) { if (DB.eventExists(this.schema, this.table, DB.EventNameSpaces.ERROR)) { DB.executeEvent( this.schema, this.table, DB.EventNameSpaces.ERROR, error, this ); } if ( DB.asyncEventExists(this.schema, this.table, DB.EventNameSpaces.ERROR) ) { await DB.executeAsyncEvent( this.schema, this.table, DB.EventNameSpaces.ERROR, error, this ); } if (DB.asyncActionExists(DB.EventNameSpaces.ERROR)) { await DB.executeAsyncAction(DB.EventNameSpaces.ERROR, error, this); } throw error; } } buildUpdateSetOperation(columns, index = 0, isConflict, updatedColumns) { return Object.entries(columns).reduce( (acc, [key, value]) => { if (SELF_UPDATE_OPERATORS[key]) { if (!DB.isObject(value)) { throw new Error( "self update operators should have an object value" ); } const [selfUpdateColumns] = this.splitRelationalAndModelColumnsInput(value); acc[0].push( ...Object.entries(selfUpdateColumns).map(([c, val]) => { const sql = `"${c}" = "${c}" ${SELF_UPDATE_OPERATORS[key]} $${ acc[1].length + 1 + index }`; acc[1].push(val); return sql; }) ); } else { if (this.isGeospatialColumn(key)) { const [str, args, _] = this.getGeospatialColumnValueForStatement( key, value, acc[1].length ); acc[0].push(`"${key}" = ${str}`); acc[1].push(...args); } else { if (value instanceof SQL) { const [str, qArgs, _] = value.__getSQL({ index: acc[1].length + 1 + index, column: key, alias: this.table, args: acc[1], binder: "", }); if (str) { acc[0].push(` "${key}" = ${str} `); acc[1].push(...qArgs); } return acc; } acc[0].push(`"${key}" = $${acc[1].length + 1 + index}`); acc[1].push(value); } } return acc; }, [[], []] ); } async update({ update, where = {}, returning = true }) { try { const [modelColumns] = this.splitRelationalAndModelColumnsInput( update, Object.keys(SELF_UPDATE_OPERATORS) ); const [columns, qArgs] = this.buildUpdateSetOperation(modelColumns, 0); const [whereStr, whereArgs] = this.makeWhereClause( this, where, qArgs.length + 1, this.table, true, true ); const sql = `update "${this.schema}"."${this.table}" set ${columns.join( "," )} ${whereStr} ${returning ? `returning *` : ""}`; qArgs.push(...whereArgs); if (DB.enableLog) { DB.log(sql, qArgs); } const result = await this.updateQueryExec(sql, qArgs, returning); if (DB.eventExists(this.schema, this.table, DB.EventNameSpaces.UPDATE)) { DB.executeEvent( this.schema, this.table, DB.EventNameSpaces.UPDATE, result, this ); } if ( DB.asyncEventExists(this.schema, this.table, DB.EventNameSpaces.UPDATE) ) { await DB.executeAsyncEvent( this.schema, this.table, DB.EventNameSpaces.UPDATE, result, this ); } if (DB.asyncActionExists(DB.EventNameSpaces.UPDATE)) { await DB.executeAsyncAction(DB.EventNameSpaces.UPDATE, result, this); } return result; } catch (error) { if (DB.eventExists(this.schema, this.table, DB.EventNameSpaces.ERROR)) { DB.executeEvent( this.schema, this.table, DB.EventNameSpaces.ERROR, error, this ); } if ( DB.asyncEventExists(this.schema, this.table, DB.EventNameSpaces.ERROR) ) { await DB.executeAsyncEvent( this.schema, this.table, DB.EventNameSpaces.ERROR, error, this ); } if (DB.asyncActionExists(DB.EventNameSpaces.ERROR)) { await DB.executeAsyncAction(DB.EventNameSpaces.ERROR, error, this); } throw error; } } async delete({ where = {}, returning = true }) { try { const [whereStr, whereArgs] = this.makeWhereClause( this, where, 1, this.table, true, true ); const sql = `delete from "${this.schema}"."${this.table}" ${whereStr} ${ returning ? `returning *` : "" }`; if (DB.enableLog) { DB.log(sql, whereArgs); } const result = await this.deleteQueryExec(sql, whereArgs, returning); if (DB.eventExists(this.schema, this.table, DB.EventNameSpaces.DELETE)) { DB.executeEvent( this.schema, this.table, DB.EventNameSpaces.DELETE, result, this ); } if ( DB.asyncEventExists(this.schema, this.table, DB.EventNameSpaces.DELETE) ) { await DB.executeAsyncEvent( this.schema, this.table, DB.EventNameSpaces.DELETE, result, this ); } if (DB.asyncActionExists(DB.EventNameSpaces.DELETE)) { await DB.executeAsyncAction(DB.EventNameSpaces.DELETE, result, this); } return result; } catch (error) { if (DB.eventExists(this.schema, this.table, DB.EventNameSpaces.ERROR)) { DB.executeEvent( this.schema, this.table, DB.EventNameSpaces.ERROR, error, this ); } if ( DB.asyncEventExists(this.schema, this.table, DB.EventNameSpaces.ERROR) ) { await DB.executeAsyncEvent( this.schema, this.table, DB.EventNameSpaces.ERROR, error, this ); } if (DB.asyncActionExists(DB.EventNameSpaces.ERROR)) { await DB.executeAsyncAction(DB.EventNameSpaces.ERROR, error, this); } throw error; } } async aggregate({ where, groupBy, distinct, _count, _max, _min, _sum, _avg, } = {}) { try { const countAgg = this.buildCountAgg(_count, distinct, this.table); const aggregations = [ { key: "_min", value: _min, }, { key: "_max", value: _max, }, { key: "_sum", value: _sum, }, { key: "_avg", value: _avg, }, ] .map((x) => this.buildAgg(x.value, x.key)) .concat(countAgg) .filter(Boolean) .join(","); if (!aggregations.length) { throw new Error("no aggregations were found for this operation"); } const [whereStr, whereArgs] = this.makeWhereClause( this, where, 1, this.table, true, true ); const groupByStr = this.makeGroupBy(groupBy, this.table); // const distinctStr = this.makeDistinctOn(distinct, this.table); const sql = `select json_build_object(${aggregations}) as ${this.table}_aggregate from "${this.schema}"."${this.table}" ${whereStr} ${groupByStr}`; if (DB.enableLog) { DB.log(sql, whereArgs); } return (await this.selectQueryExec(sql, whereArgs))?.[ `${this.table}_aggregate` ]; } catch (error) { throw error; } } aggregateInternal({ where, groupBy, distinct, _count, _max, _min, _sum, _avg, index = 1, alias, withWhere = false, relationAlias, } = {}) { try { const countAgg = this.buildCountAgg( _count, distinct, alias || this.table ); const aggregations = [ { key: "_min", value: _min, }, { key: "_max", value: _max, }, { key: "_sum", value: _sum, }, { key: "_avg", value: _avg, }, ] .map((x) => this.buildAgg(x.value, x.key)) .concat(countAgg) .filter(Boolean) .join(","); if (!aggregations.length) { throw new Error("no aggregations were found for this operation"); } if (withWhere) { const [whereStr, whereArgs, idx] = this.makeWhereClause( this, where, index, alias, true, true ); const groupByStr = this.makeGroupBy(groupBy, this.table); // const distinctStr = this.makeDistinctOn(distinct, this.table); const sql = `select json_build_object(${aggregations}) as ${relationAlias}_aggregate from "${this.schema}"."${this.table}" as ${alias} ${whereStr} ${groupByStr}`; return [sql, whereArgs, idx]; } const groupByStr = this.makeGroupBy(groupBy, this.table); const distinctStr = this.makeDistinctOn(distinct, this.table); const sql = `select ${distinctStr} json_build_object(${aggregations}) as ${relationAlias}_aggregate from "${this.schema}"."${this.table}" as ${alias} ${groupByStr}`; return [sql, "", index]; } catch (error) { throw error; } } buildCountAgg(count, distinct, table) { if (!count) { return; } if (distinct) { const formattedDistinct = Array.isArray(distinct) ? distinct.length > 1 ? distinct[0] : distinct : distinct; if (!formattedDistinct || !formattedDistinct?.length) { return `'count',count(*)`; } return `'count',count(distinct ${formattedDistinct})`; } return `'count',count(*)`; } buildAgg(aggConfig, key) { if (!DB.isObject(aggConfig)) { return; } const [modelColumns] = this.splitRelationalAndModelColumnsInput(aggConfig); const columns = Object.keys(modelColumns).filter((x) => !!modelColumns[x]); if (!columns.length) { return []; } return `'${SupportedAggregations[ key ].toLowerCase()}',json_build_object(${columns .map((column) => `'${column}',${SupportedAggregations[key]}("${column}")`) .join(",")})`; } isGeospatialColumn(key) { return ( DB.postgis && (this.columns?.[key]?.type?.toLowerCase()?.includes("geometry") || this.columns?.[key]?.type?.toLowerCase()?.includes("geography")) ); } getGeoJSONFunctionByColumnType(column) { switch (column.type?.toLowerCase()?.trim()) { case "geography": return `ST_GeogFromGeoJSON`; case "geometry": return `ST_GeomFromGeoJSON`; default: throw new Error(`Unsupported column type ${column.type}`); } } getGeospatialColumnValueForStatement(columnName, data, currentIndex) { const { srid, ...rest } = data || {}; if (!rest?.type) { throw new Error("No geometry type provided"); } const str = `${!!srid ? `ST_SetSRID(` : ""}ST_GeomFromGeoJSON($${ currentIndex + 1 })${srid ? `, $${currentIndex + 2})` : ""}`; return [ str, srid ? [rest, srid] : [rest], srid ? currentIndex + 1 : currentIndex, ]; } buildInsertQuery(args, onConflict, returning = true) { if (!DB.isObject(args)) { throw new Error(); } let index = 0; const [columns, placeholders, qArgs] = Object.entries(args).reduce( (acc, [key, value]) => { acc[0].push(`"${key}"`); if (this.isGeospatialColumn(key)) { const [str, args, currentIndex] = this.getGeospatialColumnValueForStatement(key, value, index); index = currentIndex + 1; acc[1].push(str); acc[2].push(...args); } else { acc[1].push(`$${index + 1}`); index++; acc[2].push(value); } return acc; }, [[], [], []] ); const { update, ignore, constraint, where } = onConflict || {}; let conflictSql = ""; if ( !!constraint && (typeof constraint === "string" || (Array.isArray(constraint) && constraint.length > 0)) ) { conflictSql = ` on conflict (${ Array.isArray(constraint) ? constraint.join(",") : constraint }) `; if (!!ignore) { conflictSql += ` do nothing `; } else if (update?.length) { const columns = update .reduce((acc, c) => { acc.push(`"${c}" = EXCLUDED."${c}"`); return acc; }, []) .join(","); const [whereStr, whereArgs] = this.makeWhereClause( this, where, qArgs.length + 1, this.table, true, true ); conflictSql += ` do update set ${columns} ${whereStr}`; qArgs.push(...whereArgs); } } // const conflictingSql = !!ignore ? ' ON CONFLICT DO NOTHING ' : !!update && Array.isArray(update) && update.length > 0 ? : '' return [ `insert into "${this.schema}"."${this.table}" (${columns.