@devopsplaybook.io/common-utils 1.0.0-beta.5.e74e0d9

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.
Files changed (50) hide show
  1. package/.github/workflows/main-build.yml +17 -0
  2. package/.github/workflows/npm-upgrade.yml +16 -0
  3. package/.github/workflows/pr-check.yml +26 -0
  4. package/.github/workflows/reusable-merge-build.yml +137 -0
  5. package/.github/workflows/reusable-npm-merge.yml +131 -0
  6. package/.github/workflows/reusable-npm-pr.yml +149 -0
  7. package/.github/workflows/reusable-npm-upgrade.yml +92 -0
  8. package/.github/workflows/reusable-pr-verify.yml +131 -0
  9. package/README.md +1 -0
  10. package/dist/index.d.ts +8 -0
  11. package/dist/index.js +24 -0
  12. package/dist/src/ConfigBase.d.ts +104 -0
  13. package/dist/src/ConfigBase.js +201 -0
  14. package/dist/src/DbUtils.d.ts +50 -0
  15. package/dist/src/DbUtils.js +117 -0
  16. package/dist/src/DbUtilsNoTelemetry.d.ts +27 -0
  17. package/dist/src/DbUtilsNoTelemetry.js +89 -0
  18. package/dist/src/OTelContext.d.ts +35 -0
  19. package/dist/src/OTelContext.js +42 -0
  20. package/dist/src/PostgresDbUtils.d.ts +52 -0
  21. package/dist/src/PostgresDbUtils.js +217 -0
  22. package/dist/src/SqlDbUtils.d.ts +40 -0
  23. package/dist/src/SqlDbUtils.js +156 -0
  24. package/dist/src/SystemCommand.d.ts +9 -0
  25. package/dist/src/SystemCommand.js +56 -0
  26. package/dist/src/Timeout.d.ts +6 -0
  27. package/dist/src/Timeout.js +15 -0
  28. package/eslint.config.mjs +10 -0
  29. package/index.ts +8 -0
  30. package/jest.config.js +13 -0
  31. package/package.json +50 -0
  32. package/prettierrc.json +5 -0
  33. package/src/ConfigBase.spec.ts +108 -0
  34. package/src/ConfigBase.ts +213 -0
  35. package/src/DbUtils.spec.ts +23 -0
  36. package/src/DbUtils.ts +118 -0
  37. package/src/DbUtilsNoTelemetry.spec.ts +174 -0
  38. package/src/DbUtilsNoTelemetry.ts +121 -0
  39. package/src/OTelContext.spec.ts +58 -0
  40. package/src/OTelContext.ts +65 -0
  41. package/src/PostgresDbUtils.spec.ts +155 -0
  42. package/src/PostgresDbUtils.ts +233 -0
  43. package/src/SqlDbUtils.spec.ts +111 -0
  44. package/src/SqlDbUtils.ts +153 -0
  45. package/src/SystemCommand.spec.ts +18 -0
  46. package/src/SystemCommand.ts +23 -0
  47. package/src/Timeout.spec.ts +18 -0
  48. package/src/Timeout.ts +12 -0
  49. package/tsconfig.json +14 -0
  50. package/tsconfig.spec.json +7 -0
@@ -0,0 +1,121 @@
1
+ import { StandardLogger, ModuleLogger } from "@devopsplaybook.io/otel-utils";
2
+ import {
3
+ DbUtilsGetDatabase,
4
+ DbUtilsGetType,
5
+ convertToPostgresPlaceholders,
6
+ } from "./DbUtils";
7
+
8
+ let logger: ModuleLogger;
9
+
10
+ /**
11
+ * Injects the OTel logger instance used by no-telemetry DB operations.
12
+ * Must be called once at startup.
13
+ */
14
+ export function DbUtilsNoTelemetrySetLogger(loggerIn: StandardLogger): void {
15
+ logger = loggerIn.createModuleLogger("DbUtilsNoTelemetry");
16
+ }
17
+
18
+ /**
19
+ * Execute a multi-row INSERT with a flat parameter array.
20
+ * Builds: INSERT INTO <tableCols> VALUES (?,?...),(?,?...),...
21
+ *
22
+ * @returns Number of rows inserted.
23
+ */
24
+ export function DbUtilsNoTelemetryBatchInsert(
25
+ tableCols: string,
26
+ numCols: number,
27
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
28
+ rows: any[][],
29
+ ): number | Promise<number> {
30
+ if (rows.length === 0) return 0;
31
+ const rowSQL = `(${Array.from({ length: numCols }, () => "?").join(",")})`;
32
+ const multiValues = Array.from({ length: rows.length }, () => rowSQL).join(
33
+ ",",
34
+ );
35
+ const sql = `INSERT ${tableCols} VALUES ${multiValues}`;
36
+ return DbUtilsNoTelemetryExecSQL(sql, rows.flat());
37
+ }
38
+
39
+ /**
40
+ * Execute a write SQL statement **without** creating an OTel span.
41
+ * Use this on high-throughput paths where span overhead matters.
42
+ *
43
+ * @returns Number of rows changed.
44
+ */
45
+ export function DbUtilsNoTelemetryExecSQL(
46
+ sql: string,
47
+ params: unknown[] = [],
48
+ ): number | Promise<number> {
49
+ const dbType = DbUtilsGetType();
50
+ if (dbType === "postgres") {
51
+ const pgSql = convertToPostgresPlaceholders(sql);
52
+ return new Promise((resolve, reject) => {
53
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
54
+ (DbUtilsGetDatabase() as any).query(
55
+ pgSql,
56
+ params,
57
+ (error: Error | null, result: { rowCount: number | null }) => {
58
+ if (error) {
59
+ logger.error(`SQL INSERT ERROR: ${sql.substring(0, 200)}`, error);
60
+ reject(error);
61
+ } else {
62
+ resolve(result.rowCount || 0);
63
+ }
64
+ },
65
+ );
66
+ });
67
+ }
68
+ // SQLite (better-sqlite3) – synchronous
69
+ const stmt = (
70
+ DbUtilsGetDatabase() as {
71
+ prepare: (sql: string) => {
72
+ run: (params: unknown[]) => { changes: number };
73
+ };
74
+ }
75
+ ).prepare(sql);
76
+ const result = stmt.run(params);
77
+ return result.changes;
78
+ }
79
+
80
+ /**
81
+ * Execute a read SQL query **without** creating an OTel span.
82
+ * Use this on high-throughput paths where span overhead matters.
83
+ *
84
+ * @returns Array of row objects.
85
+ */
86
+ export function DbUtilsNoTelemetryQuerySQL(
87
+ sql: string,
88
+ params: unknown[] = [],
89
+ debug = false,
90
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
91
+ ): any[] | Promise<any[]> {
92
+ if (debug) {
93
+ console.log(sql);
94
+ }
95
+ const dbType = DbUtilsGetType();
96
+ if (dbType === "postgres") {
97
+ const pgSql = convertToPostgresPlaceholders(sql);
98
+ return new Promise((resolve, reject) => {
99
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
100
+ (DbUtilsGetDatabase() as any).query(
101
+ pgSql,
102
+ params,
103
+ (error: Error | null, result: { rows: unknown[] }) => {
104
+ if (error) {
105
+ logger.error(`SQL ERROR: ${sql}`, error);
106
+ reject(error);
107
+ } else {
108
+ resolve(result.rows);
109
+ }
110
+ },
111
+ );
112
+ });
113
+ }
114
+ // SQLite (better-sqlite3) – synchronous
115
+ const stmt = (
116
+ DbUtilsGetDatabase() as {
117
+ prepare: (sql: string) => { all: (params: unknown[]) => unknown[] };
118
+ }
119
+ ).prepare(sql);
120
+ return stmt.all(params);
121
+ }
@@ -0,0 +1,58 @@
1
+ import { createOTelContext } from "./OTelContext";
2
+
3
+ describe("createOTelContext", () => {
4
+ it("should return an object with all expected methods", () => {
5
+ const ctx = createOTelContext();
6
+ expect(typeof ctx.OTelTracer).toBe("function");
7
+ expect(typeof ctx.OTelSetTracer).toBe("function");
8
+ expect(typeof ctx.OTelMeter).toBe("function");
9
+ expect(typeof ctx.OTelSetMeter).toBe("function");
10
+ expect(typeof ctx.OTelLogger).toBe("function");
11
+ expect(typeof ctx.OTelRequestSpan).toBe("function");
12
+ });
13
+
14
+ it("should create a StandardLogger lazily", () => {
15
+ const ctx = createOTelContext();
16
+ const logger = ctx.OTelLogger();
17
+ expect(logger).toBeDefined();
18
+ // Same instance on second call
19
+ expect(ctx.OTelLogger()).toBe(logger);
20
+ });
21
+
22
+ it("should store and retrieve tracer via setter/getter", () => {
23
+ const ctx = createOTelContext();
24
+ const fakeTracer = { id: "tracer-1" } as never;
25
+ ctx.OTelSetTracer(fakeTracer);
26
+ expect(ctx.OTelTracer()).toBe(fakeTracer);
27
+ });
28
+
29
+ it("should store and retrieve meter via setter/getter", () => {
30
+ const ctx = createOTelContext();
31
+ const fakeMeter = { id: "meter-1" } as never;
32
+ ctx.OTelSetMeter(fakeMeter);
33
+ expect(ctx.OTelMeter()).toBe(fakeMeter);
34
+ });
35
+
36
+ it("should isolate contexts from each other", () => {
37
+ const ctx1 = createOTelContext();
38
+ const ctx2 = createOTelContext();
39
+ const fakeTracer1 = { id: "tracer-1" } as never;
40
+ const fakeTracer2 = { id: "tracer-2" } as never;
41
+ ctx1.OTelSetTracer(fakeTracer1);
42
+ ctx2.OTelSetTracer(fakeTracer2);
43
+ expect(ctx1.OTelTracer()).toBe(fakeTracer1);
44
+ expect(ctx2.OTelTracer()).toBe(fakeTracer2);
45
+ });
46
+
47
+ it("OTelRequestSpan should return undefined for object without tracerSpanApi", () => {
48
+ const ctx = createOTelContext();
49
+ expect(ctx.OTelRequestSpan({})).toBeUndefined();
50
+ });
51
+
52
+ it("OTelRequestSpan should return the span when present", () => {
53
+ const ctx = createOTelContext();
54
+ const fakeSpan = { spanId: "123" } as never;
55
+ const req = { tracerSpanApi: fakeSpan };
56
+ expect(ctx.OTelRequestSpan(req)).toBe(fakeSpan);
57
+ });
58
+ });
@@ -0,0 +1,65 @@
1
+ import {
2
+ StandardLogger,
3
+ StandardMeter,
4
+ StandardTracer,
5
+ } from "@devopsplaybook.io/otel-utils";
6
+ import { Span } from "@opentelemetry/sdk-trace-base";
7
+
8
+ /**
9
+ * Result of {@link createOTelContext}.
10
+ * Holds module-level singletons for the OpenTelemetry tracer, meter and logger
11
+ * used throughout a server process.
12
+ */
13
+ export interface OTelContext {
14
+ OTelTracer: () => StandardTracer;
15
+ OTelSetTracer: (tracer: StandardTracer) => void;
16
+ OTelMeter: () => StandardMeter;
17
+ OTelSetMeter: (meter: StandardMeter) => void;
18
+ OTelLogger: () => StandardLogger;
19
+ /**
20
+ * Retrieves the span previously attached to a request object.
21
+ * Equivalent to `req.tracerSpanApi`.
22
+ */
23
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
24
+ OTelRequestSpan: (req: any) => Span | undefined;
25
+ }
26
+
27
+ /**
28
+ * Creates an isolated OTel context (tracer / meter / logger singletons).
29
+ *
30
+ * Each server process should call this once at startup and pass the returned
31
+ * object to modules that need telemetry access. This avoids polluting the
32
+ * global module scope and makes unit-testing straightforward.
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * const otel = createOTelContext();
37
+ * otel.OTelSetTracer(new StandardTracer(config));
38
+ * otel.OTelSetMeter(new StandardMeter(config));
39
+ * otel.OTelLogger().initOTel(config);
40
+ * ```
41
+ */
42
+ export function createOTelContext(): OTelContext {
43
+ let tracer: StandardTracer;
44
+ let meter: StandardMeter;
45
+ let logger: StandardLogger;
46
+
47
+ return {
48
+ OTelTracer: () => tracer,
49
+ OTelSetTracer: (t: StandardTracer) => {
50
+ tracer = t;
51
+ },
52
+ OTelMeter: () => meter,
53
+ OTelSetMeter: (m: StandardMeter) => {
54
+ meter = m;
55
+ },
56
+ OTelLogger: () => {
57
+ if (!logger) {
58
+ logger = new StandardLogger();
59
+ }
60
+ return logger;
61
+ },
62
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
63
+ OTelRequestSpan: (req: any) => req?.tracerSpanApi as Span | undefined,
64
+ };
65
+ }
@@ -0,0 +1,155 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+
3
+ // Mock pg before imports
4
+ const mockQuery = jest.fn();
5
+ jest.mock("pg", () => {
6
+ return {
7
+ Pool: jest.fn().mockImplementation(() => ({
8
+ query: mockQuery,
9
+ on: jest.fn(),
10
+ })),
11
+ };
12
+ });
13
+
14
+ jest.mock("fs-extra", () => ({
15
+ readdir: jest.fn().mockResolvedValue(["init-0000.sql"]),
16
+ readFile: jest
17
+ .fn()
18
+ .mockResolvedValue(Buffer.from("CREATE TABLE metadata ();")),
19
+ }));
20
+
21
+ jest.mock("@devopsplaybook.io/otel-utils", () => ({
22
+ StandardTracer: jest.fn(),
23
+ StandardLogger: jest.fn(),
24
+ ModuleLogger: jest.fn(),
25
+ }));
26
+
27
+ jest.mock("@opentelemetry/sdk-trace-base", () => ({
28
+ Span: jest.fn(),
29
+ }));
30
+
31
+ jest.mock("@opentelemetry/api", () => ({
32
+ SpanStatusCode: { ERROR: 2, OK: 1 },
33
+ }));
34
+
35
+ describe("PostgresDbUtils", () => {
36
+ let PostgresDbUtils: typeof import("./PostgresDbUtils");
37
+
38
+ const mockSpan = {
39
+ end: jest.fn(),
40
+ addEvent: jest.fn(),
41
+ setStatus: jest.fn(),
42
+ };
43
+
44
+ const mockModuleLogger = {
45
+ info: jest.fn(),
46
+ error: jest.fn(),
47
+ warn: jest.fn(),
48
+ };
49
+
50
+ const mockTracer = {
51
+ startSpan: jest.fn().mockReturnValue(mockSpan),
52
+ };
53
+
54
+ const mockLogger = {
55
+ createModuleLogger: jest.fn().mockReturnValue(mockModuleLogger),
56
+ };
57
+
58
+ beforeEach(() => {
59
+ jest.clearAllMocks();
60
+ jest.isolateModules(() => {
61
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
62
+ PostgresDbUtils = require("./PostgresDbUtils");
63
+ });
64
+ PostgresDbUtils.PostgresDbUtilsSetOTel(
65
+ mockTracer as any,
66
+ mockLogger as any,
67
+ );
68
+ });
69
+
70
+ it("PostgresDbUtilsSetOTel should accept tracer and logger", () => {
71
+ expect(() =>
72
+ PostgresDbUtils.PostgresDbUtilsSetOTel(
73
+ mockTracer as any,
74
+ mockLogger as any,
75
+ ),
76
+ ).not.toThrow();
77
+ });
78
+
79
+ it("PostgresDbUtilsGetPool should return undefined before init", () => {
80
+ expect(PostgresDbUtils.PostgresDbUtilsGetPool()).toBeUndefined();
81
+ });
82
+
83
+ it("PostgresDbUtilsExecSQL should create a span and call pool.query", async () => {
84
+ const config = {
85
+ DATABASE_POSTGRES_HOST: "localhost",
86
+ DATABASE_POSTGRES_PORT: 5432,
87
+ DATABASE_POSTGRES_USER: "user",
88
+ DATABASE_POSTGRES_PASSWORD: "pass",
89
+ DATABASE_POSTGRES_DATABASE: "testdb",
90
+ };
91
+
92
+ // Mock the version query to return version 0
93
+ // pool.query can be called with 2 args (sql, cb) or 3 args (sql, params, cb)
94
+ mockQuery.mockImplementation((...args: any[]) => {
95
+ const cb =
96
+ typeof args[args.length - 1] === "function"
97
+ ? args[args.length - 1]
98
+ : null;
99
+ if (!cb) return;
100
+ // Determine which call this is based on arg count
101
+ if (args.length === 2) {
102
+ // execSQLFile call
103
+ cb(null);
104
+ } else if (args.length === 3) {
105
+ const sql = args[0] as string;
106
+ if (sql.includes("MAX")) {
107
+ // version query
108
+ cb(null, { rows: [{ version: 0 }] });
109
+ } else {
110
+ // insert version
111
+ cb(null, { rowCount: 1 });
112
+ }
113
+ }
114
+ });
115
+
116
+ await PostgresDbUtils.PostgresDbUtilsInit(
117
+ mockSpan as any,
118
+ config,
119
+ "/fake/sql",
120
+ );
121
+
122
+ expect(mockTracer.startSpan).toHaveBeenCalledWith(
123
+ "PostgresDbUtilsInit",
124
+ mockSpan,
125
+ );
126
+ });
127
+
128
+ it("PostgresDbUtilsTransactionStart should call BEGIN", async () => {
129
+ const config = {
130
+ DATABASE_POSTGRES_HOST: "localhost",
131
+ DATABASE_POSTGRES_PORT: 5432,
132
+ DATABASE_POSTGRES_USER: "user",
133
+ DATABASE_POSTGRES_PASSWORD: "pass",
134
+ DATABASE_POSTGRES_DATABASE: "testdb",
135
+ };
136
+
137
+ // Need to init first to create pool
138
+ mockQuery.mockImplementation((_sql: any, ...args: any[]) => {
139
+ const cb = args[args.length - 1];
140
+ if (typeof cb === "function") cb(null, { rows: [], rowCount: 0 });
141
+ });
142
+
143
+ await PostgresDbUtils.PostgresDbUtilsInit(
144
+ mockSpan as any,
145
+ config,
146
+ "/fake/sql",
147
+ );
148
+ await PostgresDbUtils.PostgresDbUtilsTransactionStart(mockSpan as any);
149
+
150
+ expect(mockTracer.startSpan).toHaveBeenCalledWith(
151
+ "PostgresDbUtilsTransactionStart",
152
+ mockSpan,
153
+ );
154
+ });
155
+ });
@@ -0,0 +1,233 @@
1
+ import { Pool } from "pg";
2
+ import * as fs from "fs-extra";
3
+ import { Span } from "@opentelemetry/sdk-trace-base";
4
+ import { SpanStatusCode } from "@opentelemetry/api";
5
+ import {
6
+ StandardTracer,
7
+ StandardLogger,
8
+ ModuleLogger,
9
+ } from "@devopsplaybook.io/otel-utils";
10
+
11
+ /**
12
+ * Configuration subset required by the Postgres module.
13
+ */
14
+ export interface PostgresDbConfig {
15
+ DATABASE_POSTGRES_HOST: string;
16
+ DATABASE_POSTGRES_PORT: number;
17
+ DATABASE_POSTGRES_USER: string;
18
+ DATABASE_POSTGRES_PASSWORD: string;
19
+ DATABASE_POSTGRES_DATABASE: string;
20
+ }
21
+
22
+ let pool: Pool;
23
+ let tracer: StandardTracer;
24
+ let logger: ModuleLogger;
25
+
26
+ /**
27
+ * Injects the OTel tracer and logger instances used by all Postgres operations.
28
+ * Must be called once at startup, before {@link PostgresDbUtilsInit}.
29
+ */
30
+ export function PostgresDbUtilsSetOTel(
31
+ tracerIn: StandardTracer,
32
+ loggerIn: StandardLogger,
33
+ ): void {
34
+ tracer = tracerIn;
35
+ logger = loggerIn.createModuleLogger("PostgresDbUtils");
36
+ }
37
+
38
+ /**
39
+ * Creates the Postgres connection pool and applies pending migration files
40
+ * from `sqlDir`.
41
+ *
42
+ * Migration files must follow the naming convention `init-NNNN.sql` and are
43
+ * applied in lexicographic order. A `metadata` table tracks which migrations
44
+ * have already been applied so they are idempotent.
45
+ *
46
+ * @param context Parent OTel span.
47
+ * @param config Configuration with Postgres connection fields.
48
+ * @param sqlDir Absolute path to the directory containing SQL migration files
49
+ * written for Postgres (with `$1,$2...` placeholders).
50
+ * If migrations are SQLite-first, use `convertToPostgresPlaceholders`
51
+ * before passing them.
52
+ */
53
+ export async function PostgresDbUtilsInit(
54
+ context: Span,
55
+ config: PostgresDbConfig,
56
+ sqlDir: string,
57
+ ): Promise<void> {
58
+ const span = tracer.startSpan("PostgresDbUtilsInit", context);
59
+
60
+ pool = new Pool({
61
+ host: config.DATABASE_POSTGRES_HOST,
62
+ port: config.DATABASE_POSTGRES_PORT || 5432,
63
+ user: config.DATABASE_POSTGRES_USER,
64
+ password: config.DATABASE_POSTGRES_PASSWORD,
65
+ database: config.DATABASE_POSTGRES_DATABASE,
66
+ max: 20,
67
+ idleTimeoutMillis: 30000,
68
+ connectionTimeoutMillis: 10000,
69
+ keepAlive: true,
70
+ });
71
+
72
+ pool.on("error", (err: Error) => {
73
+ logger.error("PostgreSQL pool connection error", err);
74
+ });
75
+
76
+ await PostgresDbUtilsExecSQLFile(span, `${sqlDir}/init-0000.sql`);
77
+ const initFiles = (await fs.readdir(sqlDir)).sort();
78
+ let dbVersionApplied = 0;
79
+ const dbVersionQuery = await PostgresDbUtilsQuerySQL(
80
+ span,
81
+ "SELECT MAX(value) as version FROM metadata WHERE \"type\" = 'db_version'",
82
+ );
83
+ if (dbVersionQuery.length > 0 && dbVersionQuery[0].version) {
84
+ dbVersionApplied = Number(dbVersionQuery[0].version);
85
+ }
86
+ logger.info(`Current DB Version: ${dbVersionApplied}`, span);
87
+ for (const initFile of initFiles) {
88
+ const regex = /init-(\d+).sql/g;
89
+ const match = regex.exec(initFile);
90
+ if (match) {
91
+ const dbVersionInitFile = Number(match[1]);
92
+ if (dbVersionInitFile > dbVersionApplied) {
93
+ logger.info(`Loading init file: ${initFile}`, span);
94
+ await PostgresDbUtilsExecSQLFile(span, `${sqlDir}/${initFile}`);
95
+ await PostgresDbUtilsQuerySQL(
96
+ span,
97
+ 'INSERT INTO metadata ("type", "value", "dateCreated") VALUES ($1, $2, $3)',
98
+ ["db_version", dbVersionInitFile, new Date().toISOString()],
99
+ );
100
+ }
101
+ }
102
+ }
103
+ span.end();
104
+ }
105
+
106
+ /** Returns the underlying `pg.Pool` instance. */
107
+ export function PostgresDbUtilsGetPool(): Pool {
108
+ return pool;
109
+ }
110
+
111
+ /**
112
+ * Execute a write SQL statement with OTel tracing.
113
+ * @returns Number of rows changed.
114
+ */
115
+ export function PostgresDbUtilsExecSQL(
116
+ context: Span,
117
+ sql: string,
118
+ params: unknown[] = [],
119
+ ): Promise<number> {
120
+ const span = tracer.startSpan("PostgresDbUtilsExecSQL", context);
121
+ return new Promise((resolve, reject) => {
122
+ pool.query(
123
+ sql,
124
+ params,
125
+ (error: Error | null, result: { rowCount: number | null }) => {
126
+ if (error) {
127
+ span.setStatus({
128
+ code: SpanStatusCode.ERROR,
129
+ message: error.message,
130
+ });
131
+ span.end();
132
+ reject(error);
133
+ } else {
134
+ span.addEvent(`Impacted Rows: ${result.rowCount || 0}`);
135
+ span.end();
136
+ resolve(result.rowCount || 0);
137
+ }
138
+ },
139
+ );
140
+ });
141
+ }
142
+
143
+ /** Execute an entire SQL file (used for migrations). */
144
+ export async function PostgresDbUtilsExecSQLFile(
145
+ context: Span,
146
+ filename: string,
147
+ ): Promise<void> {
148
+ const span = tracer.startSpan("PostgresDbUtilsExecSQLFile", context);
149
+ const sql = (await fs.readFile(filename)).toString();
150
+ return new Promise((resolve, reject) => {
151
+ pool.query(sql, (error: Error | null) => {
152
+ if (error) {
153
+ span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
154
+ span.end();
155
+ reject(error);
156
+ } else {
157
+ span.end();
158
+ resolve();
159
+ }
160
+ });
161
+ });
162
+ }
163
+
164
+ /**
165
+ * Execute a read SQL query with OTel tracing.
166
+ * @returns Array of row objects.
167
+ */
168
+ export function PostgresDbUtilsQuerySQL(
169
+ context: Span,
170
+ sql: string,
171
+ params: unknown[] = [],
172
+ debug = false,
173
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
174
+ ): Promise<any[]> {
175
+ const span = tracer.startSpan("PostgresDbUtilsQuerySQL", context);
176
+ if (debug) {
177
+ console.log(sql);
178
+ }
179
+ return new Promise((resolve, reject) => {
180
+ pool.query(
181
+ sql,
182
+ params,
183
+ (error: Error | null, result: { rows: unknown[] }) => {
184
+ if (error) {
185
+ span.setStatus({
186
+ code: SpanStatusCode.ERROR,
187
+ message: error.message,
188
+ });
189
+ logger.error(`SQL ERROR: ${sql}`, error, span);
190
+ span.end();
191
+ reject(error);
192
+ } else {
193
+ span.end();
194
+ resolve(result.rows);
195
+ }
196
+ },
197
+ );
198
+ });
199
+ }
200
+
201
+ /** Start a transaction. */
202
+ export function PostgresDbUtilsTransactionStart(context: Span): Promise<void> {
203
+ const span = tracer.startSpan("PostgresDbUtilsTransactionStart", context);
204
+ return new Promise((resolve, reject) => {
205
+ pool.query("BEGIN", (error: Error | null) => {
206
+ if (error) {
207
+ span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
208
+ span.end();
209
+ reject(error);
210
+ } else {
211
+ span.end();
212
+ resolve();
213
+ }
214
+ });
215
+ });
216
+ }
217
+
218
+ /** Commit a transaction. */
219
+ export function PostgresDbUtilsTransactionCommit(context: Span): Promise<void> {
220
+ const span = tracer.startSpan("PostgresDbUtilsTransactionCommit", context);
221
+ return new Promise((resolve, reject) => {
222
+ pool.query("COMMIT", (error: Error | null) => {
223
+ if (error) {
224
+ span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
225
+ span.end();
226
+ reject(error);
227
+ } else {
228
+ span.end();
229
+ resolve();
230
+ }
231
+ });
232
+ });
233
+ }