@devopsplaybook.io/common-utils 1.10.1-beta.23.b1906d7 → 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
package/src/ConfigBase.ts
DELETED
|
@@ -1,297 +0,0 @@
|
|
|
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
|
-
* Alternative environment variable names to check when the primary field
|
|
16
|
-
* name is not found in `process.env`. Aliases are tried in order and the
|
|
17
|
-
* first match wins. Only checked in the environment layer, never in the
|
|
18
|
-
* config-file layer.
|
|
19
|
-
*
|
|
20
|
-
* @example
|
|
21
|
-
* ```ts
|
|
22
|
-
* // Look for DATABASE_POSTGRES_HOST first, then fall back to POSTGRES_HOST
|
|
23
|
-
* { field: "DATABASE_POSTGRES_HOST", envAliases: ["POSTGRES_HOST"] }
|
|
24
|
-
* ```
|
|
25
|
-
*/
|
|
26
|
-
envAliases?: string[];
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Database-specific configuration fields shared by every project that
|
|
31
|
-
* supports both SQLite and PostgreSQL backends.
|
|
32
|
-
*/
|
|
33
|
-
export interface ConfigDatabaseInterface {
|
|
34
|
-
DATABASE_TYPE: "sqlite" | "postgres";
|
|
35
|
-
DATABASE_POSTGRES_HOST: string;
|
|
36
|
-
DATABASE_POSTGRES_PORT: number;
|
|
37
|
-
DATABASE_POSTGRES_USER: string;
|
|
38
|
-
DATABASE_POSTGRES_PASSWORD: string;
|
|
39
|
-
DATABASE_POSTGRES_DATABASE: string;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Common server configuration fields shared across projects.
|
|
44
|
-
*/
|
|
45
|
-
export interface ConfigCommonInterface
|
|
46
|
-
extends ConfigOTelInterface, ConfigDatabaseInterface {
|
|
47
|
-
CONFIG_FILE: string;
|
|
48
|
-
API_PORT: number;
|
|
49
|
-
JWT_VALIDITY_DURATION: number;
|
|
50
|
-
CORS_POLICY_ORIGIN: string;
|
|
51
|
-
DATA_DIR: string;
|
|
52
|
-
JWT_KEY: string;
|
|
53
|
-
LOG_LEVEL: string;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Coerce a string value read from an environment variable to match the
|
|
58
|
-
* type of the default value (number → parseFloat, boolean → "true"/"1",
|
|
59
|
-
* array → JSON.parse, etc.). When the default is already a string or
|
|
60
|
-
* there is no default, the original string is returned as-is.
|
|
61
|
-
*/
|
|
62
|
-
function coerceValue(value: string, defaultValue: any): any {
|
|
63
|
-
if (defaultValue === undefined || defaultValue === null) {
|
|
64
|
-
return value;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
switch (typeof defaultValue) {
|
|
68
|
-
case "number": {
|
|
69
|
-
const parsed = Number(value);
|
|
70
|
-
return Number.isNaN(parsed) ? value : parsed;
|
|
71
|
-
}
|
|
72
|
-
case "boolean": {
|
|
73
|
-
return value === "true" || value === "1" || value === "yes";
|
|
74
|
-
}
|
|
75
|
-
case "object": {
|
|
76
|
-
if (Array.isArray(defaultValue)) {
|
|
77
|
-
try {
|
|
78
|
-
return JSON.parse(value);
|
|
79
|
-
} catch {
|
|
80
|
-
return value;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
if (defaultValue !== null) {
|
|
84
|
-
try {
|
|
85
|
-
return JSON.parse(value);
|
|
86
|
-
} catch {
|
|
87
|
-
return value;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
return value;
|
|
91
|
-
}
|
|
92
|
-
default:
|
|
93
|
-
return value;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Abstract base class for project configuration.
|
|
99
|
-
*
|
|
100
|
-
* Implements the three-layer override strategy used across all
|
|
101
|
-
* devopsplaybook.io server projects:
|
|
102
|
-
* 1. **Environment variable** (highest priority)
|
|
103
|
-
* 2. **config.json** file value
|
|
104
|
-
* 3. **Default** declared on the class property
|
|
105
|
-
*
|
|
106
|
-
* Subclasses add project-specific fields and call {@link addConfigField}
|
|
107
|
-
* inside their constructor so that {@link reload} picks them up.
|
|
108
|
-
*
|
|
109
|
-
* @example
|
|
110
|
-
* ```ts
|
|
111
|
-
* class MyConfig extends ConfigBase {
|
|
112
|
-
* public MY_SETTING = "default";
|
|
113
|
-
* constructor() {
|
|
114
|
-
* super("my-service");
|
|
115
|
-
* this.addConfigField({ field: "MY_SETTING" });
|
|
116
|
-
* }
|
|
117
|
-
* }
|
|
118
|
-
* ```
|
|
119
|
-
*/
|
|
120
|
-
export abstract class ConfigBase implements ConfigCommonInterface {
|
|
121
|
-
// -- OTel fields (ConfigOTelInterface) --
|
|
122
|
-
public SERVICE_ID: string;
|
|
123
|
-
public VERSION = "1";
|
|
124
|
-
public OPENTELEMETRY_COLLECTOR_HTTP_TRACES = "";
|
|
125
|
-
public OPENTELEMETRY_COLLECTOR_HTTP_METRICS = "";
|
|
126
|
-
public OPENTELEMETRY_COLLECTOR_HTTP_LOGS = "";
|
|
127
|
-
public OPENTELEMETRY_COLLECTOR_AWS = false;
|
|
128
|
-
public OPENTELEMETRY_COLLECTOR_EXPORT_LOGS_INTERVAL_SECONDS = 60;
|
|
129
|
-
public OPENTELEMETRY_COLLECTOR_EXPORT_METRICS_INTERVAL_SECONDS = 60;
|
|
130
|
-
public OPENTELEMETRY_COLLECT_AUTHORIZATION_HEADER = "";
|
|
131
|
-
|
|
132
|
-
// -- Common server fields --
|
|
133
|
-
public CONFIG_FILE: string;
|
|
134
|
-
public API_PORT = 8080;
|
|
135
|
-
public JWT_VALIDITY_DURATION = 3 * 31 * 24 * 3600;
|
|
136
|
-
public CORS_POLICY_ORIGIN = "";
|
|
137
|
-
public DATA_DIR = process.env.DATA_DIR || "/data";
|
|
138
|
-
public JWT_KEY: string = uuidv4();
|
|
139
|
-
public LOG_LEVEL = "info";
|
|
140
|
-
|
|
141
|
-
// -- Database fields --
|
|
142
|
-
public DATABASE_TYPE: "sqlite" | "postgres" = "sqlite";
|
|
143
|
-
public DATABASE_POSTGRES_HOST = "";
|
|
144
|
-
public DATABASE_POSTGRES_PORT = 5432;
|
|
145
|
-
public DATABASE_POSTGRES_USER = "";
|
|
146
|
-
public DATABASE_POSTGRES_PASSWORD = "";
|
|
147
|
-
public DATABASE_POSTGRES_DATABASE = "";
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Fields registered by subclasses (or the base) that {@link reload}
|
|
151
|
-
* should process. Common / DB / OTel fields are pre-registered.
|
|
152
|
-
*/
|
|
153
|
-
private _fields: {
|
|
154
|
-
field: string;
|
|
155
|
-
sensitive: boolean;
|
|
156
|
-
envAliases: string[];
|
|
157
|
-
defaultValue: any;
|
|
158
|
-
}[] = [];
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* @param serviceId Unique service identifier (e.g. `"cryptotrader-server"`).
|
|
162
|
-
* @param configFile Optional path to the JSON config file. Defaults to `"config.json"`.
|
|
163
|
-
*/
|
|
164
|
-
constructor(serviceId: string, configFile?: string) {
|
|
165
|
-
this.SERVICE_ID = serviceId;
|
|
166
|
-
this.CONFIG_FILE = configFile || process.env.CONFIG_FILE || "config.json";
|
|
167
|
-
|
|
168
|
-
// Auto-detect version from nearest package.json
|
|
169
|
-
try {
|
|
170
|
-
const pkg = fse.readJsonSync(path.resolve(__dirname, "../package.json"));
|
|
171
|
-
if (pkg && pkg.version) {
|
|
172
|
-
this.VERSION = pkg.version;
|
|
173
|
-
}
|
|
174
|
-
// eslint-disable-next-line no-unused-vars -- catch binding kept so dist/ stays byte-identical to the TS6 build
|
|
175
|
-
} catch (_e) {
|
|
176
|
-
// fallback to "1"
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Pre-register base + DB + OTel fields so reload() handles them
|
|
180
|
-
const baseFields: ConfigFieldDef[] = [
|
|
181
|
-
{ field: "JWT_VALIDITY_DURATION" },
|
|
182
|
-
{ field: "CORS_POLICY_ORIGIN" },
|
|
183
|
-
{ field: "DATA_DIR" },
|
|
184
|
-
{ field: "JWT_KEY", sensitive: true },
|
|
185
|
-
{ field: "LOG_LEVEL" },
|
|
186
|
-
{ field: "DATABASE_TYPE" },
|
|
187
|
-
{ field: "DATABASE_POSTGRES_HOST", envAliases: ["POSTGRES_HOST"] },
|
|
188
|
-
{ field: "DATABASE_POSTGRES_PORT", envAliases: ["POSTGRES_PORT"] },
|
|
189
|
-
{ field: "DATABASE_POSTGRES_USER", envAliases: ["POSTGRES_USER"] },
|
|
190
|
-
{
|
|
191
|
-
field: "DATABASE_POSTGRES_PASSWORD",
|
|
192
|
-
sensitive: true,
|
|
193
|
-
envAliases: ["POSTGRES_PASSWORD"],
|
|
194
|
-
},
|
|
195
|
-
{ field: "DATABASE_POSTGRES_DATABASE", envAliases: ["POSTGRES_DB"] },
|
|
196
|
-
{ field: "OPENTELEMETRY_COLLECTOR_HTTP_TRACES" },
|
|
197
|
-
{ field: "OPENTELEMETRY_COLLECTOR_HTTP_METRICS" },
|
|
198
|
-
{ field: "OPENTELEMETRY_COLLECTOR_HTTP_LOGS" },
|
|
199
|
-
{ field: "OPENTELEMETRY_COLLECTOR_AWS" },
|
|
200
|
-
{
|
|
201
|
-
field: "OPENTELEMETRY_COLLECTOR_EXPORT_LOGS_INTERVAL_SECONDS",
|
|
202
|
-
},
|
|
203
|
-
{
|
|
204
|
-
field: "OPENTELEMETRY_COLLECTOR_EXPORT_METRICS_INTERVAL_SECONDS",
|
|
205
|
-
},
|
|
206
|
-
{
|
|
207
|
-
field: "OPENTELEMETRY_COLLECT_AUTHORIZATION_HEADER",
|
|
208
|
-
sensitive: true,
|
|
209
|
-
},
|
|
210
|
-
];
|
|
211
|
-
for (const f of baseFields) {
|
|
212
|
-
this.addConfigField(f);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Register a configuration field so that {@link reload} processes it.
|
|
218
|
-
* Call this in your subclass constructor for every project-specific field.
|
|
219
|
-
*/
|
|
220
|
-
public addConfigField(def: ConfigFieldDef): void {
|
|
221
|
-
this._fields.push({
|
|
222
|
-
field: def.field,
|
|
223
|
-
sensitive: def.sensitive ?? false,
|
|
224
|
-
envAliases: def.envAliases ?? [],
|
|
225
|
-
defaultValue: (this as any)[def.field],
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Load (or reload) configuration from the JSON file and environment variables.
|
|
231
|
-
* Environment variables always take precedence over file values.
|
|
232
|
-
*
|
|
233
|
-
* @param logger Optional log callback `(message: string) => void`.
|
|
234
|
-
* When omitted nothing is logged (useful in tests).
|
|
235
|
-
*/
|
|
236
|
-
public async reload(logger?: (message: string) => void): Promise<void> {
|
|
237
|
-
const log = logger ?? (() => {});
|
|
238
|
-
let content: Record<string, unknown> = {};
|
|
239
|
-
try {
|
|
240
|
-
content = await fse.readJson(this.CONFIG_FILE);
|
|
241
|
-
// eslint-disable-next-line no-unused-vars -- catch binding kept so dist/ stays byte-identical to the TS6 build
|
|
242
|
-
} catch (_e) {
|
|
243
|
-
// config file is optional – fall back to env + defaults
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
log(`Configuration Value: CONFIG_FILE: ${this.CONFIG_FILE}`);
|
|
247
|
-
log(`Configuration Value: SERVICE_ID: ${this.SERVICE_ID}`);
|
|
248
|
-
log(`Configuration Value: VERSION: ${this.VERSION}`);
|
|
249
|
-
|
|
250
|
-
for (const { field, sensitive, envAliases, defaultValue } of this._fields) {
|
|
251
|
-
let from = "defaults";
|
|
252
|
-
let foundValue: string | undefined;
|
|
253
|
-
let usedAlias = false;
|
|
254
|
-
|
|
255
|
-
// 1. Primary environment variable name
|
|
256
|
-
if (process.env[field] !== undefined) {
|
|
257
|
-
foundValue = process.env[field];
|
|
258
|
-
from = "environment";
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// 2. Check aliases if primary env var was not set
|
|
262
|
-
if (foundValue === undefined && envAliases.length > 0) {
|
|
263
|
-
for (const alias of envAliases) {
|
|
264
|
-
if (process.env[alias] !== undefined) {
|
|
265
|
-
foundValue = process.env[alias];
|
|
266
|
-
from = "environment";
|
|
267
|
-
usedAlias = true;
|
|
268
|
-
break;
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// 3. Apply environment value (full name or alias) if found,
|
|
274
|
-
// coercing strings to match the default value type
|
|
275
|
-
if (foundValue !== undefined) {
|
|
276
|
-
(this as any)[field] = coerceValue(foundValue, defaultValue);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// 4. Config file override (environment always wins, but if neither
|
|
280
|
-
// environment nor alias matched, check config file)
|
|
281
|
-
if (foundValue === undefined && content[field] !== undefined) {
|
|
282
|
-
(this as any)[field] = content[field];
|
|
283
|
-
from = "config";
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
if (sensitive) {
|
|
287
|
-
log(
|
|
288
|
-
`Configuration Value: ${field}: ******************** (from ${from})`,
|
|
289
|
-
);
|
|
290
|
-
} else {
|
|
291
|
-
log(
|
|
292
|
-
`Configuration Value: ${field}: ${(this as any)[field]} (from ${from}${usedAlias ? ` via alias` : ""})`,
|
|
293
|
-
);
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
}
|
package/src/DbUtils.spec.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
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
|
-
export function DbUtilsGetDatabase(): any {
|
|
57
|
-
if (databaseType === "postgres") {
|
|
58
|
-
return PostgresDbUtils.PostgresDbUtilsGetPool();
|
|
59
|
-
}
|
|
60
|
-
return SqlDbUtils.SqlDbUtilsGetDatabase();
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** Convert SQLite `?` placeholders to PostgreSQL `$1, $2, ...` numbering. */
|
|
64
|
-
export function convertToPostgresPlaceholders(sql: string): string {
|
|
65
|
-
let paramIndex = 1;
|
|
66
|
-
return sql.replace(/\?/g, () => `$${paramIndex++}`);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Execute a write SQL statement with OTel tracing.
|
|
71
|
-
* Automatically converts `?` placeholders to `$N` when using Postgres.
|
|
72
|
-
*
|
|
73
|
-
* @returns Number of rows changed.
|
|
74
|
-
*/
|
|
75
|
-
export function DbUtilsExecSQL(
|
|
76
|
-
context: Span,
|
|
77
|
-
sql: string,
|
|
78
|
-
params: unknown[] = [],
|
|
79
|
-
): number | Promise<number> {
|
|
80
|
-
if (databaseType === "postgres") {
|
|
81
|
-
return PostgresDbUtils.PostgresDbUtilsExecSQL(
|
|
82
|
-
context,
|
|
83
|
-
convertToPostgresPlaceholders(sql),
|
|
84
|
-
params,
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
return SqlDbUtils.SqlDbUtilsExecSQL(context, sql, params);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Execute a read SQL query with OTel tracing.
|
|
92
|
-
* Automatically converts `?` placeholders to `$N` when using Postgres.
|
|
93
|
-
*
|
|
94
|
-
* @returns Array of row objects.
|
|
95
|
-
*/
|
|
96
|
-
export function DbUtilsQuerySQL(
|
|
97
|
-
context: Span,
|
|
98
|
-
sql: string,
|
|
99
|
-
params: unknown[] = [],
|
|
100
|
-
debug = false,
|
|
101
|
-
): any[] | Promise<any[]> {
|
|
102
|
-
if (databaseType === "postgres") {
|
|
103
|
-
return PostgresDbUtils.PostgresDbUtilsQuerySQL(
|
|
104
|
-
context,
|
|
105
|
-
convertToPostgresPlaceholders(sql),
|
|
106
|
-
params,
|
|
107
|
-
debug,
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
return SqlDbUtils.SqlDbUtilsQuerySQL(context, sql, params, debug);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/** Returns the active database type (`"sqlite"` or `"postgres"`). */
|
|
114
|
-
export function DbUtilsGetType(): "sqlite" | "postgres" {
|
|
115
|
-
return databaseType;
|
|
116
|
-
}
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DbUtilsNoTelemetryBatchInsert,
|
|
3
|
-
DbUtilsNoTelemetryExecSQL,
|
|
4
|
-
DbUtilsNoTelemetryQuerySQL,
|
|
5
|
-
} from "./DbUtilsNoTelemetry";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
// Shared mock DB handle – defined BEFORE jest.mock factory so it's hoisted correctly.
|
|
9
|
-
// We use jest.fn() at module scope; the mock factory captures the same reference.
|
|
10
|
-
const mockPrepare = jest.fn(() => ({
|
|
11
|
-
run: jest.fn().mockReturnValue({ changes: 0 }),
|
|
12
|
-
all: jest.fn().mockReturnValue([]),
|
|
13
|
-
}));
|
|
14
|
-
const mockQuery = jest.fn();
|
|
15
|
-
const mockDbHandle = { prepare: mockPrepare, query: mockQuery };
|
|
16
|
-
|
|
17
|
-
let currentDbType: "sqlite" | "postgres" = "sqlite";
|
|
18
|
-
|
|
19
|
-
jest.mock("./DbUtils", () => ({
|
|
20
|
-
DbUtilsGetDatabase: jest.fn(() => mockDbHandle),
|
|
21
|
-
DbUtilsGetType: jest.fn(() => currentDbType),
|
|
22
|
-
convertToPostgresPlaceholders: jest.fn((sql: string) => {
|
|
23
|
-
let idx = 1;
|
|
24
|
-
return sql.replace(/\?/g, () => `$${idx++}`);
|
|
25
|
-
}),
|
|
26
|
-
}));
|
|
27
|
-
|
|
28
|
-
jest.mock("@devopsplaybook.io/otel-utils", () => ({
|
|
29
|
-
ModuleLogger: jest.fn(),
|
|
30
|
-
StandardLogger: jest.fn(),
|
|
31
|
-
}));
|
|
32
|
-
|
|
33
|
-
import * as DbUtilsNoTelemetryModule from "./DbUtilsNoTelemetry";
|
|
34
|
-
|
|
35
|
-
beforeAll(() => {
|
|
36
|
-
const mockLogger = {
|
|
37
|
-
error: jest.fn(),
|
|
38
|
-
info: jest.fn(),
|
|
39
|
-
warn: jest.fn(),
|
|
40
|
-
} as any;
|
|
41
|
-
DbUtilsNoTelemetryModule.DbUtilsNoTelemetrySetLogger({
|
|
42
|
-
createModuleLogger: () => mockLogger,
|
|
43
|
-
} as any);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
beforeEach(() => {
|
|
47
|
-
jest.clearAllMocks();
|
|
48
|
-
currentDbType = "sqlite";
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
describe("DbUtilsNoTelemetryBatchInsert", () => {
|
|
52
|
-
it("returns 0 for empty rows", () => {
|
|
53
|
-
const result = DbUtilsNoTelemetryBatchInsert("INTO t (c)", 1, []);
|
|
54
|
-
expect(result).toBe(0);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("generates correct multi-row VALUES SQL (sqlite)", () => {
|
|
58
|
-
currentDbType = "sqlite";
|
|
59
|
-
mockPrepare.mockReturnValue({
|
|
60
|
-
run: jest.fn().mockReturnValue({ changes: 2 }),
|
|
61
|
-
all: jest.fn(),
|
|
62
|
-
} as any);
|
|
63
|
-
|
|
64
|
-
const rows = [
|
|
65
|
-
["a1", "b1"],
|
|
66
|
-
["a2", "b2"],
|
|
67
|
-
];
|
|
68
|
-
const result = DbUtilsNoTelemetryBatchInsert("INTO t (c1,c2)", 2, rows);
|
|
69
|
-
expect(result).toBe(2);
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
describe("DbUtilsNoTelemetryExecSQL (sqlite)", () => {
|
|
74
|
-
beforeEach(() => {
|
|
75
|
-
currentDbType = "sqlite";
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it("resolves with changes count on success", () => {
|
|
79
|
-
mockPrepare.mockReturnValue({
|
|
80
|
-
run: jest.fn().mockReturnValue({ changes: 3 }),
|
|
81
|
-
all: jest.fn(),
|
|
82
|
-
} as any);
|
|
83
|
-
|
|
84
|
-
const result = DbUtilsNoTelemetryExecSQL("INSERT INTO t (c) VALUES (?)", [
|
|
85
|
-
"x",
|
|
86
|
-
]);
|
|
87
|
-
expect(result).toBe(3);
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
describe("DbUtilsNoTelemetryExecSQL (postgres)", () => {
|
|
92
|
-
beforeEach(() => {
|
|
93
|
-
currentDbType = "postgres";
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it("resolves with rowCount on success", async () => {
|
|
97
|
-
mockQuery.mockImplementation(
|
|
98
|
-
(_sql: string, _params: unknown[], cb: Function) => {
|
|
99
|
-
cb(null, { rowCount: 5 });
|
|
100
|
-
},
|
|
101
|
-
);
|
|
102
|
-
|
|
103
|
-
const result = await DbUtilsNoTelemetryExecSQL(
|
|
104
|
-
"INSERT INTO t (c) VALUES (?)",
|
|
105
|
-
["x"],
|
|
106
|
-
);
|
|
107
|
-
expect(result).toBe(5);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it("rejects on error", async () => {
|
|
111
|
-
mockQuery.mockImplementation(
|
|
112
|
-
(_sql: string, _params: unknown[], cb: Function) => {
|
|
113
|
-
cb(new Error("deadlock detected"));
|
|
114
|
-
},
|
|
115
|
-
);
|
|
116
|
-
|
|
117
|
-
await expect(
|
|
118
|
-
DbUtilsNoTelemetryExecSQL("INSERT INTO t (c) VALUES (?)", ["x"]),
|
|
119
|
-
).rejects.toThrow("deadlock detected");
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
describe("DbUtilsNoTelemetryQuerySQL (sqlite)", () => {
|
|
124
|
-
beforeEach(() => {
|
|
125
|
-
currentDbType = "sqlite";
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it("returns rows on success", () => {
|
|
129
|
-
const expectedRows = [{ id: 1 }, { id: 2 }];
|
|
130
|
-
mockPrepare.mockReturnValue({
|
|
131
|
-
run: jest.fn(),
|
|
132
|
-
all: jest.fn().mockReturnValue(expectedRows),
|
|
133
|
-
} as any);
|
|
134
|
-
|
|
135
|
-
const result = DbUtilsNoTelemetryQuerySQL("SELECT * FROM t");
|
|
136
|
-
expect(result).toEqual(expectedRows);
|
|
137
|
-
});
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
describe("DbUtilsNoTelemetryQuerySQL (postgres)", () => {
|
|
141
|
-
beforeEach(() => {
|
|
142
|
-
currentDbType = "postgres";
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it("returns rows on success", async () => {
|
|
146
|
-
const expectedRows = [{ id: 1 }, { id: 2 }];
|
|
147
|
-
mockQuery.mockImplementation(
|
|
148
|
-
(_sql: string, _params: unknown[], cb: Function) => {
|
|
149
|
-
cb(null, { rows: expectedRows });
|
|
150
|
-
},
|
|
151
|
-
);
|
|
152
|
-
|
|
153
|
-
const result = await DbUtilsNoTelemetryQuerySQL("SELECT * FROM t");
|
|
154
|
-
expect(result).toEqual(expectedRows);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it("rejects on error", async () => {
|
|
158
|
-
mockQuery.mockImplementation(
|
|
159
|
-
(_sql: string, _params: unknown[], cb: Function) => {
|
|
160
|
-
cb(new Error("connection lost"));
|
|
161
|
-
},
|
|
162
|
-
);
|
|
163
|
-
|
|
164
|
-
await expect(DbUtilsNoTelemetryQuerySQL("SELECT * FROM t")).rejects.toThrow(
|
|
165
|
-
"connection lost",
|
|
166
|
-
);
|
|
167
|
-
});
|
|
168
|
-
});
|