@devopsplaybook.io/common-utils 1.10.1 → 1.11.0-beta.24.d8f9aef
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/README.md +73 -27
- package/dist/src/ConfigBase.d.ts +31 -0
- package/dist/src/ConfigBase.js +58 -16
- package/dist/src/DbUtils.d.ts +20 -3
- package/dist/src/DbUtils.js +93 -2
- package/dist/src/DbUtilsNoTelemetry.d.ts +4 -1
- package/dist/src/DbUtilsNoTelemetry.js +62 -6
- package/dist/src/PostgresDbUtils.d.ts +41 -10
- package/dist/src/PostgresDbUtils.js +357 -304
- package/dist/src/SqlDbUtils.d.ts +12 -4
- package/dist/src/SqlDbUtils.js +76 -30
- package/dist/src/users/Auth.d.ts +11 -1
- package/dist/src/users/Auth.js +160 -44
- package/dist/src/users/User.d.ts +10 -0
- package/dist/src/users/User.js +30 -10
- package/dist/src/users/UserApiToken.d.ts +4 -0
- package/dist/src/users/UserApiToken.js +11 -0
- package/dist/src/users/UsersApiTokensData.d.ts +12 -0
- package/dist/src/users/UsersApiTokensData.js +130 -33
- package/dist/src/users/UsersData.d.ts +20 -1
- package/dist/src/users/UsersData.js +150 -52
- package/dist/src/users/UsersRoutes.js +178 -61
- package/dist/src/users/index.d.ts +8 -0
- package/dist/src/users/index.js +24 -0
- package/package.json +58 -1
- package/.github/workflows/main-build.yml +0 -18
- package/.github/workflows/pr-check.yml +0 -27
- package/.github/workflows/reusable-merge-build.yml +0 -197
- package/.github/workflows/reusable-npm-merge.yml +0 -135
- package/.github/workflows/reusable-npm-pr.yml +0 -183
- package/.github/workflows/reusable-npm-upgrade.yml +0 -92
- package/.github/workflows/reusable-pr-verify.yml +0 -181
- package/AGENTS.md +0 -105
- package/index.ts +0 -18
- package/jest.config.js +0 -17
- package/prettierrc.json +0 -5
- package/src/ConfigBase.spec.ts +0 -108
- package/src/ConfigBase.ts +0 -297
- package/src/DbUtils.spec.ts +0 -23
- package/src/DbUtils.ts +0 -116
- package/src/DbUtilsNoTelemetry.spec.ts +0 -168
- package/src/DbUtilsNoTelemetry.ts +0 -117
- package/src/LLM.spec.ts +0 -303
- package/src/LLM.ts +0 -204
- package/src/Notifications.spec.ts +0 -265
- package/src/Notifications.ts +0 -201
- package/src/OTelContext.spec.ts +0 -58
- package/src/OTelContext.ts +0 -63
- package/src/PostgresDbUtils.spec.ts +0 -153
- package/src/PostgresDbUtils.ts +0 -666
- package/src/SqlDbUtils.spec.ts +0 -108
- package/src/SqlDbUtils.ts +0 -152
- package/src/SystemCommand.spec.ts +0 -18
- package/src/SystemCommand.ts +0 -23
- package/src/Timeout.spec.ts +0 -18
- package/src/Timeout.ts +0 -12
- package/src/users/Auth.spec.ts +0 -268
- package/src/users/Auth.ts +0 -202
- package/src/users/User.ts +0 -75
- package/src/users/UserApiToken.ts +0 -55
- package/src/users/UserPassword.spec.ts +0 -28
- package/src/users/UserPassword.ts +0 -20
- package/src/users/UserSession.ts +0 -9
- package/src/users/UsersApiTokensData.spec.ts +0 -158
- package/src/users/UsersApiTokensData.ts +0 -125
- package/src/users/UsersData.ts +0 -141
- package/src/users/UsersRoutes.ts +0 -374
- package/tsconfig.json +0 -15
- package/tsconfig.spec.json +0 -8
|
@@ -33,9 +33,10 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.PostgresSchemaDbUtils = void 0;
|
|
36
|
+
exports.PostgresSchemaDbUtils = exports.POSTGRES_LOCK_KEYS = void 0;
|
|
37
37
|
exports.PostgresDbUtilsSetOTel = PostgresDbUtilsSetOTel;
|
|
38
38
|
exports.PostgresDbUtilsInit = PostgresDbUtilsInit;
|
|
39
|
+
exports.PostgresDbUtilsWithAdvisoryLock = PostgresDbUtilsWithAdvisoryLock;
|
|
39
40
|
exports.PostgresDbUtilsGetPool = PostgresDbUtilsGetPool;
|
|
40
41
|
exports.PostgresDbUtilsExecSQL = PostgresDbUtilsExecSQL;
|
|
41
42
|
exports.PostgresDbUtilsExecSQLFile = PostgresDbUtilsExecSQLFile;
|
|
@@ -45,6 +46,11 @@ exports.PostgresDbUtilsTransactionCommit = PostgresDbUtilsTransactionCommit;
|
|
|
45
46
|
const pg_1 = require("pg");
|
|
46
47
|
const fs = __importStar(require("fs-extra"));
|
|
47
48
|
const api_1 = require("@opentelemetry/api");
|
|
49
|
+
exports.POSTGRES_LOCK_KEYS = {
|
|
50
|
+
migration: 0x6d696772, // "migr"
|
|
51
|
+
auth_token: 0x61757468, // "auth"
|
|
52
|
+
users_bootstrap: 0x75736572, // "user"
|
|
53
|
+
};
|
|
48
54
|
// ---------------------------------------------------------------------------
|
|
49
55
|
// Module-level state
|
|
50
56
|
// ---------------------------------------------------------------------------
|
|
@@ -53,6 +59,115 @@ let tracer;
|
|
|
53
59
|
let logger;
|
|
54
60
|
let standardLogger;
|
|
55
61
|
// ---------------------------------------------------------------------------
|
|
62
|
+
// Internal helpers
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
/** Build the connection options shared by every pool. */
|
|
65
|
+
function buildPoolConfig(config, searchPath, max, keepAlive) {
|
|
66
|
+
const poolConfig = {
|
|
67
|
+
host: config.DATABASE_POSTGRES_HOST,
|
|
68
|
+
port: config.DATABASE_POSTGRES_PORT || 5432,
|
|
69
|
+
user: config.DATABASE_POSTGRES_USER,
|
|
70
|
+
password: config.DATABASE_POSTGRES_PASSWORD,
|
|
71
|
+
database: config.DATABASE_POSTGRES_DATABASE,
|
|
72
|
+
max,
|
|
73
|
+
idleTimeoutMillis: 30000,
|
|
74
|
+
connectionTimeoutMillis: 10000,
|
|
75
|
+
keepAlive,
|
|
76
|
+
};
|
|
77
|
+
if (searchPath) {
|
|
78
|
+
poolConfig.options = `-c search_path=${searchPath}`;
|
|
79
|
+
}
|
|
80
|
+
if (config.DATABASE_POSTGRES_STATEMENT_TIMEOUT_MS) {
|
|
81
|
+
poolConfig.statement_timeout = config.DATABASE_POSTGRES_STATEMENT_TIMEOUT_MS;
|
|
82
|
+
}
|
|
83
|
+
if (config.DATABASE_POSTGRES_IDLE_IN_TRANSACTION_TIMEOUT_MS) {
|
|
84
|
+
poolConfig.idle_in_transaction_session_timeout =
|
|
85
|
+
config.DATABASE_POSTGRES_IDLE_IN_TRANSACTION_TIMEOUT_MS;
|
|
86
|
+
}
|
|
87
|
+
return poolConfig;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Run a callback while holding a session-level advisory lock on the given
|
|
91
|
+
* client. Replicas booting concurrently serialise on the lock instead of
|
|
92
|
+
* applying the same migration twice.
|
|
93
|
+
*/
|
|
94
|
+
async function withAdvisoryLockOnClient(client, lockKey, callback) {
|
|
95
|
+
await client.query("SELECT pg_advisory_lock($1)", [lockKey]);
|
|
96
|
+
try {
|
|
97
|
+
return await callback();
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
await client.query("SELECT pg_advisory_unlock($1)", [lockKey]).catch(() => {
|
|
101
|
+
// The connection may already be broken; the lock dies with it.
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/** Run one query on a dedicated client with its own span. */
|
|
106
|
+
async function queryOnClient(client, context, spanName, sql, params = []) {
|
|
107
|
+
const span = tracer.startSpan(spanName, context);
|
|
108
|
+
try {
|
|
109
|
+
const result = await client.query(sql, params);
|
|
110
|
+
return result.rows;
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
span.setStatus({
|
|
114
|
+
code: api_1.SpanStatusCode.ERROR,
|
|
115
|
+
message: error.message,
|
|
116
|
+
});
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
span.end();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Apply one SQL file on a dedicated client inside a transaction, together
|
|
125
|
+
* with its `metadata` version row when a version is given. On failure the
|
|
126
|
+
* transaction is rolled back: no partial state, no version row.
|
|
127
|
+
*
|
|
128
|
+
* Migration files must not contain their own transaction control statements.
|
|
129
|
+
*/
|
|
130
|
+
async function applyMigrationOnClient(client, context, filename, version) {
|
|
131
|
+
const span = tracer.startSpan("PostgresDbUtilsExecSQLFile", context);
|
|
132
|
+
try {
|
|
133
|
+
const sql = (await fs.readFile(filename)).toString();
|
|
134
|
+
await client.query("BEGIN");
|
|
135
|
+
try {
|
|
136
|
+
await client.query(sql);
|
|
137
|
+
if (version !== null) {
|
|
138
|
+
await client.query('INSERT INTO metadata ("type", "value", "dateCreated") VALUES ($1, $2, $3)', ["db_version", version, new Date().toISOString()]);
|
|
139
|
+
}
|
|
140
|
+
await client.query("COMMIT");
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
await client.query("ROLLBACK").catch(() => {
|
|
144
|
+
// Connection failure: nothing to roll back.
|
|
145
|
+
});
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
span.setStatus({
|
|
151
|
+
code: api_1.SpanStatusCode.ERROR,
|
|
152
|
+
message: error.message,
|
|
153
|
+
});
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
span.end();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/** Read the highest applied migration version (numeric, not lexicographic). */
|
|
161
|
+
async function readAppliedVersion(client, context, spanName) {
|
|
162
|
+
// `metadata.value` has text affinity: a plain MAX(value) is lexicographic and
|
|
163
|
+
// ranks "9" above "10", which re-applies init-0010.sql on every boot.
|
|
164
|
+
const rows = await queryOnClient(client, context, spanName, "SELECT MAX(CAST(value AS INTEGER)) as version FROM metadata WHERE \"type\" = 'db_version'");
|
|
165
|
+
if (rows.length > 0 && rows[0].version !== null && rows[0].version !== undefined) {
|
|
166
|
+
return Number(rows[0].version);
|
|
167
|
+
}
|
|
168
|
+
return 0;
|
|
169
|
+
}
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
56
171
|
// Class-based API – supports per-schema pools + shared runtime pool
|
|
57
172
|
// ---------------------------------------------------------------------------
|
|
58
173
|
/**
|
|
@@ -83,55 +198,50 @@ class PostgresSchemaDbUtils {
|
|
|
83
198
|
*/
|
|
84
199
|
async initSchema(context, config, sqlDir) {
|
|
85
200
|
const span = tracer.startSpan("PostgresSchemaDbUtilsInit", context);
|
|
86
|
-
const poolOptions = {
|
|
87
|
-
host: config.DATABASE_POSTGRES_HOST,
|
|
88
|
-
port: config.DATABASE_POSTGRES_PORT || 5432,
|
|
89
|
-
user: config.DATABASE_POSTGRES_USER,
|
|
90
|
-
password: config.DATABASE_POSTGRES_PASSWORD,
|
|
91
|
-
database: config.DATABASE_POSTGRES_DATABASE,
|
|
92
|
-
options: `-c search_path=${this.schemaName}`,
|
|
93
|
-
max: 5,
|
|
94
|
-
idleTimeoutMillis: 30000,
|
|
95
|
-
connectionTimeoutMillis: 10000,
|
|
96
|
-
};
|
|
97
|
-
if (this.schemaPool) {
|
|
98
|
-
this.moduleLogger.info("Closing existing schema pool");
|
|
99
|
-
await this.schemaPool.end().catch(() => {
|
|
100
|
-
// Ignore errors on close
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
this.schemaPool = new pg_1.Pool(poolOptions);
|
|
104
|
-
this.moduleLogger.info(`Schema pool initialized with search_path: ${this.schemaName}`);
|
|
105
|
-
// Create schema if not exists
|
|
106
|
-
await this.execSQLForSchema(span, `CREATE SCHEMA IF NOT EXISTS ${this.schemaName};`);
|
|
107
|
-
await this.execSQLForSchema(span, `SET search_path TO ${this.schemaName};`);
|
|
108
|
-
// Run init SQL files
|
|
109
|
-
await this.execSQLFileForSchema(span, `${sqlDir}/init-0000.sql`);
|
|
110
|
-
const initFiles = (await fs.readdir(sqlDir)).sort();
|
|
111
|
-
let dbVersionApplied = 0;
|
|
112
201
|
try {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
202
|
+
if (this.schemaPool) {
|
|
203
|
+
this.moduleLogger.info("Closing existing schema pool");
|
|
204
|
+
await this.schemaPool.end().catch(() => {
|
|
205
|
+
// Ignore errors on close
|
|
206
|
+
});
|
|
116
207
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
208
|
+
const schemaPool = new pg_1.Pool(buildPoolConfig(config, this.schemaName, 5, false));
|
|
209
|
+
this.schemaPool = schemaPool;
|
|
210
|
+
this.moduleLogger.info(`Schema pool initialized with search_path: ${this.schemaName}`);
|
|
211
|
+
const client = await schemaPool.connect();
|
|
212
|
+
try {
|
|
213
|
+
await withAdvisoryLockOnClient(client, exports.POSTGRES_LOCK_KEYS.migration, async () => {
|
|
214
|
+
await queryOnClient(client, span, "PostgresSchemaDbUtilsExecSQLForSchema", `CREATE SCHEMA IF NOT EXISTS ${this.schemaName};`);
|
|
215
|
+
await queryOnClient(client, span, "PostgresSchemaDbUtilsExecSQLForSchema", `SET search_path TO ${this.schemaName};`);
|
|
216
|
+
await applyMigrationOnClient(client, span, `${sqlDir}/init-0000.sql`, null);
|
|
217
|
+
let dbVersionApplied = 0;
|
|
218
|
+
try {
|
|
219
|
+
dbVersionApplied = await readAppliedVersion(client, span, "PostgresSchemaDbUtilsQuerySQLForSchema");
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
// The metadata table might not exist yet
|
|
223
|
+
}
|
|
224
|
+
this.moduleLogger.info(`Current DB Version: ${dbVersionApplied}`);
|
|
225
|
+
const initFiles = (await fs.readdir(sqlDir)).sort();
|
|
226
|
+
for (const initFile of initFiles) {
|
|
227
|
+
const match = /init-(\d+)\.sql/.exec(initFile);
|
|
228
|
+
if (match) {
|
|
229
|
+
const dbVersionInitFile = Number(match[1]);
|
|
230
|
+
if (dbVersionInitFile > dbVersionApplied) {
|
|
231
|
+
this.moduleLogger.info(`Applying migration: ${initFile}`);
|
|
232
|
+
await applyMigrationOnClient(client, span, `${sqlDir}/${initFile}`, dbVersionInitFile);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
finally {
|
|
239
|
+
client.release();
|
|
132
240
|
}
|
|
133
241
|
}
|
|
134
|
-
|
|
242
|
+
finally {
|
|
243
|
+
span.end();
|
|
244
|
+
}
|
|
135
245
|
}
|
|
136
246
|
/**
|
|
137
247
|
* Initialise (or replace) the shared runtime pool.
|
|
@@ -144,20 +254,7 @@ class PostgresSchemaDbUtils {
|
|
|
144
254
|
// Ignore errors on close
|
|
145
255
|
});
|
|
146
256
|
}
|
|
147
|
-
this.runtimePool = new pg_1.Pool(
|
|
148
|
-
host: config.DATABASE_POSTGRES_HOST,
|
|
149
|
-
port: config.DATABASE_POSTGRES_PORT || 5432,
|
|
150
|
-
user: config.DATABASE_POSTGRES_USER,
|
|
151
|
-
password: config.DATABASE_POSTGRES_PASSWORD,
|
|
152
|
-
database: config.DATABASE_POSTGRES_DATABASE,
|
|
153
|
-
options: searchPath
|
|
154
|
-
? `-c search_path=${searchPath}`
|
|
155
|
-
: `-c search_path=${this.schemaName}`,
|
|
156
|
-
max: 20,
|
|
157
|
-
idleTimeoutMillis: 30000,
|
|
158
|
-
connectionTimeoutMillis: 10000,
|
|
159
|
-
keepAlive: true,
|
|
160
|
-
});
|
|
257
|
+
this.runtimePool = new pg_1.Pool(buildPoolConfig(config, searchPath || this.schemaName, 20, true));
|
|
161
258
|
this.moduleLogger.info(`Runtime pool initialized (search_path: ${searchPath || this.schemaName})`);
|
|
162
259
|
}
|
|
163
260
|
/**
|
|
@@ -166,104 +263,117 @@ class PostgresSchemaDbUtils {
|
|
|
166
263
|
* otherwise use the runtime pool (default).
|
|
167
264
|
* @returns Number of rows changed.
|
|
168
265
|
*/
|
|
169
|
-
execSQL(context, sql, params = [], useSchemaPool = false) {
|
|
266
|
+
async execSQL(context, sql, params = [], useSchemaPool = false) {
|
|
170
267
|
const span = tracer.startSpan("PostgresSchemaDbUtilsExecSQL", context);
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
268
|
+
let targetPool = null;
|
|
269
|
+
try {
|
|
270
|
+
targetPool = useSchemaPool ? this.schemaPool : this.runtimePool;
|
|
271
|
+
if (!targetPool) {
|
|
272
|
+
throw new Error(`Pool not initialized${useSchemaPool ? ` for schema: ${this.schemaName}` : ""}`);
|
|
273
|
+
}
|
|
274
|
+
const result = await targetPool.query(sql, params);
|
|
275
|
+
return result.rowCount || 0;
|
|
174
276
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
span.setStatus({
|
|
180
|
-
code: api_1.SpanStatusCode.ERROR,
|
|
181
|
-
message: error.message,
|
|
182
|
-
});
|
|
183
|
-
this.moduleLogger.error(`[${useSchemaPool ? this.schemaName : "RUNTIME"}] SQL EXEC ERROR: ${sql}`, error);
|
|
184
|
-
reject(error);
|
|
185
|
-
}
|
|
186
|
-
else {
|
|
187
|
-
resolve(result.rowCount || 0);
|
|
188
|
-
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
span.setStatus({
|
|
279
|
+
code: api_1.SpanStatusCode.ERROR,
|
|
280
|
+
message: error.message,
|
|
189
281
|
});
|
|
190
|
-
|
|
282
|
+
if (targetPool) {
|
|
283
|
+
this.moduleLogger.error(`[${useSchemaPool ? this.schemaName : "RUNTIME"}] SQL EXEC ERROR: ${sql}`, error);
|
|
284
|
+
}
|
|
285
|
+
throw error;
|
|
286
|
+
}
|
|
287
|
+
finally {
|
|
288
|
+
span.end();
|
|
289
|
+
}
|
|
191
290
|
}
|
|
192
|
-
/**
|
|
291
|
+
/**
|
|
292
|
+
* Execute an entire SQL file (used for migrations).
|
|
293
|
+
* Migration files must not contain their own transaction control statements.
|
|
294
|
+
*/
|
|
193
295
|
async execSQLFile(context, filename, useSchemaPool = false) {
|
|
194
296
|
const span = tracer.startSpan("PostgresSchemaDbUtilsExecSQLFile", context);
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
297
|
+
let targetPool = null;
|
|
298
|
+
try {
|
|
299
|
+
targetPool = useSchemaPool ? this.schemaPool : this.runtimePool;
|
|
300
|
+
if (!targetPool) {
|
|
301
|
+
throw new Error(`Pool not initialized${useSchemaPool ? ` for schema: ${this.schemaName}` : ""}`);
|
|
302
|
+
}
|
|
303
|
+
const sql = (await fs.readFile(filename)).toString();
|
|
304
|
+
await targetPool.query(sql);
|
|
199
305
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
span.setStatus({
|
|
205
|
-
code: api_1.SpanStatusCode.ERROR,
|
|
206
|
-
message: error.message,
|
|
207
|
-
});
|
|
208
|
-
reject(error);
|
|
209
|
-
}
|
|
210
|
-
else {
|
|
211
|
-
resolve();
|
|
212
|
-
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
span.setStatus({
|
|
308
|
+
code: api_1.SpanStatusCode.ERROR,
|
|
309
|
+
message: error.message,
|
|
213
310
|
});
|
|
214
|
-
|
|
311
|
+
throw error;
|
|
312
|
+
}
|
|
313
|
+
finally {
|
|
314
|
+
span.end();
|
|
315
|
+
}
|
|
215
316
|
}
|
|
216
317
|
/**
|
|
217
318
|
* Execute a read SQL query with OTel tracing.
|
|
218
319
|
* @returns Array of row objects.
|
|
219
320
|
*/
|
|
220
|
-
querySQL(context, sql, params = [], useSchemaPool = false) {
|
|
321
|
+
async querySQL(context, sql, params = [], useSchemaPool = false) {
|
|
221
322
|
const span = tracer.startSpan("PostgresSchemaDbUtilsQuerySQL", context);
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
323
|
+
let targetPool = null;
|
|
324
|
+
try {
|
|
325
|
+
targetPool = useSchemaPool ? this.schemaPool : this.runtimePool;
|
|
326
|
+
if (!targetPool) {
|
|
327
|
+
throw new Error(`Pool not initialized${useSchemaPool ? ` for schema: ${this.schemaName}` : ""}`);
|
|
328
|
+
}
|
|
329
|
+
const result = await targetPool.query(sql, params);
|
|
330
|
+
return result.rows;
|
|
225
331
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
span.setStatus({
|
|
231
|
-
code: api_1.SpanStatusCode.ERROR,
|
|
232
|
-
message: error.message,
|
|
233
|
-
});
|
|
234
|
-
this.moduleLogger.error(`[${useSchemaPool ? this.schemaName : "RUNTIME"}] SQL QUERY ERROR: ${sql}`, error);
|
|
235
|
-
reject(error);
|
|
236
|
-
}
|
|
237
|
-
else {
|
|
238
|
-
resolve(result.rows);
|
|
239
|
-
}
|
|
332
|
+
catch (error) {
|
|
333
|
+
span.setStatus({
|
|
334
|
+
code: api_1.SpanStatusCode.ERROR,
|
|
335
|
+
message: error.message,
|
|
240
336
|
});
|
|
241
|
-
|
|
337
|
+
if (targetPool) {
|
|
338
|
+
this.moduleLogger.error(`[${useSchemaPool ? this.schemaName : "RUNTIME"}] SQL QUERY ERROR: ${sql}`, error);
|
|
339
|
+
}
|
|
340
|
+
throw error;
|
|
341
|
+
}
|
|
342
|
+
finally {
|
|
343
|
+
span.end();
|
|
344
|
+
}
|
|
242
345
|
}
|
|
243
346
|
/**
|
|
244
347
|
* Run a callback inside a transaction.
|
|
245
348
|
*/
|
|
246
349
|
async transaction(context, callback, useSchemaPool = false) {
|
|
247
350
|
const span = tracer.startSpan("PostgresSchemaDbUtilsTransaction", context);
|
|
248
|
-
|
|
249
|
-
if (!pool) {
|
|
250
|
-
throw new Error(`Pool not initialized${useSchemaPool ? ` for schema: ${this.schemaName}` : ""}`);
|
|
251
|
-
}
|
|
252
|
-
this.moduleLogger.info(`[${useSchemaPool ? this.schemaName : "RUNTIME"}] Starting transaction`);
|
|
253
|
-
const client = await pool.connect();
|
|
351
|
+
let client = null;
|
|
254
352
|
try {
|
|
353
|
+
const pool = useSchemaPool ? this.schemaPool : this.runtimePool;
|
|
354
|
+
if (!pool) {
|
|
355
|
+
throw new Error(`Pool not initialized${useSchemaPool ? ` for schema: ${this.schemaName}` : ""}`);
|
|
356
|
+
}
|
|
357
|
+
this.moduleLogger.info(`[${useSchemaPool ? this.schemaName : "RUNTIME"}] Starting transaction`);
|
|
358
|
+
client = await pool.connect();
|
|
255
359
|
await client.query("BEGIN");
|
|
256
360
|
await callback(client);
|
|
257
361
|
await client.query("COMMIT");
|
|
258
362
|
this.moduleLogger.info(`[${useSchemaPool ? this.schemaName : "RUNTIME"}] Transaction committed`);
|
|
259
363
|
}
|
|
260
364
|
catch (error) {
|
|
261
|
-
|
|
262
|
-
|
|
365
|
+
if (client) {
|
|
366
|
+
await client.query("ROLLBACK").catch(() => {
|
|
367
|
+
// Connection failure: nothing to roll back.
|
|
368
|
+
});
|
|
369
|
+
this.moduleLogger.error(`[${useSchemaPool ? this.schemaName : "RUNTIME"}] Transaction rolled back`, error);
|
|
370
|
+
}
|
|
263
371
|
throw error;
|
|
264
372
|
}
|
|
265
373
|
finally {
|
|
266
|
-
client
|
|
374
|
+
if (client) {
|
|
375
|
+
client.release();
|
|
376
|
+
}
|
|
267
377
|
span.end();
|
|
268
378
|
}
|
|
269
379
|
}
|
|
@@ -287,78 +397,10 @@ class PostgresSchemaDbUtils {
|
|
|
287
397
|
await Promise.all(promises);
|
|
288
398
|
this.moduleLogger.info("All database pools closed");
|
|
289
399
|
}
|
|
290
|
-
// -- Internal helpers (schema pool only) ----------------------------------
|
|
291
|
-
execSQLForSchema(context, sql, params = []) {
|
|
292
|
-
const span = tracer.startSpan("PostgresSchemaDbUtilsExecSQLForSchema", context);
|
|
293
|
-
if (!this.schemaPool) {
|
|
294
|
-
throw new Error(`Pool not initialized for schema: ${this.schemaName}`);
|
|
295
|
-
}
|
|
296
|
-
return new Promise((resolve, reject) => {
|
|
297
|
-
this.schemaPool.query(sql, params, (error) => {
|
|
298
|
-
span.end();
|
|
299
|
-
if (error) {
|
|
300
|
-
reject(error);
|
|
301
|
-
}
|
|
302
|
-
else {
|
|
303
|
-
resolve();
|
|
304
|
-
}
|
|
305
|
-
});
|
|
306
|
-
});
|
|
307
|
-
}
|
|
308
|
-
async execSQLFileForSchema(context, filename) {
|
|
309
|
-
try {
|
|
310
|
-
const span = tracer.startSpan("PostgresSchemaDbUtilsExecSQLFileForSchema", context);
|
|
311
|
-
const sql = (await fs.readFile(filename)).toString();
|
|
312
|
-
if (!this.schemaPool) {
|
|
313
|
-
throw new Error(`Pool not initialized for schema: ${this.schemaName}`);
|
|
314
|
-
}
|
|
315
|
-
return new Promise((resolve, reject) => {
|
|
316
|
-
this.schemaPool.query(sql, (error) => {
|
|
317
|
-
span.end();
|
|
318
|
-
if (error) {
|
|
319
|
-
if (error.code === "ENOENT") {
|
|
320
|
-
resolve();
|
|
321
|
-
}
|
|
322
|
-
else {
|
|
323
|
-
reject(error);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
else {
|
|
327
|
-
resolve();
|
|
328
|
-
}
|
|
329
|
-
});
|
|
330
|
-
});
|
|
331
|
-
}
|
|
332
|
-
catch (error) {
|
|
333
|
-
if (error.code === "ENOENT") {
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
|
-
throw error;
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
querySQLForSchema(context, sql, params = []) {
|
|
340
|
-
const span = tracer.startSpan("PostgresSchemaDbUtilsQuerySQLForSchema", context);
|
|
341
|
-
if (!this.schemaPool) {
|
|
342
|
-
throw new Error(`Pool not initialized for schema: ${this.schemaName}`);
|
|
343
|
-
}
|
|
344
|
-
return new Promise((resolve, reject) => {
|
|
345
|
-
this.schemaPool.query(sql, params, (error, result) => {
|
|
346
|
-
span.end();
|
|
347
|
-
if (error) {
|
|
348
|
-
reject(error);
|
|
349
|
-
}
|
|
350
|
-
else {
|
|
351
|
-
resolve(result.rows);
|
|
352
|
-
}
|
|
353
|
-
});
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
400
|
}
|
|
357
401
|
exports.PostgresSchemaDbUtils = PostgresSchemaDbUtils;
|
|
358
402
|
// ---------------------------------------------------------------------------
|
|
359
403
|
// Functional API – single-pool mode used by the DbUtils facade.
|
|
360
|
-
// An internal PostgresSchemaDbUtils instance backs these functions so the
|
|
361
|
-
// behaviour is identical to before.
|
|
362
404
|
// ---------------------------------------------------------------------------
|
|
363
405
|
/**
|
|
364
406
|
* Injects the OTel tracer and logger instances used by all Postgres operations.
|
|
@@ -372,47 +414,59 @@ function PostgresDbUtilsSetOTel(tracerIn, loggerIn) {
|
|
|
372
414
|
/**
|
|
373
415
|
* Creates the Postgres connection pool and applies pending migration files
|
|
374
416
|
* from `sqlDir`.
|
|
417
|
+
*
|
|
418
|
+
* Migrations are applied under a session-level advisory lock (concurrently
|
|
419
|
+
* booting replicas serialise instead of double-applying) and each file plus
|
|
420
|
+
* its `db_version` row runs in its own transaction: a failing migration is
|
|
421
|
+
* rolled back and never recorded.
|
|
375
422
|
*/
|
|
376
423
|
async function PostgresDbUtilsInit(context, config, sqlDir) {
|
|
377
424
|
const span = tracer.startSpan("PostgresDbUtilsInit", context);
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
}
|
|
402
|
-
logger.info(`Current DB Version: ${dbVersionApplied}`, span);
|
|
403
|
-
for (const initFile of initFiles) {
|
|
404
|
-
const regex = /init-(\d+).sql/g;
|
|
405
|
-
const match = regex.exec(initFile);
|
|
406
|
-
if (match) {
|
|
407
|
-
const dbVersionInitFile = Number(match[1]);
|
|
408
|
-
if (dbVersionInitFile > dbVersionApplied) {
|
|
409
|
-
logger.info(`Loading init file: ${initFile}`, span);
|
|
410
|
-
await PostgresDbUtilsExecSQLFile(span, `${sqlDir}/${initFile}`);
|
|
411
|
-
await PostgresDbUtilsQuerySQL(span, 'INSERT INTO metadata ("type", "value", "dateCreated") VALUES ($1, $2, $3)', ["db_version", dbVersionInitFile, new Date().toISOString()]);
|
|
412
|
-
}
|
|
425
|
+
try {
|
|
426
|
+
pool = new pg_1.Pool(buildPoolConfig(config, undefined, 20, true));
|
|
427
|
+
pool.on("error", (err) => {
|
|
428
|
+
logger.error("PostgreSQL pool connection error", err);
|
|
429
|
+
});
|
|
430
|
+
const client = await pool.connect();
|
|
431
|
+
try {
|
|
432
|
+
await withAdvisoryLockOnClient(client, exports.POSTGRES_LOCK_KEYS.migration, async () => {
|
|
433
|
+
await applyMigrationOnClient(client, span, `${sqlDir}/init-0000.sql`, null);
|
|
434
|
+
const dbVersionApplied = await readAppliedVersion(client, span, "PostgresDbUtilsQuerySQL");
|
|
435
|
+
logger.info(`Current DB Version: ${dbVersionApplied}`, span);
|
|
436
|
+
const initFiles = (await fs.readdir(sqlDir)).sort();
|
|
437
|
+
for (const initFile of initFiles) {
|
|
438
|
+
const match = /init-(\d+)\.sql/.exec(initFile);
|
|
439
|
+
if (match) {
|
|
440
|
+
const dbVersionInitFile = Number(match[1]);
|
|
441
|
+
if (dbVersionInitFile > dbVersionApplied) {
|
|
442
|
+
logger.info(`Loading init file: ${initFile}`, span);
|
|
443
|
+
await applyMigrationOnClient(client, span, `${sqlDir}/${initFile}`, dbVersionInitFile);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
});
|
|
413
448
|
}
|
|
449
|
+
finally {
|
|
450
|
+
client.release();
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
finally {
|
|
454
|
+
span.end();
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Run a callback while holding a Postgres advisory lock, serialising the
|
|
459
|
+
* callback across every replica of every service booting against the same
|
|
460
|
+
* database (used for the `auth_token` and first-user bootstrap races).
|
|
461
|
+
*/
|
|
462
|
+
async function PostgresDbUtilsWithAdvisoryLock(lock, callback) {
|
|
463
|
+
const client = await pool.connect();
|
|
464
|
+
try {
|
|
465
|
+
return await withAdvisoryLockOnClient(client, exports.POSTGRES_LOCK_KEYS[lock], callback);
|
|
466
|
+
}
|
|
467
|
+
finally {
|
|
468
|
+
client.release();
|
|
414
469
|
}
|
|
415
|
-
span.end();
|
|
416
470
|
}
|
|
417
471
|
/** Returns the underlying `pg.Pool` instance. */
|
|
418
472
|
function PostgresDbUtilsGetPool() {
|
|
@@ -422,102 +476,101 @@ function PostgresDbUtilsGetPool() {
|
|
|
422
476
|
* Execute a write SQL statement with OTel tracing.
|
|
423
477
|
* @returns Number of rows changed.
|
|
424
478
|
*/
|
|
425
|
-
function PostgresDbUtilsExecSQL(context, sql, params = []) {
|
|
479
|
+
async function PostgresDbUtilsExecSQL(context, sql, params = []) {
|
|
426
480
|
const span = tracer.startSpan("PostgresDbUtilsExecSQL", context);
|
|
427
|
-
|
|
428
|
-
pool.query(sql, params
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
}
|
|
437
|
-
else {
|
|
438
|
-
span.addEvent(`Impacted Rows: ${result.rowCount || 0}`);
|
|
439
|
-
span.end();
|
|
440
|
-
resolve(result.rowCount || 0);
|
|
441
|
-
}
|
|
481
|
+
try {
|
|
482
|
+
const result = await pool.query(sql, params);
|
|
483
|
+
span.addEvent(`Impacted Rows: ${result.rowCount || 0}`);
|
|
484
|
+
return result.rowCount || 0;
|
|
485
|
+
}
|
|
486
|
+
catch (error) {
|
|
487
|
+
span.setStatus({
|
|
488
|
+
code: api_1.SpanStatusCode.ERROR,
|
|
489
|
+
message: error.message,
|
|
442
490
|
});
|
|
443
|
-
|
|
491
|
+
throw error;
|
|
492
|
+
}
|
|
493
|
+
finally {
|
|
494
|
+
span.end();
|
|
495
|
+
}
|
|
444
496
|
}
|
|
445
|
-
/**
|
|
497
|
+
/**
|
|
498
|
+
* Execute an entire SQL file (used for migrations).
|
|
499
|
+
* Migration files must not contain their own transaction control statements.
|
|
500
|
+
*/
|
|
446
501
|
async function PostgresDbUtilsExecSQLFile(context, filename) {
|
|
447
502
|
const span = tracer.startSpan("PostgresDbUtilsExecSQLFile", context);
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
pool.query(sql
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
else {
|
|
457
|
-
span.end();
|
|
458
|
-
resolve();
|
|
459
|
-
}
|
|
503
|
+
try {
|
|
504
|
+
const sql = (await fs.readFile(filename)).toString();
|
|
505
|
+
await pool.query(sql);
|
|
506
|
+
}
|
|
507
|
+
catch (error) {
|
|
508
|
+
span.setStatus({
|
|
509
|
+
code: api_1.SpanStatusCode.ERROR,
|
|
510
|
+
message: error.message,
|
|
460
511
|
});
|
|
461
|
-
|
|
512
|
+
throw error;
|
|
513
|
+
}
|
|
514
|
+
finally {
|
|
515
|
+
span.end();
|
|
516
|
+
}
|
|
462
517
|
}
|
|
463
518
|
/**
|
|
464
519
|
* Execute a read SQL query with OTel tracing.
|
|
465
520
|
* @returns Array of row objects.
|
|
466
521
|
*/
|
|
467
|
-
function PostgresDbUtilsQuerySQL(context, sql, params = [], debug = false) {
|
|
522
|
+
async function PostgresDbUtilsQuerySQL(context, sql, params = [], debug = false) {
|
|
468
523
|
const span = tracer.startSpan("PostgresDbUtilsQuerySQL", context);
|
|
469
524
|
if (debug) {
|
|
470
525
|
console.log(sql);
|
|
471
526
|
}
|
|
472
|
-
|
|
473
|
-
pool.query(sql, params
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
span.end();
|
|
481
|
-
reject(error);
|
|
482
|
-
}
|
|
483
|
-
else {
|
|
484
|
-
span.end();
|
|
485
|
-
resolve(result.rows);
|
|
486
|
-
}
|
|
527
|
+
try {
|
|
528
|
+
const result = await pool.query(sql, params);
|
|
529
|
+
return result.rows;
|
|
530
|
+
}
|
|
531
|
+
catch (error) {
|
|
532
|
+
span.setStatus({
|
|
533
|
+
code: api_1.SpanStatusCode.ERROR,
|
|
534
|
+
message: error.message,
|
|
487
535
|
});
|
|
488
|
-
|
|
536
|
+
logger.error(`SQL ERROR: ${sql}`, error, span);
|
|
537
|
+
throw error;
|
|
538
|
+
}
|
|
539
|
+
finally {
|
|
540
|
+
span.end();
|
|
541
|
+
}
|
|
489
542
|
}
|
|
490
543
|
/** Start a transaction. */
|
|
491
|
-
function PostgresDbUtilsTransactionStart(context) {
|
|
544
|
+
async function PostgresDbUtilsTransactionStart(context) {
|
|
492
545
|
const span = tracer.startSpan("PostgresDbUtilsTransactionStart", context);
|
|
493
|
-
|
|
494
|
-
pool.query("BEGIN"
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
else {
|
|
501
|
-
span.end();
|
|
502
|
-
resolve();
|
|
503
|
-
}
|
|
546
|
+
try {
|
|
547
|
+
await pool.query("BEGIN");
|
|
548
|
+
}
|
|
549
|
+
catch (error) {
|
|
550
|
+
span.setStatus({
|
|
551
|
+
code: api_1.SpanStatusCode.ERROR,
|
|
552
|
+
message: error.message,
|
|
504
553
|
});
|
|
505
|
-
|
|
554
|
+
throw error;
|
|
555
|
+
}
|
|
556
|
+
finally {
|
|
557
|
+
span.end();
|
|
558
|
+
}
|
|
506
559
|
}
|
|
507
560
|
/** Commit a transaction. */
|
|
508
|
-
function PostgresDbUtilsTransactionCommit(context) {
|
|
561
|
+
async function PostgresDbUtilsTransactionCommit(context) {
|
|
509
562
|
const span = tracer.startSpan("PostgresDbUtilsTransactionCommit", context);
|
|
510
|
-
|
|
511
|
-
pool.query("COMMIT"
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
else {
|
|
518
|
-
span.end();
|
|
519
|
-
resolve();
|
|
520
|
-
}
|
|
563
|
+
try {
|
|
564
|
+
await pool.query("COMMIT");
|
|
565
|
+
}
|
|
566
|
+
catch (error) {
|
|
567
|
+
span.setStatus({
|
|
568
|
+
code: api_1.SpanStatusCode.ERROR,
|
|
569
|
+
message: error.message,
|
|
521
570
|
});
|
|
522
|
-
|
|
571
|
+
throw error;
|
|
572
|
+
}
|
|
573
|
+
finally {
|
|
574
|
+
span.end();
|
|
575
|
+
}
|
|
523
576
|
}
|