@saltcorn/postgres 1.6.0 → 1.7.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/postgres.d.ts +286 -0
- package/dist/postgres.d.ts.map +1 -0
- package/dist/postgres.js +702 -0
- package/dist/postgres.js.map +1 -0
- package/package.json +29 -5
- package/index.js +0 -14
- package/postgres.js +0 -786
package/dist/postgres.js
ADDED
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostgreSQL data access layer
|
|
3
|
+
* @category postgres
|
|
4
|
+
* @module postgres
|
|
5
|
+
*/
|
|
6
|
+
// TODO move postgresql specific to this module
|
|
7
|
+
import { Pool, types } from "pg";
|
|
8
|
+
import * as copyStreams from "pg-copy-streams";
|
|
9
|
+
import { pipeline } from "stream/promises";
|
|
10
|
+
import replace from "replacestream";
|
|
11
|
+
import { createRequire } from "module";
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
import { sqlsanitize, mkWhere, mkSelectOptions, } from "@saltcorn/db-common/internal";
|
|
14
|
+
import PlainDate from "@saltcorn/plain-date";
|
|
15
|
+
import tenantsModule from "@saltcorn/db-common/tenants";
|
|
16
|
+
types.setTypeParser(types.builtins.DATE, (d) => d === null ? null : new PlainDate(d));
|
|
17
|
+
let getTenantSchema;
|
|
18
|
+
let getRequestContext;
|
|
19
|
+
let getConnectObject = null;
|
|
20
|
+
export let pool = null;
|
|
21
|
+
let log_sql_enabled = false;
|
|
22
|
+
const quote = (s) => `"${s}"`;
|
|
23
|
+
const ppPK = (pk) => (pk ? quote(pk) : "id");
|
|
24
|
+
/**
|
|
25
|
+
* Control Logging sql statements to console
|
|
26
|
+
* @param {boolean} [val = true] - if true then log sql statements to console
|
|
27
|
+
*/
|
|
28
|
+
export function set_sql_logging(val = true) {
|
|
29
|
+
log_sql_enabled = val;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Get sql logging state
|
|
33
|
+
* @returns {boolean} if true then sql logging eabled
|
|
34
|
+
*/
|
|
35
|
+
export function get_sql_logging() {
|
|
36
|
+
return log_sql_enabled;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Log SQL statement to console
|
|
40
|
+
* @param {string} sql - SQL statement
|
|
41
|
+
* @param {object} [vs] - any additional parameter
|
|
42
|
+
*/
|
|
43
|
+
export function sql_log(sql, vs) {
|
|
44
|
+
if (log_sql_enabled)
|
|
45
|
+
if (typeof vs === "undefined")
|
|
46
|
+
console.log(sql);
|
|
47
|
+
else
|
|
48
|
+
console.log(sql, vs);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Close database connection
|
|
52
|
+
* @returns {Promise<void>}
|
|
53
|
+
*/
|
|
54
|
+
export const close = async () => {
|
|
55
|
+
if (pool)
|
|
56
|
+
await pool.end();
|
|
57
|
+
pool = null;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Change connection (close connection and open new connection from connObj)
|
|
61
|
+
* @param {object} [connObj = {}] - connection object
|
|
62
|
+
* @returns {Promise<void>}
|
|
63
|
+
*/
|
|
64
|
+
export const changeConnection = async (connObj = Object.create(null)) => {
|
|
65
|
+
await close();
|
|
66
|
+
pool = new Pool(getConnectObject(connObj));
|
|
67
|
+
};
|
|
68
|
+
export const begin = async () => {
|
|
69
|
+
//client = await getClient();
|
|
70
|
+
await query("BEGIN");
|
|
71
|
+
};
|
|
72
|
+
export const commit = async () => {
|
|
73
|
+
await query("COMMIT");
|
|
74
|
+
};
|
|
75
|
+
export const rollback = async () => {
|
|
76
|
+
await query("ROLLBACK");
|
|
77
|
+
};
|
|
78
|
+
const getMyClient = (selopts) => {
|
|
79
|
+
const ctx = getRequestContext();
|
|
80
|
+
return selopts?.client || ctx?.client || pool;
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* Execute Select statement
|
|
84
|
+
* @param {string} tbl - table name
|
|
85
|
+
* @param {object} whereObj - where object
|
|
86
|
+
* @param {object} [selectopts = {}] - select options
|
|
87
|
+
* @returns {Promise<*>} return rows
|
|
88
|
+
*/
|
|
89
|
+
export const select = async (tbl, whereObj, selectopts = Object.create(null)) => {
|
|
90
|
+
const { where, values } = mkWhere(whereObj);
|
|
91
|
+
const schema = selectopts.schema || getTenantSchema();
|
|
92
|
+
let sql;
|
|
93
|
+
if (selectopts.tree_field && !whereObj[selectopts.tree_field])
|
|
94
|
+
sql = `WITH RECURSIVE _tree AS (
|
|
95
|
+
SELECT ${selectopts.fields ? selectopts.fields.join(", ") : `*`}, 0 as _level
|
|
96
|
+
${selectopts.orderBy ? `, ARRAY[row_number() over (ORDER BY "${sqlsanitize(selectopts.orderBy)}"${selectopts.orderDesc ? " DESC" : ""})] as _sort_path` : ""}
|
|
97
|
+
FROM "${schema}"."${sqlsanitize(tbl)}"
|
|
98
|
+
WHERE "${selectopts.tree_field}" IS NULL ${where ? `AND ${where.replace("where ", "")}` : ""}
|
|
99
|
+
|
|
100
|
+
UNION ALL
|
|
101
|
+
|
|
102
|
+
SELECT ${selectopts.fields
|
|
103
|
+
? selectopts.fields.map((f) => `p."${f}"`).join(", ")
|
|
104
|
+
: `p.*`}, pt._level+1
|
|
105
|
+
${selectopts.orderBy ? `, pt._sort_path || row_number() OVER (PARTITION BY p."${selectopts.tree_field}" ORDER BY p."${selectopts.orderBy}"${selectopts.orderDesc ? " DESC" : ""})` : ""}
|
|
106
|
+
FROM "${schema}"."${sqlsanitize(tbl)}" p
|
|
107
|
+
JOIN _tree pt ON p."${selectopts.tree_field}" = pt."${selectopts.pk_name || "id"}"
|
|
108
|
+
)
|
|
109
|
+
SELECT ${selectopts.fields ? selectopts.fields.join(", ") : `*`}, _level FROM _tree ${where} ${mkSelectOptions(selectopts.orderBy
|
|
110
|
+
? { ...selectopts, orderBy: "_sort_path", orderDesc: false }
|
|
111
|
+
: selectopts, values, false)}`;
|
|
112
|
+
else
|
|
113
|
+
sql = `SELECT ${selectopts.fields ? selectopts.fields.join(", ") : `*`} FROM "${schema}"."${sqlsanitize(tbl)}" ${where} ${mkSelectOptions(selectopts, values, false)}`;
|
|
114
|
+
sql_log(sql, values);
|
|
115
|
+
const tq = await getMyClient(selectopts).query(sql, values);
|
|
116
|
+
return tq.rows;
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* Reset DB Schema using drop schema and recreate it
|
|
120
|
+
* Atterntion! You will lost data after call this function!
|
|
121
|
+
* @param {string} schema - db schema name
|
|
122
|
+
* @returns {Promise<void>} no result
|
|
123
|
+
*/
|
|
124
|
+
export const drop_reset_schema = async (schema) => {
|
|
125
|
+
const sql = `DROP SCHEMA IF EXISTS "${schema}" CASCADE;
|
|
126
|
+
CREATE SCHEMA "${schema}";
|
|
127
|
+
GRANT ALL ON SCHEMA "${schema}" TO postgres;
|
|
128
|
+
GRANT ALL ON SCHEMA "${schema}" TO "public" ;
|
|
129
|
+
COMMENT ON SCHEMA "${schema}" IS 'standard public schema';`;
|
|
130
|
+
sql_log(sql);
|
|
131
|
+
await getMyClient().query(sql);
|
|
132
|
+
};
|
|
133
|
+
/**
|
|
134
|
+
* Create the Postgres schema backing a tenant namespace.
|
|
135
|
+
* @param {string} name - tenant/schema name
|
|
136
|
+
* @param {boolean} [ifNotExists] - use IF NOT EXISTS (idempotent, used by tests)
|
|
137
|
+
* @returns {Promise<void>} no result
|
|
138
|
+
*/
|
|
139
|
+
export const create_tenant_schema = async (name, ifNotExists) => {
|
|
140
|
+
const sql = `CREATE SCHEMA ${ifNotExists ? "IF NOT EXISTS " : ""}"${sqlsanitize(name)}";`;
|
|
141
|
+
sql_log(sql);
|
|
142
|
+
await getMyClient().query(sql);
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* Drop the Postgres schema backing a tenant namespace.
|
|
146
|
+
* @param {string} name - tenant/schema name
|
|
147
|
+
* @returns {Promise<void>} no result
|
|
148
|
+
*/
|
|
149
|
+
export const drop_tenant_schema = async (name) => {
|
|
150
|
+
const sql = `drop schema if exists "${sqlsanitize(name)}" CASCADE`;
|
|
151
|
+
sql_log(sql);
|
|
152
|
+
await getMyClient().query(sql);
|
|
153
|
+
};
|
|
154
|
+
/**
|
|
155
|
+
* Upsert a row into _sc_config.
|
|
156
|
+
* @param {string} key
|
|
157
|
+
* @param {any} value
|
|
158
|
+
* @returns {Promise<void>} no result
|
|
159
|
+
*/
|
|
160
|
+
export const upsert_config = async (key, value) => {
|
|
161
|
+
const sql = `insert into "${getTenantSchema()}"."_sc_config"("key", value) values($1, $2)
|
|
162
|
+
on conflict ("key") do update set value = $2`;
|
|
163
|
+
sql_log(sql, [key, { v: value }]);
|
|
164
|
+
await getMyClient().query(sql, [key, { v: value }]);
|
|
165
|
+
};
|
|
166
|
+
/**
|
|
167
|
+
* Build the express-session Store backed by this Postgres pool.
|
|
168
|
+
* @param {any} session - the express-session module instance the app uses
|
|
169
|
+
* @param {object} [opts]
|
|
170
|
+
* @returns {any} a session.Store instance
|
|
171
|
+
*/
|
|
172
|
+
export const getExpressSessionStore = (session, opts = {}) => {
|
|
173
|
+
const pgSession = require("connect-pg-simple")(session);
|
|
174
|
+
return new pgSession({
|
|
175
|
+
schemaName: getTenantSchema(),
|
|
176
|
+
pool,
|
|
177
|
+
tableName: "_sc_session",
|
|
178
|
+
createTableIfMissing: true,
|
|
179
|
+
pruneSessionInterval: (opts.pruneInterval ?? 0) > 0 ? opts.pruneInterval : false,
|
|
180
|
+
});
|
|
181
|
+
};
|
|
182
|
+
/**
|
|
183
|
+
* Get count of rows in table
|
|
184
|
+
* @param {string} - tbl - table name
|
|
185
|
+
* @param {object} - whereObj - where object
|
|
186
|
+
* @returns {Promise<number>} count of tables
|
|
187
|
+
*/
|
|
188
|
+
export const count = async (tbl, whereObj, opts) => {
|
|
189
|
+
const { where, values } = mkWhere(whereObj);
|
|
190
|
+
if (!where) {
|
|
191
|
+
try {
|
|
192
|
+
// fast count for large table but may be stale
|
|
193
|
+
// https://stackoverflow.com/questions/7943233/fast-way-to-discover-the-row-count-of-a-table-in-postgresql
|
|
194
|
+
//https://www.citusdata.com/blog/2016/10/12/count-performance/
|
|
195
|
+
const sql = `SELECT (CASE WHEN c.reltuples < 0 THEN NULL
|
|
196
|
+
WHEN c.relpages = 0 THEN float8 '0' -- empty table
|
|
197
|
+
ELSE c.reltuples / c.relpages END
|
|
198
|
+
* (pg_catalog.pg_relation_size(c.oid)
|
|
199
|
+
/ pg_catalog.current_setting('block_size')::int)
|
|
200
|
+
)::bigint
|
|
201
|
+
FROM pg_catalog.pg_class c
|
|
202
|
+
WHERE c.oid = '"${opts?.schema || getTenantSchema()}"."${sqlsanitize(tbl)}"'::regclass`;
|
|
203
|
+
sql_log(sql);
|
|
204
|
+
const tq = await getMyClient(opts).query(sql, []);
|
|
205
|
+
const n = +tq.rows[0].int8;
|
|
206
|
+
if (n && n > 10000)
|
|
207
|
+
return n;
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
//skip fast estimate
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const core_sql = `FROM "${opts?.schema || getTenantSchema()}"."${sqlsanitize(tbl)}" ${where}`;
|
|
214
|
+
//https://pganalyze.com/blog/5mins-postgres-limited-count
|
|
215
|
+
const sql = opts?.limit
|
|
216
|
+
? `SELECT count(*) AS count FROM (
|
|
217
|
+
SELECT 1 ${core_sql} limit ${+opts?.limit}) limited_count`
|
|
218
|
+
: `SELECT COUNT(*) ${core_sql}`;
|
|
219
|
+
sql_log(sql, values);
|
|
220
|
+
const tq = await getMyClient(opts).query(sql, values);
|
|
221
|
+
return parseInt(tq.rows[0].count);
|
|
222
|
+
};
|
|
223
|
+
/**
|
|
224
|
+
* Get version of PostgreSQL
|
|
225
|
+
* @param {boolean} short - if true return short version info else full version info
|
|
226
|
+
* @returns {Promise<string>} returns version
|
|
227
|
+
*/
|
|
228
|
+
export const getVersion = async (short) => {
|
|
229
|
+
const sql = `SELECT version();`;
|
|
230
|
+
sql_log(sql);
|
|
231
|
+
const tq = await getMyClient().query(sql);
|
|
232
|
+
const v = tq.rows[0].version;
|
|
233
|
+
if (short) {
|
|
234
|
+
const ws = v.split(" ");
|
|
235
|
+
return ws[1];
|
|
236
|
+
}
|
|
237
|
+
return v;
|
|
238
|
+
};
|
|
239
|
+
/**
|
|
240
|
+
* Delete rows in table
|
|
241
|
+
* @param {string} tbl - table name
|
|
242
|
+
* @param {object} whereObj - where object
|
|
243
|
+
* @param {object} [opts = {}]
|
|
244
|
+
* @returns {Promise<object[]>} result of delete execution
|
|
245
|
+
*/
|
|
246
|
+
export const deleteWhere = async (tbl, whereObj, opts = Object.create(null)) => {
|
|
247
|
+
const { where, values } = mkWhere(whereObj);
|
|
248
|
+
const sql = `delete FROM "${opts.schema || getTenantSchema()}"."${sqlsanitize(tbl)}" ${where}`;
|
|
249
|
+
sql_log(sql, values);
|
|
250
|
+
const tq = await getMyClient(opts).query(sql, values);
|
|
251
|
+
return tq.rows;
|
|
252
|
+
};
|
|
253
|
+
export const truncate = async (tbl) => {
|
|
254
|
+
const sql = `truncate "${getTenantSchema()}"."${sqlsanitize(tbl)}"`;
|
|
255
|
+
sql_log(sql, []);
|
|
256
|
+
const tq = await getMyClient().query(sql, []);
|
|
257
|
+
return tq.rows;
|
|
258
|
+
};
|
|
259
|
+
/**
|
|
260
|
+
* Insert rows into table
|
|
261
|
+
* @param {string} tbl - table name
|
|
262
|
+
* @param {object} obj - columns names and data
|
|
263
|
+
* @param {object} [opts = {}] - columns attributes
|
|
264
|
+
* @returns {Promise<string>} returns primary key column or Id column value. If primary key column is not defined then return value of Id column.
|
|
265
|
+
*/
|
|
266
|
+
export const insert = async (tbl, obj, opts = Object.create(null)) => {
|
|
267
|
+
const kvs = Object.entries(obj);
|
|
268
|
+
const fnameList = kvs.map(([k, v]) => `"${sqlsanitize(k)}"`).join();
|
|
269
|
+
var valPosList = [];
|
|
270
|
+
var valList = [];
|
|
271
|
+
const schema = opts.schema || getTenantSchema();
|
|
272
|
+
const conflict = opts.onConflictDoNothing ? "on conflict do nothing " : "";
|
|
273
|
+
kvs.forEach(([k, v]) => {
|
|
274
|
+
if (v && v.next_version_by_id) {
|
|
275
|
+
valList.push(v.next_version_by_id);
|
|
276
|
+
valPosList.push(`coalesce((select max(_version) from "${schema}"."${sqlsanitize(tbl)}" where "${v.pk_name || "id"}"=$${valList.length}), 0)+1`);
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
valList.push(v);
|
|
280
|
+
valPosList.push(`$${valList.length}`);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
const sql = valPosList.length > 0
|
|
284
|
+
? `insert into "${schema}"."${sqlsanitize(tbl)}"(${fnameList}) values(${valPosList.join()}) ${conflict}returning ${opts.noid ? "*" : ppPK(opts.pk_name)}`
|
|
285
|
+
: `insert into "${schema}"."${sqlsanitize(tbl)}" DEFAULT VALUES returning ${opts.noid ? "*" : ppPK(opts.pk_name)}`;
|
|
286
|
+
sql_log(sql, valList);
|
|
287
|
+
const { rows } = await getMyClient(opts).query(sql, valList);
|
|
288
|
+
if (opts.noid)
|
|
289
|
+
return;
|
|
290
|
+
else if (conflict && rows.length === 0)
|
|
291
|
+
return;
|
|
292
|
+
else
|
|
293
|
+
return rows[0][opts.pk_name || "id"];
|
|
294
|
+
};
|
|
295
|
+
/**
|
|
296
|
+
* Update table records
|
|
297
|
+
* @param {string} tbl - table name
|
|
298
|
+
* @param {object} obj - columns names and data
|
|
299
|
+
* @param {number|undefined} id - id of record (primary key column value)
|
|
300
|
+
* @param {object} [opts = {}] - columns attributes
|
|
301
|
+
* @returns {Promise<void>} no result
|
|
302
|
+
*/
|
|
303
|
+
export const update = async (tbl, obj, id, opts = Object.create(null)) => {
|
|
304
|
+
const kvs = Object.entries(obj);
|
|
305
|
+
if (kvs.length === 0)
|
|
306
|
+
return;
|
|
307
|
+
const assigns = kvs
|
|
308
|
+
.map(([k, v], ix) => `"${sqlsanitize(k)}"=$${ix + 1}`)
|
|
309
|
+
.join();
|
|
310
|
+
let valList = kvs.map(([k, v]) => v);
|
|
311
|
+
// TBD check that is correct - because in insert function opts.noid ? "*" : opts.pk_name || "id"
|
|
312
|
+
//valList.push(id === "undefined"? obj[opts.pk_name]: id);
|
|
313
|
+
let whereS;
|
|
314
|
+
if (id && typeof id == "object") {
|
|
315
|
+
let n = kvs.length + 1;
|
|
316
|
+
const whereStrs = [];
|
|
317
|
+
Object.keys(id).forEach((k) => {
|
|
318
|
+
valList.push(id[k]);
|
|
319
|
+
whereStrs.push(`"${k}"=$${n}`);
|
|
320
|
+
n += 1;
|
|
321
|
+
});
|
|
322
|
+
whereS = whereStrs.join(" and ");
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
valList.push(id === "undefined" ? obj[opts.pk_name || "id"] : id);
|
|
326
|
+
whereS = `${ppPK(opts.pk_name)}=$${kvs.length + 1}`;
|
|
327
|
+
}
|
|
328
|
+
const q = `update "${opts.schema || getTenantSchema()}"."${sqlsanitize(tbl)}" set ${assigns} where ${whereS}`;
|
|
329
|
+
sql_log(q, valList);
|
|
330
|
+
await getMyClient(opts).query(q, valList);
|
|
331
|
+
};
|
|
332
|
+
/**
|
|
333
|
+
* Update table records
|
|
334
|
+
* @param {string} tbl - table name
|
|
335
|
+
* @param {object} obj - columns names and data
|
|
336
|
+
* @param {object} whereObj - where object
|
|
337
|
+
* @param {object} opts - can contain a db client for transactions
|
|
338
|
+
* @returns {Promise<void>} no result
|
|
339
|
+
*/
|
|
340
|
+
export const updateWhere = async (tbl, obj, whereObj, opts = Object.create(null)) => {
|
|
341
|
+
const kvs = Object.entries(obj);
|
|
342
|
+
if (kvs.length === 0)
|
|
343
|
+
return;
|
|
344
|
+
const { where, values } = mkWhere(whereObj, false, kvs.length);
|
|
345
|
+
const assigns = kvs
|
|
346
|
+
.map(([k, v], ix) => `"${sqlsanitize(k)}"=$${ix + 1}`)
|
|
347
|
+
.join();
|
|
348
|
+
let valList = [...kvs.map(([k, v]) => v), ...values];
|
|
349
|
+
const q = `update "${getTenantSchema()}"."${sqlsanitize(tbl)}" set ${assigns} ${where}`;
|
|
350
|
+
sql_log(q, valList);
|
|
351
|
+
await getMyClient().query(q, valList);
|
|
352
|
+
};
|
|
353
|
+
/**
|
|
354
|
+
* Select one record
|
|
355
|
+
* @param {srting} tbl - table name
|
|
356
|
+
* @param {object} where - where object
|
|
357
|
+
* @param {object} [selectopts = {}] - select options
|
|
358
|
+
* @returns {Promise<object>} return first record from sql result
|
|
359
|
+
* @throws {Error}
|
|
360
|
+
*/
|
|
361
|
+
export const selectOne = async (tbl, where, selectopts = Object.create(null)) => {
|
|
362
|
+
const rows = await select(tbl, where, { ...selectopts, limit: 1 });
|
|
363
|
+
if (rows.length === 0) {
|
|
364
|
+
const w = mkWhere(where);
|
|
365
|
+
throw new Error(`no ${tbl} ${w.where} are ${w.values}`);
|
|
366
|
+
}
|
|
367
|
+
else
|
|
368
|
+
return rows[0];
|
|
369
|
+
};
|
|
370
|
+
/**
|
|
371
|
+
* Select one record or null if no records
|
|
372
|
+
* @param {string} tbl - table name
|
|
373
|
+
* @param {object} where - where object
|
|
374
|
+
* @param {object} [selectopts = {}] - select options
|
|
375
|
+
* @returns {Promise<null|object>} - null if no record or first record data
|
|
376
|
+
*/
|
|
377
|
+
export const selectMaybeOne = async (tbl, where, selectopts = Object.create(null)) => {
|
|
378
|
+
const rows = await select(tbl, where, { ...selectopts, limit: 1 });
|
|
379
|
+
if (rows.length === 0)
|
|
380
|
+
return null;
|
|
381
|
+
else
|
|
382
|
+
return rows[0];
|
|
383
|
+
};
|
|
384
|
+
/**
|
|
385
|
+
* Open db connection
|
|
386
|
+
* Only for PG.
|
|
387
|
+
* @returns {Promise<*>} db connection object
|
|
388
|
+
*/
|
|
389
|
+
// TBD Currently this function supported only for PG
|
|
390
|
+
export const getClient = async () => await pool.connect();
|
|
391
|
+
/**
|
|
392
|
+
* Reset sequence
|
|
393
|
+
* Only for PG
|
|
394
|
+
* @param {string} tblname - table name
|
|
395
|
+
* @returns {Promise<void>} no result
|
|
396
|
+
*/
|
|
397
|
+
export const reset_sequence = async (tblname, pkname = "id") => {
|
|
398
|
+
const sql = `SELECT setval(pg_get_serial_sequence('"${getTenantSchema()}"."${sqlsanitize(tblname)}"', '${pkname}'), coalesce(max("${pkname}"),0) + 1, false) FROM "${getTenantSchema()}"."${sqlsanitize(tblname)}";`;
|
|
399
|
+
await getMyClient().query(sql);
|
|
400
|
+
};
|
|
401
|
+
/**
|
|
402
|
+
* Add unique constraint
|
|
403
|
+
* @param {string} table_name - table name
|
|
404
|
+
* @param {string[]} field_names - list of columns (members of constraint)
|
|
405
|
+
* @returns {Promise<void>} no result
|
|
406
|
+
*/
|
|
407
|
+
export const add_unique_constraint = async (table_name, field_names) => {
|
|
408
|
+
// TBD check that there are no problems with lenght of constraint name
|
|
409
|
+
const sql = `alter table "${getTenantSchema()}"."${sqlsanitize(table_name)}" add CONSTRAINT "${sqlsanitize(table_name)}_${field_names
|
|
410
|
+
.map((f) => sqlsanitize(f))
|
|
411
|
+
.join("_")}_unique" UNIQUE (${field_names
|
|
412
|
+
.map((f) => `"${sqlsanitize(f)}"`)
|
|
413
|
+
.join(",")});`;
|
|
414
|
+
sql_log(sql);
|
|
415
|
+
await getMyClient().query(sql);
|
|
416
|
+
};
|
|
417
|
+
/**
|
|
418
|
+
* Drop unique constraint
|
|
419
|
+
* @param {string} table_name - table name
|
|
420
|
+
* @param {string[]} field_names - list of columns (members of constraint)
|
|
421
|
+
* @returns {Promise<void>} no results
|
|
422
|
+
*/
|
|
423
|
+
export const drop_unique_constraint = async (table_name, field_names) => {
|
|
424
|
+
// TBD check that there are no problems with lenght of constraint name
|
|
425
|
+
const sql = `alter table "${getTenantSchema()}"."${sqlsanitize(table_name)}" drop CONSTRAINT IF EXISTS "${sqlsanitize(table_name)}_${field_names
|
|
426
|
+
.map((f) => sqlsanitize(f))
|
|
427
|
+
.join("_")}_unique";`;
|
|
428
|
+
sql_log(sql);
|
|
429
|
+
await getMyClient().query(sql);
|
|
430
|
+
};
|
|
431
|
+
/**
|
|
432
|
+
* Add index
|
|
433
|
+
* @param {string} table_name - table name
|
|
434
|
+
* @param {string} field_name - list of columns (members of constraint)
|
|
435
|
+
* @returns {Promise<void>} no result
|
|
436
|
+
*/
|
|
437
|
+
export const add_index = async (table_name, field_name) => {
|
|
438
|
+
// TBD check that there are no problems with lenght of constraint name
|
|
439
|
+
const sql = `create index "${sqlsanitize(table_name)}_${sqlsanitize(field_name)}_index" on "${getTenantSchema()}"."${sqlsanitize(table_name)}" ("${sqlsanitize(field_name)}");`;
|
|
440
|
+
sql_log(sql);
|
|
441
|
+
await getMyClient().query(sql);
|
|
442
|
+
};
|
|
443
|
+
/**
|
|
444
|
+
* Add Full-text search index
|
|
445
|
+
* @param {string} table_name - table name
|
|
446
|
+
* @param {string} field_name - list of columns (members of constraint)
|
|
447
|
+
* @returns {Promise<void>} no result
|
|
448
|
+
*/
|
|
449
|
+
export const add_fts_index = async (table_name, field_expression, language, disable_fts) => {
|
|
450
|
+
// TBD check that there are no problems with lenght of constraint name
|
|
451
|
+
//CREATE INDEX ON public.test_table USING gist
|
|
452
|
+
// ((name || (cars ->> 'values') || surname) gist_trgm_ops);
|
|
453
|
+
let sql;
|
|
454
|
+
if (disable_fts) {
|
|
455
|
+
await getMyClient().query("CREATE EXTENSION IF NOT EXISTS pg_trgm;");
|
|
456
|
+
sql = `create index "${sqlsanitize(table_name)}_fts_index" on "${getTenantSchema()}"."${sqlsanitize(table_name)}" USING gist ((${field_expression}) gist_trgm_ops);`;
|
|
457
|
+
}
|
|
458
|
+
else
|
|
459
|
+
sql = `create index "${sqlsanitize(table_name)}_fts_index" on "${getTenantSchema()}"."${sqlsanitize(table_name)}" USING GIN (to_tsvector('${language || "english"}', ${field_expression}));`;
|
|
460
|
+
sql_log(sql);
|
|
461
|
+
await getMyClient().query(sql);
|
|
462
|
+
};
|
|
463
|
+
export const drop_fts_index = async (table_name) => {
|
|
464
|
+
// TBD check that there are no problems with lenght of constraint name
|
|
465
|
+
const sql = `drop index "${getTenantSchema()}"."${sqlsanitize(table_name)}_fts_index";`;
|
|
466
|
+
sql_log(sql);
|
|
467
|
+
await getMyClient().query(sql);
|
|
468
|
+
};
|
|
469
|
+
/**
|
|
470
|
+
* Add index
|
|
471
|
+
* @param {string} table_name - table name
|
|
472
|
+
* @param {string} field_name - list of columns (members of constraint)
|
|
473
|
+
* @returns {Promise<void>} no result
|
|
474
|
+
*/
|
|
475
|
+
export const drop_index = async (table_name, field_name) => {
|
|
476
|
+
// TBD check that there are no problems with lenght of constraint name
|
|
477
|
+
const sql = `drop index "${getTenantSchema()}"."${sqlsanitize(table_name)}_${sqlsanitize(field_name)}_index";`;
|
|
478
|
+
sql_log(sql);
|
|
479
|
+
await getMyClient().query(sql);
|
|
480
|
+
};
|
|
481
|
+
/**
|
|
482
|
+
* Copy data from CSV to table?
|
|
483
|
+
* Only for PG
|
|
484
|
+
* @param {object} fileStream - file stream
|
|
485
|
+
* @param {string} tableName - table name
|
|
486
|
+
* @param {string[]} fieldNames - list of columns
|
|
487
|
+
* @param {object} client - db connection
|
|
488
|
+
* @returns {Promise<void>} no results
|
|
489
|
+
*/
|
|
490
|
+
export const copyFrom = async (fileStream, tableName, fieldNames, client) => {
|
|
491
|
+
const sql = `COPY "${getTenantSchema()}"."${sqlsanitize(tableName)}" (${fieldNames.map(quote).join(",")}) FROM STDIN CSV HEADER`;
|
|
492
|
+
sql_log(sql);
|
|
493
|
+
const stream = client.query(copyStreams.from(sql));
|
|
494
|
+
return await pipeline(fileStream, stream);
|
|
495
|
+
};
|
|
496
|
+
export const copyToJson = async (fileStream, tableName, client) => {
|
|
497
|
+
const sql = `COPY (SELECT (row_to_json("${sqlsanitize(tableName)}".*) || ',')
|
|
498
|
+
FROM "${getTenantSchema()}"."${sqlsanitize(tableName)}") TO STDOUT`;
|
|
499
|
+
sql_log(sql);
|
|
500
|
+
const stream = (client || getMyClient()).query(copyStreams.to(sql));
|
|
501
|
+
return await pipeline(stream, replace("\\\\", "\\"), fileStream);
|
|
502
|
+
};
|
|
503
|
+
export const slugify = (s) => s
|
|
504
|
+
.toLowerCase()
|
|
505
|
+
.replace(/\s+/g, "-")
|
|
506
|
+
.replace(/[^\w-]/g, "");
|
|
507
|
+
export const time = async () => {
|
|
508
|
+
const result = await getMyClient().query("select now()");
|
|
509
|
+
const row = result.rows[0];
|
|
510
|
+
return new Date(row.now);
|
|
511
|
+
};
|
|
512
|
+
/**
|
|
513
|
+
*
|
|
514
|
+
* @returns
|
|
515
|
+
*/
|
|
516
|
+
export const listTables = async () => {
|
|
517
|
+
const tq = await getMyClient().query(`SELECT table_name FROM information_schema.tables WHERE table_schema = '${getTenantSchema()}'`);
|
|
518
|
+
return tq.rows.map((row) => {
|
|
519
|
+
return { name: row.table_name };
|
|
520
|
+
});
|
|
521
|
+
};
|
|
522
|
+
/**
|
|
523
|
+
*
|
|
524
|
+
* @returns
|
|
525
|
+
*/
|
|
526
|
+
export const listUserDefinedTables = async () => {
|
|
527
|
+
const tq = await getMyClient().query(`SELECT table_name FROM information_schema.tables WHERE table_schema = '${getTenantSchema()}' AND table_name NOT LIKE '_sc_%'`);
|
|
528
|
+
return tq.rows.map((row) => {
|
|
529
|
+
return { name: row.table_name };
|
|
530
|
+
});
|
|
531
|
+
};
|
|
532
|
+
/**
|
|
533
|
+
*
|
|
534
|
+
* @returns
|
|
535
|
+
*/
|
|
536
|
+
export const listScTables = async () => {
|
|
537
|
+
const tq = await getMyClient().query(`SELECT table_name FROM information_schema.tables WHERE table_schema = '${getTenantSchema()}' AND table_name LIKE '_sc_%'`);
|
|
538
|
+
return tq.rows.map((row) => {
|
|
539
|
+
return { name: row.table_name };
|
|
540
|
+
});
|
|
541
|
+
};
|
|
542
|
+
export const setRequestUserContext = async (client, isLocal = false) => {
|
|
543
|
+
const reqCon = getRequestContext();
|
|
544
|
+
// No request context means an internal/background operation — leave GUC unset
|
|
545
|
+
// so COALESCE(…, 1) in sc_rls_elevated grants it admin-level access intentionally.
|
|
546
|
+
if (!reqCon)
|
|
547
|
+
return;
|
|
548
|
+
const user = reqCon?.req?.user;
|
|
549
|
+
await client.query(`SELECT set_config('app.current_user_id', $1, ${isLocal}), ` +
|
|
550
|
+
`set_config('app.current_user_role', $2, ${isLocal})`, user?.id
|
|
551
|
+
? [String(user.id), String(user.role_id ?? 100)]
|
|
552
|
+
: ["0", "100"]);
|
|
553
|
+
};
|
|
554
|
+
/* rules of using this:
|
|
555
|
+
|
|
556
|
+
- no try catch inside unless you rethrow: wouldnt roll back
|
|
557
|
+
- no state.refresh_*() inside: other works wouldnt see updates as they are in transactioon
|
|
558
|
+
- you can use state.refresh_*(true) for update on own worker only
|
|
559
|
+
|
|
560
|
+
*/
|
|
561
|
+
export const withTransaction = async (f, onError) => {
|
|
562
|
+
const client = await getClient();
|
|
563
|
+
const reqCon = getRequestContext();
|
|
564
|
+
if (reqCon) {
|
|
565
|
+
reqCon.client = client;
|
|
566
|
+
reqCon.inTransaction = true;
|
|
567
|
+
}
|
|
568
|
+
sql_log("BEGIN;");
|
|
569
|
+
await client.query("BEGIN;");
|
|
570
|
+
let aborted = false;
|
|
571
|
+
const rollback = async () => {
|
|
572
|
+
aborted = true;
|
|
573
|
+
sql_log("ROLLBACK;");
|
|
574
|
+
await client.query("ROLLBACK;");
|
|
575
|
+
};
|
|
576
|
+
try {
|
|
577
|
+
await setRequestUserContext(client, true);
|
|
578
|
+
const result = await f(rollback);
|
|
579
|
+
if (!aborted) {
|
|
580
|
+
sql_log("COMMIT;");
|
|
581
|
+
await client.query("COMMIT;");
|
|
582
|
+
}
|
|
583
|
+
return result;
|
|
584
|
+
}
|
|
585
|
+
catch (error) {
|
|
586
|
+
if (!aborted) {
|
|
587
|
+
sql_log("ROLLBACK;");
|
|
588
|
+
await client.query("ROLLBACK;");
|
|
589
|
+
}
|
|
590
|
+
if (onError)
|
|
591
|
+
return onError(error);
|
|
592
|
+
else
|
|
593
|
+
throw error;
|
|
594
|
+
}
|
|
595
|
+
finally {
|
|
596
|
+
if (reqCon) {
|
|
597
|
+
reqCon.client = null;
|
|
598
|
+
reqCon.inTransaction = false;
|
|
599
|
+
}
|
|
600
|
+
client.release();
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
export const commitAndBeginNewTransaction = async () => {
|
|
604
|
+
const client = await getMyClient();
|
|
605
|
+
sql_log("COMMIT;");
|
|
606
|
+
await client.query("COMMIT;");
|
|
607
|
+
sql_log("BEGIN;");
|
|
608
|
+
await client.query("BEGIN;");
|
|
609
|
+
};
|
|
610
|
+
export const tryCatchInTransaction = async (f, onError) => {
|
|
611
|
+
const rndid = Math.floor(Math.random() * 16777215).toString(16);
|
|
612
|
+
const reqCon = getRequestContext();
|
|
613
|
+
if (reqCon?.inTransaction)
|
|
614
|
+
await query(`SAVEPOINT sp${rndid}`);
|
|
615
|
+
try {
|
|
616
|
+
return await f();
|
|
617
|
+
}
|
|
618
|
+
catch (error) {
|
|
619
|
+
if (reqCon?.inTransaction)
|
|
620
|
+
await query(`ROLLBACK TO SAVEPOINT sp${rndid}`);
|
|
621
|
+
if (onError)
|
|
622
|
+
return await onError(error);
|
|
623
|
+
}
|
|
624
|
+
finally {
|
|
625
|
+
if (reqCon?.inTransaction)
|
|
626
|
+
await query(`RELEASE SAVEPOINT sp${rndid}`);
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
/**
|
|
630
|
+
* Should be used for code that is sometimes called from within a withTransaction block
|
|
631
|
+
* and sometimes not.
|
|
632
|
+
* @param {Function} f logic to execute
|
|
633
|
+
* @param {Function} onError error handler
|
|
634
|
+
* @returns
|
|
635
|
+
*/
|
|
636
|
+
export const openOrUseTransaction = async (f, onError) => {
|
|
637
|
+
const reqCon = getRequestContext();
|
|
638
|
+
if (reqCon?.inTransaction)
|
|
639
|
+
return await f();
|
|
640
|
+
else
|
|
641
|
+
return await withTransaction(f, onError);
|
|
642
|
+
};
|
|
643
|
+
/**
|
|
644
|
+
* Wait some time until current transaction COMMITs,
|
|
645
|
+
* then open another transaction.
|
|
646
|
+
* @param {Function} f logic to execute
|
|
647
|
+
* @param {Function} onError error handler
|
|
648
|
+
* @returns
|
|
649
|
+
*/
|
|
650
|
+
export const whenTransactionisFree = (f, onError) => {
|
|
651
|
+
return new Promise((resolve, reject) => {
|
|
652
|
+
// wait until transaction is free
|
|
653
|
+
let counter = 0;
|
|
654
|
+
const interval = setInterval(async () => {
|
|
655
|
+
const reqCon = getRequestContext();
|
|
656
|
+
if (!reqCon?.inTransaction) {
|
|
657
|
+
clearInterval(interval);
|
|
658
|
+
try {
|
|
659
|
+
resolve(await withTransaction(f, onError));
|
|
660
|
+
}
|
|
661
|
+
catch (e) {
|
|
662
|
+
reject(e);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
if (++counter > 100) {
|
|
666
|
+
clearInterval(interval);
|
|
667
|
+
reject(new Error("Timeout waiting for transaction to be free"));
|
|
668
|
+
}
|
|
669
|
+
}, 200);
|
|
670
|
+
});
|
|
671
|
+
};
|
|
672
|
+
export const query = (text, params) => {
|
|
673
|
+
sql_log(text, params);
|
|
674
|
+
return getMyClient().query(text, params);
|
|
675
|
+
};
|
|
676
|
+
export { mkWhere };
|
|
677
|
+
export const driverName = "postgres";
|
|
678
|
+
export const array_agg_sql_fn = "array_agg";
|
|
679
|
+
export const serial_pk_sql_type = "serial";
|
|
680
|
+
export const json_sql_type = "jsonb";
|
|
681
|
+
export const indexable_text_sql_type = "text";
|
|
682
|
+
export const supports_search_path = true;
|
|
683
|
+
/**
|
|
684
|
+
* Initializes internals of the the postgres module.
|
|
685
|
+
* It must be called after importing the module.
|
|
686
|
+
* @param getConnectObjectPara function returning the connection object
|
|
687
|
+
*/
|
|
688
|
+
export const init = (getConnectObjectPara) => {
|
|
689
|
+
if (!pool) {
|
|
690
|
+
getConnectObject = getConnectObjectPara;
|
|
691
|
+
const connectObj = getConnectObject();
|
|
692
|
+
if (connectObj) {
|
|
693
|
+
pool = new Pool(connectObj);
|
|
694
|
+
getTenantSchema = tenantsModule(connectObj).getTenantSchema;
|
|
695
|
+
getRequestContext = tenantsModule(connectObj).getRequestContext;
|
|
696
|
+
}
|
|
697
|
+
else {
|
|
698
|
+
throw new Error("Unable to retrieve a database connection object.");
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
//# sourceMappingURL=postgres.js.map
|