@devopsplaybook.io/common-utils 1.0.0-beta.5.9c5ecde

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 +141 -0
  5. package/.github/workflows/reusable-npm-merge.yml +135 -0
  6. package/.github/workflows/reusable-npm-pr.yml +153 -0
  7. package/.github/workflows/reusable-npm-upgrade.yml +92 -0
  8. package/.github/workflows/reusable-pr-verify.yml +135 -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,56 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.SystemCommandExecute = SystemCommandExecute;
37
+ const childProcess = __importStar(require("child_process"));
38
+ /**
39
+ * Execute a shell command and return its stdout.
40
+ *
41
+ * @param command The command string to execute.
42
+ * @param options Optional `child_process.exec` options.
43
+ * @returns Resolves with stdout on success, rejects on error.
44
+ */
45
+ function SystemCommandExecute(command, options) {
46
+ return new Promise((resolve, reject) => {
47
+ childProcess.exec(command, options || {}, (error, stdout) => {
48
+ if (error) {
49
+ reject(error);
50
+ }
51
+ else {
52
+ resolve(String(stdout));
53
+ }
54
+ });
55
+ });
56
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Promise-based wrapper around `setTimeout`.
3
+ *
4
+ * @param duration Delay in milliseconds.
5
+ */
6
+ export declare function TimeoutWait(duration: number): Promise<void>;
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TimeoutWait = TimeoutWait;
4
+ /**
5
+ * Promise-based wrapper around `setTimeout`.
6
+ *
7
+ * @param duration Delay in milliseconds.
8
+ */
9
+ function TimeoutWait(duration) {
10
+ return new Promise((resolve) => {
11
+ setTimeout(() => {
12
+ resolve();
13
+ }, duration);
14
+ });
15
+ }
@@ -0,0 +1,10 @@
1
+ // @ts-check
2
+
3
+ import eslint from "@eslint/js";
4
+ import tseslint from "typescript-eslint";
5
+
6
+ export default tseslint.config(
7
+ eslint.configs.recommended,
8
+ ...tseslint.configs.strict,
9
+ ...tseslint.configs.stylistic,
10
+ );
package/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ export * from "./src/OTelContext";
2
+ export * from "./src/ConfigBase";
3
+ export * from "./src/DbUtils";
4
+ export * from "./src/DbUtilsNoTelemetry";
5
+ export * from "./src/SqlDbUtils";
6
+ export * from "./src/PostgresDbUtils";
7
+ export * from "./src/SystemCommand";
8
+ export * from "./src/Timeout";
package/jest.config.js ADDED
@@ -0,0 +1,13 @@
1
+ module.exports = {
2
+ moduleFileExtensions: ["ts", "js"],
3
+ transform: {
4
+ "^.+\\.(ts|tsx)$": [
5
+ "ts-jest",
6
+ {
7
+ tsconfig: "tsconfig.spec.json",
8
+ },
9
+ ],
10
+ },
11
+ testMatch: ["/**/src/**/*.spec.(ts|js)"],
12
+ testEnvironment: "node",
13
+ };
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@devopsplaybook.io/common-utils",
3
+ "version": "1.0.0-beta.5.9c5ecde",
4
+ "description": "Shared utility modules for devopsplaybook.io projects (DB, Config, OTel context, system helpers)",
5
+ "keywords": [
6
+ "Open Telemetry",
7
+ "OTel",
8
+ "SQLite",
9
+ "Postgres",
10
+ "Config",
11
+ "Utilities"
12
+ ],
13
+ "license": "ISC",
14
+ "author": "",
15
+ "type": "commonjs",
16
+ "main": "dist/index.js",
17
+ "types": "dist/index.d.ts",
18
+ "scripts": {
19
+ "build": "tsc",
20
+ "lint": "eslint src",
21
+ "test": "jest --coverage"
22
+ },
23
+ "dependencies": {
24
+ "@devopsplaybook.io/otel-utils": "^1.1.0",
25
+ "@opentelemetry/api": "^1.9.1",
26
+ "@opentelemetry/sdk-trace-base": "^2.7.1",
27
+ "better-sqlite3": "^12.9.0",
28
+ "fs-extra": "^11.3.5",
29
+ "pg": "^8.21.0",
30
+ "uuid": "^14.0.0"
31
+ },
32
+ "devDependencies": {
33
+ "@eslint/js": "^10.0.1",
34
+ "@types/better-sqlite3": "^7.6.13",
35
+ "@types/fs-extra": "^11.0.4",
36
+ "@types/jest": "^30.0.0",
37
+ "@types/node": "^25.9.1",
38
+ "@types/pg": "^8.20.0",
39
+ "@types/uuid": "^10.0.0",
40
+ "eslint": "^10.4.0",
41
+ "jest": "^30.4.2",
42
+ "ts-jest": "^29.4.11",
43
+ "ts-node": "^10.9.2",
44
+ "typescript": "^6.0.3",
45
+ "typescript-eslint": "^8.59.4"
46
+ },
47
+ "publishConfig": {
48
+ "access": "public"
49
+ }
50
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "tabWidth": 2,
3
+ "semi": true,
4
+ "singleQuote": false
5
+ }
@@ -0,0 +1,108 @@
1
+ import * as fse from "fs-extra";
2
+ import * as path from "path";
3
+ import * as os from "os";
4
+
5
+ jest.mock("uuid", () => ({
6
+ v4: () => "mock-uuid-1234",
7
+ }));
8
+
9
+ import { ConfigBase } from "./ConfigBase";
10
+
11
+ /** Concrete subclass for testing. */
12
+ class TestConfig extends ConfigBase {
13
+ public MY_SETTING = "default_value";
14
+ public SECRET_KEY = "secret_default";
15
+
16
+ constructor(configFile?: string) {
17
+ super("test-service", configFile);
18
+ this.addConfigField({ field: "MY_SETTING" });
19
+ this.addConfigField({ field: "SECRET_KEY", sensitive: true });
20
+ }
21
+ }
22
+
23
+ describe("ConfigBase", () => {
24
+ let tmpDir: string;
25
+ let configPath: string;
26
+
27
+ beforeEach(() => {
28
+ tmpDir = fse.mkdtempSync(path.join(os.tmpdir(), "config-test-"));
29
+ configPath = path.join(tmpDir, "config.json");
30
+ });
31
+
32
+ afterEach(() => {
33
+ fse.removeSync(tmpDir);
34
+ // Clean up env vars that tests may have set
35
+ delete process.env.MY_SETTING;
36
+ delete process.env.SECRET_KEY;
37
+ delete process.env.LOG_LEVEL;
38
+ delete process.env.DATA_DIR;
39
+ });
40
+
41
+ it("should initialise with default values", () => {
42
+ const config = new TestConfig(configPath);
43
+ expect(config.SERVICE_ID).toBe("test-service");
44
+ expect(config.MY_SETTING).toBe("default_value");
45
+ expect(config.SECRET_KEY).toBe("secret_default");
46
+ expect(config.LOG_LEVEL).toBe("info");
47
+ expect(config.DATABASE_TYPE).toBe("sqlite");
48
+ });
49
+
50
+ it("should load values from config.json", async () => {
51
+ fse.writeJsonSync(configPath, {
52
+ MY_SETTING: "from_file",
53
+ LOG_LEVEL: "debug",
54
+ });
55
+ const config = new TestConfig(configPath);
56
+ await config.reload();
57
+ expect(config.MY_SETTING).toBe("from_file");
58
+ expect(config.LOG_LEVEL).toBe("debug");
59
+ });
60
+
61
+ it("should prefer environment variables over config.json", async () => {
62
+ fse.writeJsonSync(configPath, {
63
+ MY_SETTING: "from_file",
64
+ });
65
+ process.env.MY_SETTING = "from_env";
66
+ const config = new TestConfig(configPath);
67
+ await config.reload();
68
+ expect(config.MY_SETTING).toBe("from_env");
69
+ });
70
+
71
+ it("should mask sensitive fields in log output", async () => {
72
+ const logs: string[] = [];
73
+ const config = new TestConfig(configPath);
74
+ await config.reload((msg) => logs.push(msg));
75
+ const secretLog = logs.find((l) => l.includes("SECRET_KEY"));
76
+ expect(secretLog).toContain("********************");
77
+ expect(secretLog).not.toContain("secret_default");
78
+ });
79
+
80
+ it("should not mask non-sensitive fields in log output", async () => {
81
+ const logs: string[] = [];
82
+ const config = new TestConfig(configPath);
83
+ await config.reload((msg) => logs.push(msg));
84
+ const settingLog = logs.find((l) => l.includes("MY_SETTING"));
85
+ expect(settingLog).toContain("default_value");
86
+ });
87
+
88
+ it("should survive missing config.json gracefully", async () => {
89
+ const config = new TestConfig("/nonexistent/path/config.json");
90
+ await config.reload();
91
+ // Should keep defaults
92
+ expect(config.MY_SETTING).toBe("default_value");
93
+ });
94
+
95
+ it("should register additional fields via addConfigField", async () => {
96
+ fse.writeJsonSync(configPath, { MY_SETTING: "updated" });
97
+ const config = new TestConfig(configPath);
98
+ await config.reload();
99
+ expect(config.MY_SETTING).toBe("updated");
100
+ });
101
+
102
+ it("should handle DATABASE_TYPE field", async () => {
103
+ fse.writeJsonSync(configPath, { DATABASE_TYPE: "postgres" });
104
+ const config = new TestConfig(configPath);
105
+ await config.reload();
106
+ expect(config.DATABASE_TYPE).toBe("postgres");
107
+ });
108
+ });
@@ -0,0 +1,213 @@
1
+ import { ConfigOTelInterface } from "@devopsplaybook.io/otel-utils";
2
+ import * as fse from "fs-extra";
3
+ import { v4 as uuidv4 } from "uuid";
4
+ import path from "path";
5
+
6
+ /**
7
+ * Configuration field descriptor used by {@link ConfigBase.addConfigField}.
8
+ */
9
+ export interface ConfigFieldDef {
10
+ /** Property name on the config instance. */
11
+ field: string;
12
+ /** When `true` the value is masked in log output. */
13
+ sensitive?: boolean;
14
+ }
15
+
16
+ /**
17
+ * Database-specific configuration fields shared by every project that
18
+ * supports both SQLite and PostgreSQL backends.
19
+ */
20
+ export interface ConfigDatabaseInterface {
21
+ DATABASE_TYPE: "sqlite" | "postgres";
22
+ DATABASE_POSTGRES_HOST: string;
23
+ DATABASE_POSTGRES_PORT: number;
24
+ DATABASE_POSTGRES_USER: string;
25
+ DATABASE_POSTGRES_PASSWORD: string;
26
+ DATABASE_POSTGRES_DATABASE: string;
27
+ }
28
+
29
+ /**
30
+ * Common server configuration fields shared across projects.
31
+ */
32
+ export interface ConfigCommonInterface
33
+ extends ConfigOTelInterface, ConfigDatabaseInterface {
34
+ CONFIG_FILE: string;
35
+ API_PORT: number;
36
+ JWT_VALIDITY_DURATION: number;
37
+ CORS_POLICY_ORIGIN: string;
38
+ DATA_DIR: string;
39
+ JWT_KEY: string;
40
+ LOG_LEVEL: string;
41
+ }
42
+
43
+ /**
44
+ * Abstract base class for project configuration.
45
+ *
46
+ * Implements the three-layer override strategy used across all
47
+ * devopsplaybook.io server projects:
48
+ * 1. **Environment variable** (highest priority)
49
+ * 2. **config.json** file value
50
+ * 3. **Default** declared on the class property
51
+ *
52
+ * Subclasses add project-specific fields and call {@link addConfigField}
53
+ * inside their constructor so that {@link reload} picks them up.
54
+ *
55
+ * @example
56
+ * ```ts
57
+ * class MyConfig extends ConfigBase {
58
+ * public MY_SETTING = "default";
59
+ * constructor() {
60
+ * super("my-service");
61
+ * this.addConfigField({ field: "MY_SETTING" });
62
+ * }
63
+ * }
64
+ * ```
65
+ */
66
+ export abstract class ConfigBase implements ConfigCommonInterface {
67
+ // -- OTel fields (ConfigOTelInterface) --
68
+ public SERVICE_ID: string;
69
+ public VERSION = "1";
70
+ public OPENTELEMETRY_COLLECTOR_HTTP_TRACES = "";
71
+ public OPENTELEMETRY_COLLECTOR_HTTP_METRICS = "";
72
+ public OPENTELEMETRY_COLLECTOR_HTTP_LOGS = "";
73
+ public OPENTELEMETRY_COLLECTOR_AWS = false;
74
+ public OPENTELEMETRY_COLLECTOR_EXPORT_LOGS_INTERVAL_SECONDS = 60;
75
+ public OPENTELEMETRY_COLLECTOR_EXPORT_METRICS_INTERVAL_SECONDS = 60;
76
+ public OPENTELEMETRY_COLLECT_AUTHORIZATION_HEADER = "";
77
+
78
+ // -- Common server fields --
79
+ public CONFIG_FILE: string;
80
+ public API_PORT = 8080;
81
+ public JWT_VALIDITY_DURATION = 3 * 31 * 24 * 3600;
82
+ public CORS_POLICY_ORIGIN = "";
83
+ public DATA_DIR = process.env.DATA_DIR || "/data";
84
+ public JWT_KEY: string = uuidv4();
85
+ public LOG_LEVEL = "info";
86
+
87
+ // -- Database fields --
88
+ public DATABASE_TYPE: "sqlite" | "postgres" = "sqlite";
89
+ public DATABASE_POSTGRES_HOST = "";
90
+ public DATABASE_POSTGRES_PORT = 5432;
91
+ public DATABASE_POSTGRES_USER = "";
92
+ public DATABASE_POSTGRES_PASSWORD = "";
93
+ public DATABASE_POSTGRES_DATABASE = "";
94
+
95
+ /**
96
+ * Fields registered by subclasses (or the base) that {@link reload}
97
+ * should process. Common / DB / OTel fields are pre-registered.
98
+ */
99
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
100
+ private _fields: { field: string; sensitive: boolean; defaultValue: any }[] =
101
+ [];
102
+
103
+ /**
104
+ * @param serviceId Unique service identifier (e.g. `"cryptotrader-server"`).
105
+ * @param configFile Optional path to the JSON config file. Defaults to `"config.json"`.
106
+ */
107
+ constructor(serviceId: string, configFile?: string) {
108
+ this.SERVICE_ID = serviceId;
109
+ this.CONFIG_FILE = configFile || process.env.CONFIG_FILE || "config.json";
110
+
111
+ // Auto-detect version from nearest package.json
112
+ try {
113
+ const pkg = fse.readJsonSync(path.resolve(__dirname, "../package.json"));
114
+ if (pkg && pkg.version) {
115
+ this.VERSION = pkg.version;
116
+ }
117
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
118
+ } catch (_e) {
119
+ // fallback to "1"
120
+ }
121
+
122
+ // Pre-register base + DB + OTel fields so reload() handles them
123
+ const baseFields: ConfigFieldDef[] = [
124
+ { field: "JWT_VALIDITY_DURATION" },
125
+ { field: "CORS_POLICY_ORIGIN" },
126
+ { field: "DATA_DIR" },
127
+ { field: "JWT_KEY", sensitive: true },
128
+ { field: "LOG_LEVEL" },
129
+ { field: "DATABASE_TYPE" },
130
+ { field: "DATABASE_POSTGRES_HOST" },
131
+ { field: "DATABASE_POSTGRES_PORT" },
132
+ { field: "DATABASE_POSTGRES_USER" },
133
+ { field: "DATABASE_POSTGRES_PASSWORD", sensitive: true },
134
+ { field: "DATABASE_POSTGRES_DATABASE" },
135
+ { field: "OPENTELEMETRY_COLLECTOR_HTTP_TRACES" },
136
+ { field: "OPENTELEMETRY_COLLECTOR_HTTP_METRICS" },
137
+ { field: "OPENTELEMETRY_COLLECTOR_HTTP_LOGS" },
138
+ { field: "OPENTELEMETRY_COLLECTOR_AWS" },
139
+ {
140
+ field: "OPENTELEMETRY_COLLECTOR_EXPORT_LOGS_INTERVAL_SECONDS",
141
+ },
142
+ {
143
+ field: "OPENTELEMETRY_COLLECTOR_EXPORT_METRICS_INTERVAL_SECONDS",
144
+ },
145
+ {
146
+ field: "OPENTELEMETRY_COLLECT_AUTHORIZATION_HEADER",
147
+ sensitive: true,
148
+ },
149
+ ];
150
+ for (const f of baseFields) {
151
+ this.addConfigField(f);
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Register a configuration field so that {@link reload} processes it.
157
+ * Call this in your subclass constructor for every project-specific field.
158
+ */
159
+ public addConfigField(def: ConfigFieldDef): void {
160
+ this._fields.push({
161
+ field: def.field,
162
+ sensitive: def.sensitive ?? false,
163
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
164
+ defaultValue: (this as any)[def.field],
165
+ });
166
+ }
167
+
168
+ /**
169
+ * Load (or reload) configuration from the JSON file and environment variables.
170
+ * Environment variables always take precedence over file values.
171
+ *
172
+ * @param logger Optional log callback `(message: string) => void`.
173
+ * When omitted nothing is logged (useful in tests).
174
+ */
175
+ public async reload(logger?: (message: string) => void): Promise<void> {
176
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
177
+ const log = logger ?? (() => {});
178
+ let content: Record<string, unknown> = {};
179
+ try {
180
+ content = await fse.readJson(this.CONFIG_FILE);
181
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
182
+ } catch (_e) {
183
+ // config file is optional – fall back to env + defaults
184
+ }
185
+
186
+ log(`Configuration Value: CONFIG_FILE: ${this.CONFIG_FILE}`);
187
+ log(`Configuration Value: SERVICE_ID: ${this.SERVICE_ID}`);
188
+ log(`Configuration Value: VERSION: ${this.VERSION}`);
189
+
190
+ for (const { field, sensitive } of this._fields) {
191
+ let from = "defaults";
192
+ if (process.env[field] !== undefined) {
193
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
194
+ (this as any)[field] = process.env[field];
195
+ from = "environment";
196
+ } else if (content[field] !== undefined) {
197
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
198
+ (this as any)[field] = content[field];
199
+ from = "config";
200
+ }
201
+ if (sensitive) {
202
+ log(
203
+ `Configuration Value: ${field}: ******************** (from ${from})`,
204
+ );
205
+ } else {
206
+ log(
207
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
208
+ `Configuration Value: ${field}: ${(this as any)[field]} (from ${from})`,
209
+ );
210
+ }
211
+ }
212
+ }
213
+ }
@@ -0,0 +1,23 @@
1
+ import { convertToPostgresPlaceholders } from "./DbUtils";
2
+
3
+ describe("convertToPostgresPlaceholders", () => {
4
+ it("should convert single ?", () => {
5
+ expect(convertToPostgresPlaceholders("SELECT * FROM t WHERE id = ?")).toBe(
6
+ "SELECT * FROM t WHERE id = $1",
7
+ );
8
+ });
9
+
10
+ it("should convert multiple ? to $1, $2, ...", () => {
11
+ expect(
12
+ convertToPostgresPlaceholders("INSERT INTO t (a,b,c) VALUES (?,?,?)"),
13
+ ).toBe("INSERT INTO t (a,b,c) VALUES ($1,$2,$3)");
14
+ });
15
+
16
+ it("should return SQL unchanged when no ? present", () => {
17
+ expect(convertToPostgresPlaceholders("SELECT 1")).toBe("SELECT 1");
18
+ });
19
+
20
+ it("should handle empty string", () => {
21
+ expect(convertToPostgresPlaceholders("")).toBe("");
22
+ });
23
+ });
package/src/DbUtils.ts ADDED
@@ -0,0 +1,118 @@
1
+ import { Span } from "@opentelemetry/sdk-trace-base";
2
+ import { StandardTracer, StandardLogger } from "@devopsplaybook.io/otel-utils";
3
+ import * as SqlDbUtils from "./SqlDbUtils";
4
+ import * as PostgresDbUtils from "./PostgresDbUtils";
5
+
6
+ /**
7
+ * Configuration subset required by the unified DB facade.
8
+ */
9
+ export interface DbUtilsConfig
10
+ extends SqlDbUtils.SqlDbConfig, PostgresDbUtils.PostgresDbConfig {
11
+ DATABASE_TYPE: "sqlite" | "postgres";
12
+ }
13
+
14
+ let databaseType: "sqlite" | "postgres" = "sqlite";
15
+
16
+ /**
17
+ * Injects the OTel tracer and logger instances used by the DB layer.
18
+ * Must be called once at startup, before {@link DbUtilsInit}.
19
+ */
20
+ export function DbUtilsSetOTel(
21
+ tracer: StandardTracer,
22
+ logger: StandardLogger,
23
+ ): void {
24
+ SqlDbUtils.SqlDbUtilsSetOTel(tracer, logger);
25
+ PostgresDbUtils.PostgresDbUtilsSetOTel(tracer, logger);
26
+ }
27
+
28
+ /**
29
+ * Initialise the database layer.
30
+ *
31
+ * Dispatches to the SQLite or Postgres backend depending on
32
+ * `config.DATABASE_TYPE` and runs pending migration files from `sqlDir`.
33
+ *
34
+ * @param context Parent OTel span.
35
+ * @param config Server configuration.
36
+ * @param sqlDir Absolute path to the directory containing SQL migration files.
37
+ */
38
+ export async function DbUtilsInit(
39
+ context: Span,
40
+ config: DbUtilsConfig,
41
+ sqlDir: string,
42
+ ): Promise<void> {
43
+ databaseType = config.DATABASE_TYPE;
44
+ if (databaseType === "postgres") {
45
+ await PostgresDbUtils.PostgresDbUtilsInit(context, config, sqlDir);
46
+ } else {
47
+ await SqlDbUtils.SqlDbUtilsInit(context, config, sqlDir);
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Returns the native database handle.
53
+ * - SQLite: `better-sqlite3` `Database` instance
54
+ * - Postgres: `pg` `Pool` instance
55
+ */
56
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
57
+ export function DbUtilsGetDatabase(): any {
58
+ if (databaseType === "postgres") {
59
+ return PostgresDbUtils.PostgresDbUtilsGetPool();
60
+ }
61
+ return SqlDbUtils.SqlDbUtilsGetDatabase();
62
+ }
63
+
64
+ /** Convert SQLite `?` placeholders to PostgreSQL `$1, $2, ...` numbering. */
65
+ export function convertToPostgresPlaceholders(sql: string): string {
66
+ let paramIndex = 1;
67
+ return sql.replace(/\?/g, () => `$${paramIndex++}`);
68
+ }
69
+
70
+ /**
71
+ * Execute a write SQL statement with OTel tracing.
72
+ * Automatically converts `?` placeholders to `$N` when using Postgres.
73
+ *
74
+ * @returns Number of rows changed.
75
+ */
76
+ export function DbUtilsExecSQL(
77
+ context: Span,
78
+ sql: string,
79
+ params: unknown[] = [],
80
+ ): number | Promise<number> {
81
+ if (databaseType === "postgres") {
82
+ return PostgresDbUtils.PostgresDbUtilsExecSQL(
83
+ context,
84
+ convertToPostgresPlaceholders(sql),
85
+ params,
86
+ );
87
+ }
88
+ return SqlDbUtils.SqlDbUtilsExecSQL(context, sql, params);
89
+ }
90
+
91
+ /**
92
+ * Execute a read SQL query with OTel tracing.
93
+ * Automatically converts `?` placeholders to `$N` when using Postgres.
94
+ *
95
+ * @returns Array of row objects.
96
+ */
97
+ export function DbUtilsQuerySQL(
98
+ context: Span,
99
+ sql: string,
100
+ params: unknown[] = [],
101
+ debug = false,
102
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
103
+ ): any[] | Promise<any[]> {
104
+ if (databaseType === "postgres") {
105
+ return PostgresDbUtils.PostgresDbUtilsQuerySQL(
106
+ context,
107
+ convertToPostgresPlaceholders(sql),
108
+ params,
109
+ debug,
110
+ );
111
+ }
112
+ return SqlDbUtils.SqlDbUtilsQuerySQL(context, sql, params, debug);
113
+ }
114
+
115
+ /** Returns the active database type (`"sqlite"` or `"postgres"`). */
116
+ export function DbUtilsGetType(): "sqlite" | "postgres" {
117
+ return databaseType;
118
+ }