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
JavaScript
"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.