@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.
@@ -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