@saltcorn/postgres 1.6.3-beta.1 → 1.7.0-alpha.1

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,824 @@
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 sql_backend_display_name = "PostgreSQL";
679
+ export const array_agg_sql_fn = "array_agg";
680
+ export const serial_pk_sql_type = "serial";
681
+ export const json_sql_type = "jsonb";
682
+ export const indexable_text_sql_type = "text";
683
+ export const timestamp_sql_type = "timestamptz";
684
+ export const millis_timestamp_sql_type = "timestamp";
685
+ export const supports_search_path = true;
686
+ // Backend capability flags (see DbExportsType in db-common/types)
687
+ export const supports_multiple_schemas = true;
688
+ export const pools_connections = true;
689
+ export const supports_for_update = true;
690
+ export const supports_row_level_security = true;
691
+ export const supports_alter_table = true;
692
+ export const supports_non_integer_pk = true;
693
+ export const json_read_returns_string = false;
694
+ export const json_write_needs_stringify = true;
695
+ export const stores_dates_as_text = false;
696
+ export const supports_large_bind_lists = true;
697
+ export const supports_database_views = true;
698
+ export const supports_table_discovery = true;
699
+ export const supports_session_pruning = true;
700
+ export const supports_agg_order_by = true;
701
+ export const coerce_numeric_aggregates_to_string = false;
702
+ export const uses_positional_placeholders = false;
703
+ export const coerce_read_dates = false;
704
+ export const epochToTimestampSql = (param) => `to_timestamp(${param})`;
705
+ export const truncateMillisSql = (expr) => `date_trunc('milliseconds', ${expr})`;
706
+ export const castExprToTextSql = (expr) => `${expr}::text`;
707
+ export const castBindParamSql = (param, sqlType) => `${param}::${sqlType}`;
708
+ export const timeDiffWithinSql = (startExpr, endExpr, seconds) => `${endExpr} - ${startExpr} <= INTERVAL '${seconds} seconds'`;
709
+ export const isDistinctFromSql = (a, b) => `${a} IS DISTINCT FROM ${b}`;
710
+ export const wrapDeleteSubselect = (subquery) => subquery;
711
+ export const text_requires_length_for_index = false;
712
+ export const setColumnNullabilitySql = (qTable, qCol, _sqlType, notNull) => `alter table ${qTable} alter column ${qCol} ${notNull ? "set" : "drop"} not null;`;
713
+ export const alterColumnTypeSql = (qTable, qCol, newType, using, def) => `alter table ${qTable} alter column ${qCol} TYPE ${newType} ${using} ${def};`;
714
+ export const changePkColumnTypeStatements = (qTable, qCol, newType, def, _wasAlreadyPk) => [
715
+ `ALTER TABLE ${qTable} drop column ${qCol};`,
716
+ `ALTER TABLE ${qTable} add column ${qCol} ${newType} primary key ${def};`,
717
+ ];
718
+ // foreign-key DDL (see DbExportsType in db-common/types)
719
+ export const inline_fk_in_column_type = true;
720
+ export const fk_must_be_dropped_before_column = false;
721
+ export const fk_deferrable_clause = " DEFERRABLE";
722
+ export const dropForeignKeyIfExists = async (qTable, conName) => {
723
+ await query(`ALTER TABLE ${qTable} drop constraint if exists "${conName}"`);
724
+ };
725
+ export const replaceForeignKey = async (params) => {
726
+ const { qTable, oldConName, newConName, colName, qRefTable, refCol, onDelete } = params;
727
+ await query(`ALTER TABLE ${qTable} drop constraint "${oldConName}", add constraint "${newConName}" foreign key ("${colName}") references ${qRefTable}("${refCol}")${onDelete}${fk_deferrable_clause}`);
728
+ };
729
+ export const addColumnWithDefault = async (params) => {
730
+ const { qTable, colName, sqlType, bareType, required, defaultValue } = params;
731
+ const cn = sqlsanitize(colName);
732
+ await query(`DROP FUNCTION IF EXISTS add_field_${cn};
733
+ CREATE FUNCTION add_field_${cn}(thedef ${bareType}) RETURNS void AS $$
734
+ BEGIN
735
+ EXECUTE format('alter table ${qTable} add column "${cn}" ${sqlType} ${required ? "not null" : ""} default %L', thedef);
736
+ END;
737
+ $$ LANGUAGE plpgsql;`);
738
+ await query(`SELECT add_field_${cn}($1)`, [defaultValue]);
739
+ };
740
+ export const isDuplicateForeignKeyError = (_e) => false;
741
+ // misc dialect-specific SQL / behaviour (see DbExportsType)
742
+ export const jsonMergeExpr = (colExpr, param) => `coalesce(${colExpr}, '{}'::jsonb) || ${param}::jsonb`;
743
+ export const supports_array_param_in = true;
744
+ export const deleteSessionsForUser = async (userId, tenant) => {
745
+ await query(`delete from _sc_session
746
+ where sess->'passport'->'user'->>'id' = $1
747
+ and sess->'passport'->'user'->>'tenant' = $2`, [userId, tenant]);
748
+ };
749
+ export const deleteSessionsForTenant = async (tenant) => {
750
+ await query(`delete from _sc_session
751
+ where sess->'passport'->'user'->>'tenant' = $1`, [tenant]);
752
+ };
753
+ export const dropCheckConstraintIfExists = async (qTable, conName) => {
754
+ await query(`alter table ${qTable} drop constraint IF EXISTS "${conName}";`);
755
+ };
756
+ export const migration_sql_dialect = "sql_pg";
757
+ export const migration_translates_from_pg = false;
758
+ export const discovery_keys_uppercase = false;
759
+ export const discovery_columns_order_by = "";
760
+ export const discovery_pk_sql = `SELECT c.column_name, c.column_default
761
+ FROM information_schema.table_constraints tc
762
+ JOIN information_schema.constraint_column_usage AS ccu USING (constraint_schema, constraint_name)
763
+ JOIN information_schema.columns AS c ON c.table_schema = tc.constraint_schema
764
+ AND tc.table_name = c.table_name AND ccu.column_name = c.column_name
765
+ WHERE constraint_type = 'PRIMARY KEY' and tc.table_schema=$1 and tc.table_name = $2;`;
766
+ export const discovery_fk_sql = `SELECT
767
+ tc.table_schema,
768
+ tc.constraint_name,
769
+ tc.table_name,
770
+ kcu.column_name,
771
+ ccu.table_schema AS foreign_table_schema,
772
+ ccu.table_name AS foreign_table_name,
773
+ ccu.column_name AS foreign_column_name
774
+ FROM
775
+ information_schema.table_constraints AS tc
776
+ JOIN information_schema.key_column_usage AS kcu
777
+ ON tc.constraint_name = kcu.constraint_name
778
+ AND tc.table_schema = kcu.table_schema
779
+ JOIN information_schema.constraint_column_usage AS ccu
780
+ ON ccu.constraint_name = tc.constraint_name
781
+ AND ccu.table_schema = tc.table_schema
782
+ WHERE tc.constraint_type = 'FOREIGN KEY' and tc.table_schema=$1 AND tc.table_name=$2;`;
783
+ export const dropPrimaryKey = async (schemaPrefix, tableName, tenantSchema) => {
784
+ const { rows } = await query(`select constraint_name
785
+ from information_schema.table_constraints
786
+ where table_schema = '${tenantSchema || "public"}'
787
+ and table_name = '${tableName}'
788
+ and constraint_type = 'PRIMARY KEY';`);
789
+ const cname = rows[0]?.constraint_name;
790
+ await query(`alter table ${schemaPrefix}"${tableName}" drop constraint "${cname}"`);
791
+ };
792
+ // Defer FK checks for the remainder of the current transaction.
793
+ export const deferForeignKeys = async (client) => {
794
+ await client.query("SET CONSTRAINTS ALL DEFERRED");
795
+ };
796
+ // Pull the offending field name out of a unique-violation error message.
797
+ // e.g. `duplicate key value violates unique constraint "books_author_unique"`
798
+ export const parseUniqueConstraintError = (msg, _tableName) => {
799
+ const m = msg.match(/duplicate key value violates unique constraint "(.*?)_(.*?)_unique"/);
800
+ return m ? m[2] : "";
801
+ };
802
+ // Canonical key for a multi-field unique constraint, to compare against the
803
+ // field name parsed above (postgres names them `<field1>_<field2>_...`).
804
+ export const uniqueConstraintFieldsKey = (fields, _tableName) => fields.join("_");
805
+ /**
806
+ * Initializes internals of the the postgres module.
807
+ * It must be called after importing the module.
808
+ * @param getConnectObjectPara function returning the connection object
809
+ */
810
+ export const init = (getConnectObjectPara) => {
811
+ if (!pool) {
812
+ getConnectObject = getConnectObjectPara;
813
+ const connectObj = getConnectObject();
814
+ if (connectObj) {
815
+ pool = new Pool(connectObj);
816
+ getTenantSchema = tenantsModule(connectObj).getTenantSchema;
817
+ getRequestContext = tenantsModule(connectObj).getRequestContext;
818
+ }
819
+ else {
820
+ throw new Error("Unable to retrieve a database connection object.");
821
+ }
822
+ }
823
+ };
824
+ //# sourceMappingURL=postgres.js.map