@nmakarov/cli-toolkit 0.2.0 → 0.4.0
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 +112 -1
- package/dist/db.cjs +351 -0
- package/dist/db.cjs.map +1 -0
- package/dist/db.js +314 -0
- package/dist/db.js.map +1 -0
- package/dist/{filestore.cjs → filedatabase.cjs} +191 -37
- package/dist/filedatabase.cjs.map +1 -0
- package/dist/{filestore.js → filedatabase.js} +187 -33
- package/dist/filedatabase.js.map +1 -0
- package/dist/http-client.cjs +356 -0
- package/dist/http-client.cjs.map +1 -0
- package/dist/http-client.js +318 -0
- package/dist/http-client.js.map +1 -0
- package/dist/index.cjs +483 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +482 -32
- package/dist/index.js.map +1 -1
- package/dist/mock-server.cjs +1483 -0
- package/dist/mock-server.cjs.map +1 -0
- package/dist/mock-server.js +1445 -0
- package/dist/mock-server.js.map +1 -0
- package/package.json +49 -11
- package/dist/filestore.cjs.map +0 -1
- package/dist/filestore.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -31,6 +31,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var src_exports = {};
|
|
32
32
|
__export(src_exports, {
|
|
33
33
|
Args: () => Args,
|
|
34
|
+
Db: () => Db,
|
|
34
35
|
Divider: () => Divider,
|
|
35
36
|
FileDatabase: () => FileDatabase,
|
|
36
37
|
FileDatabaseError: () => FileDatabaseError,
|
|
@@ -1777,7 +1778,7 @@ function organizeFooterMessages(messages) {
|
|
|
1777
1778
|
return lines;
|
|
1778
1779
|
}
|
|
1779
1780
|
|
|
1780
|
-
// src/
|
|
1781
|
+
// src/filedatabase/index.ts
|
|
1781
1782
|
var import_fs4 = __toESM(require("fs"), 1);
|
|
1782
1783
|
var import_path4 = __toESM(require("path"), 1);
|
|
1783
1784
|
|
|
@@ -1853,7 +1854,7 @@ function isTimestampFolder(folderName) {
|
|
|
1853
1854
|
return !isNaN(date.getTime()) && date.getTime() > 0;
|
|
1854
1855
|
}
|
|
1855
1856
|
|
|
1856
|
-
// src/
|
|
1857
|
+
// src/filedatabase/serializers.ts
|
|
1857
1858
|
function detectDataType(data) {
|
|
1858
1859
|
if (Array.isArray(data)) {
|
|
1859
1860
|
return "json-array";
|
|
@@ -1885,7 +1886,7 @@ function deserializeData(rawData, dataType) {
|
|
|
1885
1886
|
}
|
|
1886
1887
|
}
|
|
1887
1888
|
|
|
1888
|
-
// src/
|
|
1889
|
+
// src/filedatabase/synopsis-functions.ts
|
|
1889
1890
|
function defaultFileSynopsisFunction(fileEntry, data) {
|
|
1890
1891
|
if (!Array.isArray(data) || data.length === 0) {
|
|
1891
1892
|
return { ...fileEntry };
|
|
@@ -1953,7 +1954,7 @@ function defaultVersionSynopsisFunction(metadata) {
|
|
|
1953
1954
|
return result;
|
|
1954
1955
|
}
|
|
1955
1956
|
|
|
1956
|
-
// src/
|
|
1957
|
+
// src/filedatabase/index.ts
|
|
1957
1958
|
var FileDatabaseError = class extends Error {
|
|
1958
1959
|
constructor(message) {
|
|
1959
1960
|
super(message);
|
|
@@ -1964,6 +1965,7 @@ var FileDatabase = class {
|
|
|
1964
1965
|
basePath;
|
|
1965
1966
|
namespace;
|
|
1966
1967
|
tableName = null;
|
|
1968
|
+
versioned;
|
|
1967
1969
|
maxVersions;
|
|
1968
1970
|
pageSize;
|
|
1969
1971
|
useMetadata;
|
|
@@ -1987,6 +1989,7 @@ var FileDatabase = class {
|
|
|
1987
1989
|
this.basePath = config2.basePath;
|
|
1988
1990
|
this.namespace = config2.namespace || "default";
|
|
1989
1991
|
this.tableName = config2.tableName || null;
|
|
1992
|
+
this.versioned = config2.versioned ?? true;
|
|
1990
1993
|
this.maxVersions = config2.maxVersions || 5;
|
|
1991
1994
|
this.pageSize = config2.pageSize || 5e3;
|
|
1992
1995
|
this.useMetadata = config2.useMetadata !== false;
|
|
@@ -2009,14 +2012,21 @@ var FileDatabase = class {
|
|
|
2009
2012
|
};
|
|
2010
2013
|
}
|
|
2011
2014
|
/**
|
|
2012
|
-
* Get the destination path (basePath/namespace/tableName)
|
|
2015
|
+
* Get the destination path (basePath/namespace/tableName[/version])
|
|
2013
2016
|
*/
|
|
2014
|
-
getDestinationPath() {
|
|
2017
|
+
getDestinationPath(version) {
|
|
2015
2018
|
const errors = ["basePath", "namespace", "tableName"].filter((prop) => !this[prop]).map((prop) => `${prop} is not set`);
|
|
2016
2019
|
if (errors.length) {
|
|
2017
2020
|
throw new FileDatabaseError(`[FileDatabase] ${errors.join("; ")}`);
|
|
2018
2021
|
}
|
|
2019
|
-
|
|
2022
|
+
let parts = [this.basePath, this.namespace];
|
|
2023
|
+
if (this.tableName) {
|
|
2024
|
+
parts.push(...this.tableName.split("/"));
|
|
2025
|
+
}
|
|
2026
|
+
if (this.versioned && version) {
|
|
2027
|
+
parts.push(version);
|
|
2028
|
+
}
|
|
2029
|
+
return import_path4.default.resolve(...parts);
|
|
2020
2030
|
}
|
|
2021
2031
|
/**
|
|
2022
2032
|
* Set current version and version folder
|
|
@@ -2027,8 +2037,13 @@ var FileDatabase = class {
|
|
|
2027
2037
|
}
|
|
2028
2038
|
/**
|
|
2029
2039
|
* Create a new version folder with comprehensive timestamp logic
|
|
2040
|
+
* Only works in versioned mode
|
|
2030
2041
|
*/
|
|
2031
2042
|
async makeNewVersion() {
|
|
2043
|
+
if (!this.versioned) {
|
|
2044
|
+
throw new FileDatabaseError("makeNewVersion() only works in versioned mode");
|
|
2045
|
+
}
|
|
2046
|
+
this.metadata = this.getDefaultMetadata();
|
|
2032
2047
|
const existingVersions = await this.getVersions();
|
|
2033
2048
|
let versionName;
|
|
2034
2049
|
if (existingVersions.length > 0) {
|
|
@@ -2056,8 +2071,12 @@ var FileDatabase = class {
|
|
|
2056
2071
|
}
|
|
2057
2072
|
/**
|
|
2058
2073
|
* Get list of all versions (sorted chronologically)
|
|
2074
|
+
* Only works in versioned mode
|
|
2059
2075
|
*/
|
|
2060
2076
|
async getVersions() {
|
|
2077
|
+
if (!this.versioned) {
|
|
2078
|
+
return [];
|
|
2079
|
+
}
|
|
2061
2080
|
const destPath = this.getDestinationPath();
|
|
2062
2081
|
try {
|
|
2063
2082
|
await ensurePath(destPath);
|
|
@@ -2072,6 +2091,86 @@ var FileDatabase = class {
|
|
|
2072
2091
|
return [];
|
|
2073
2092
|
}
|
|
2074
2093
|
}
|
|
2094
|
+
/**
|
|
2095
|
+
* Get the latest version (most recent timestamp)
|
|
2096
|
+
* Only works in versioned mode
|
|
2097
|
+
* @returns Latest version string or null if no versions
|
|
2098
|
+
*/
|
|
2099
|
+
async getLatestVersion() {
|
|
2100
|
+
if (!this.versioned) {
|
|
2101
|
+
throw new FileDatabaseError("getLatestVersion() only works in versioned mode");
|
|
2102
|
+
}
|
|
2103
|
+
const versions = await this.getVersions();
|
|
2104
|
+
if (versions.length === 0) {
|
|
2105
|
+
return null;
|
|
2106
|
+
}
|
|
2107
|
+
return versions[versions.length - 1];
|
|
2108
|
+
}
|
|
2109
|
+
/**
|
|
2110
|
+
* Check if any data exists in this table
|
|
2111
|
+
* Works for both versioned and non-versioned modes
|
|
2112
|
+
* @returns true if data exists
|
|
2113
|
+
*/
|
|
2114
|
+
async hasData() {
|
|
2115
|
+
const tablePath = this.getDestinationPath();
|
|
2116
|
+
if (!import_fs4.default.existsSync(tablePath)) {
|
|
2117
|
+
return false;
|
|
2118
|
+
}
|
|
2119
|
+
if (this.versioned) {
|
|
2120
|
+
const versions = await this.getVersions();
|
|
2121
|
+
return versions.length > 0;
|
|
2122
|
+
} else {
|
|
2123
|
+
const items = await import_fs4.default.promises.readdir(tablePath);
|
|
2124
|
+
return items.some(
|
|
2125
|
+
(item) => item === "metadata.json" || item.match(/^\d{6}\.(json|txt|xml)$/) || item.endsWith(".json")
|
|
2126
|
+
);
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
/**
|
|
2130
|
+
* Auto-detect the data format in this table
|
|
2131
|
+
* Used when reading existing data
|
|
2132
|
+
* @returns Format detection result
|
|
2133
|
+
*/
|
|
2134
|
+
async detectDataFormat() {
|
|
2135
|
+
const tablePath = this.getDestinationPath();
|
|
2136
|
+
if (!import_fs4.default.existsSync(tablePath)) {
|
|
2137
|
+
return { versioned: false, hasMetadata: false, dataType: null };
|
|
2138
|
+
}
|
|
2139
|
+
const items = await import_fs4.default.promises.readdir(tablePath);
|
|
2140
|
+
if (items.includes("metadata.json")) {
|
|
2141
|
+
const metadata = JSON.parse(
|
|
2142
|
+
await import_fs4.default.promises.readFile(import_path4.default.join(tablePath, "metadata.json"), "utf8")
|
|
2143
|
+
);
|
|
2144
|
+
return {
|
|
2145
|
+
versioned: false,
|
|
2146
|
+
hasMetadata: true,
|
|
2147
|
+
dataType: metadata.dataType || null
|
|
2148
|
+
};
|
|
2149
|
+
}
|
|
2150
|
+
const versionFolders = items.filter((item) => {
|
|
2151
|
+
const itemPath = import_path4.default.join(tablePath, item);
|
|
2152
|
+
const stat = import_fs4.default.statSync(itemPath);
|
|
2153
|
+
return stat.isDirectory() && isTimestampFolder(item);
|
|
2154
|
+
});
|
|
2155
|
+
if (versionFolders.length > 0) {
|
|
2156
|
+
const latestVersion = versionFolders.sort().pop();
|
|
2157
|
+
const versionMetadataPath = import_path4.default.join(tablePath, latestVersion, "metadata.json");
|
|
2158
|
+
return {
|
|
2159
|
+
versioned: true,
|
|
2160
|
+
hasMetadata: import_fs4.default.existsSync(versionMetadataPath),
|
|
2161
|
+
dataType: null
|
|
2162
|
+
};
|
|
2163
|
+
}
|
|
2164
|
+
const dataFiles = items.filter((f) => f.match(/^\d{6}\.(json|txt|xml)$/));
|
|
2165
|
+
if (dataFiles.length > 0) {
|
|
2166
|
+
return {
|
|
2167
|
+
versioned: false,
|
|
2168
|
+
hasMetadata: false,
|
|
2169
|
+
dataType: null
|
|
2170
|
+
};
|
|
2171
|
+
}
|
|
2172
|
+
return { versioned: false, hasMetadata: false, dataType: null };
|
|
2173
|
+
}
|
|
2075
2174
|
/**
|
|
2076
2175
|
* Load metadata from JSON file
|
|
2077
2176
|
*/
|
|
@@ -2215,11 +2314,19 @@ var FileDatabase = class {
|
|
|
2215
2314
|
* Save version metadata to file
|
|
2216
2315
|
*/
|
|
2217
2316
|
async saveVersionMetadata(metadata) {
|
|
2218
|
-
if (!this.useMetadata
|
|
2317
|
+
if (!this.useMetadata) {
|
|
2219
2318
|
return;
|
|
2220
2319
|
}
|
|
2221
2320
|
const metadataToSave = metadata || this.metadata;
|
|
2222
|
-
|
|
2321
|
+
let metadataFile;
|
|
2322
|
+
if (this.versioned) {
|
|
2323
|
+
if (!this.currentVersion) {
|
|
2324
|
+
return;
|
|
2325
|
+
}
|
|
2326
|
+
metadataFile = import_path4.default.join(this.getDestinationPath(), this.currentVersion, "metadata.json");
|
|
2327
|
+
} else {
|
|
2328
|
+
metadataFile = import_path4.default.join(this.getDestinationPath(), "metadata.json");
|
|
2329
|
+
}
|
|
2223
2330
|
await import_fs4.default.promises.writeFile(metadataFile, JSON.stringify(metadataToSave, null, 4), "utf8");
|
|
2224
2331
|
}
|
|
2225
2332
|
/**
|
|
@@ -2347,31 +2454,75 @@ var FileDatabase = class {
|
|
|
2347
2454
|
*/
|
|
2348
2455
|
async prepare({ write, read, version }) {
|
|
2349
2456
|
if (write) {
|
|
2350
|
-
if (this.
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2457
|
+
if (this.versioned) {
|
|
2458
|
+
if (this.currentVersion === null) {
|
|
2459
|
+
await this.makeNewVersion();
|
|
2460
|
+
this.metadata = this.getDefaultMetadata();
|
|
2461
|
+
this.metadata.version = this.currentVersion;
|
|
2462
|
+
this.makeNewFile();
|
|
2463
|
+
} else {
|
|
2464
|
+
if (!this.metadata.files.length) {
|
|
2465
|
+
this.metadata = await this.figureMetadata(this.currentVersion);
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2355
2468
|
} else {
|
|
2356
|
-
|
|
2357
|
-
|
|
2469
|
+
await ensurePath(this.getDestinationPath());
|
|
2470
|
+
if (this.useMetadata === true) {
|
|
2471
|
+
const metadataPath = import_path4.default.join(this.getDestinationPath(), "metadata.json");
|
|
2472
|
+
if (import_fs4.default.existsSync(metadataPath)) {
|
|
2473
|
+
try {
|
|
2474
|
+
const rawData = await import_fs4.default.promises.readFile(metadataPath, "utf8");
|
|
2475
|
+
this.metadata = JSON.parse(rawData);
|
|
2476
|
+
} catch (e) {
|
|
2477
|
+
this.metadata = this.getDefaultMetadata();
|
|
2478
|
+
}
|
|
2479
|
+
} else {
|
|
2480
|
+
this.metadata = this.getDefaultMetadata();
|
|
2481
|
+
this.makeNewFile();
|
|
2482
|
+
}
|
|
2483
|
+
} else {
|
|
2484
|
+
this.metadata = this.getDefaultMetadata();
|
|
2485
|
+
this.makeNewFile();
|
|
2358
2486
|
}
|
|
2359
2487
|
}
|
|
2360
2488
|
} else if (read) {
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
if (
|
|
2367
|
-
|
|
2489
|
+
if (this.versioned) {
|
|
2490
|
+
const versions = await this.getVersions();
|
|
2491
|
+
if (versions.length === 0) {
|
|
2492
|
+
throw new FileDatabaseError("[FileDatabase] No versions found, cannot read");
|
|
2493
|
+
}
|
|
2494
|
+
if (version) {
|
|
2495
|
+
if (!versions.includes(version)) {
|
|
2496
|
+
throw new FileDatabaseError(`[FileDatabase] Version "${version}" not found`);
|
|
2497
|
+
}
|
|
2498
|
+
await this.setCurrentVersion(version);
|
|
2499
|
+
} else {
|
|
2500
|
+
await this.setCurrentVersion(versions[versions.length - 1]);
|
|
2501
|
+
}
|
|
2502
|
+
if (!this.metadata.files.length) {
|
|
2503
|
+
this.metadata = await this.figureMetadata(this.currentVersion);
|
|
2368
2504
|
}
|
|
2369
|
-
await this.setCurrentVersion(version);
|
|
2370
2505
|
} else {
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2506
|
+
this.currentVersion = null;
|
|
2507
|
+
if (this.useMetadata === void 0) {
|
|
2508
|
+
const format = await this.detectDataFormat();
|
|
2509
|
+
this.useMetadata = format.hasMetadata;
|
|
2510
|
+
}
|
|
2511
|
+
if (this.useMetadata) {
|
|
2512
|
+
const metadataPath = import_path4.default.join(this.getDestinationPath(), "metadata.json");
|
|
2513
|
+
if (import_fs4.default.existsSync(metadataPath)) {
|
|
2514
|
+
try {
|
|
2515
|
+
const rawData = await import_fs4.default.promises.readFile(metadataPath, "utf8");
|
|
2516
|
+
this.metadata = JSON.parse(rawData);
|
|
2517
|
+
} catch (e) {
|
|
2518
|
+
throw new FileDatabaseError(`Failed to read metadata: ${e.message}`);
|
|
2519
|
+
}
|
|
2520
|
+
} else {
|
|
2521
|
+
throw new FileDatabaseError("[FileDatabase] No metadata found in non-versioned mode");
|
|
2522
|
+
}
|
|
2523
|
+
} else {
|
|
2524
|
+
this.metadata = await this.figureMetadataFromVersionFiles("");
|
|
2525
|
+
}
|
|
2375
2526
|
}
|
|
2376
2527
|
}
|
|
2377
2528
|
}
|
|
@@ -2379,6 +2530,9 @@ var FileDatabase = class {
|
|
|
2379
2530
|
* Write data to the file database
|
|
2380
2531
|
*/
|
|
2381
2532
|
async write(data, options = {}) {
|
|
2533
|
+
if (options.forceNewVersion && !this.versioned) {
|
|
2534
|
+
throw new FileDatabaseError("Cannot use forceNewVersion in non-versioned mode");
|
|
2535
|
+
}
|
|
2382
2536
|
await this.prepare({ write: true });
|
|
2383
2537
|
if (options.forceNewVersion) {
|
|
2384
2538
|
await this.makeNewVersion();
|
|
@@ -2387,11 +2541,12 @@ var FileDatabase = class {
|
|
|
2387
2541
|
this.makeNewFile();
|
|
2388
2542
|
}
|
|
2389
2543
|
let { dataToWrite, dataLeftOver, fileName } = this.figureOutDataAndFileToWrite(data);
|
|
2390
|
-
|
|
2544
|
+
const destPath = this.getDestinationPath(this.currentVersion || void 0);
|
|
2545
|
+
await this.safeWrite(import_path4.default.join(destPath, fileName), dataToWrite);
|
|
2391
2546
|
this.updateMetadata(dataToWrite, fileName);
|
|
2392
2547
|
while (dataLeftOver && dataLeftOver.length > 0) {
|
|
2393
2548
|
const writeContext = this.figureOutDataAndFileToWrite(dataLeftOver);
|
|
2394
|
-
await this.safeWrite(import_path4.default.join(
|
|
2549
|
+
await this.safeWrite(import_path4.default.join(destPath, writeContext.fileName), writeContext.dataToWrite);
|
|
2395
2550
|
this.updateMetadata(writeContext.dataToWrite, writeContext.fileName);
|
|
2396
2551
|
dataLeftOver = writeContext.dataLeftOver;
|
|
2397
2552
|
}
|
|
@@ -2409,7 +2564,7 @@ var FileDatabase = class {
|
|
|
2409
2564
|
const isNonPaginatedData = this.metadata.dataType === "text" || this.metadata.dataType === "xml" || this.metadata.dataType === "json-object";
|
|
2410
2565
|
if (isNonPaginatedData) {
|
|
2411
2566
|
const file = this.metadata.files[0];
|
|
2412
|
-
const filePath = import_path4.default.join(this.getDestinationPath(
|
|
2567
|
+
const filePath = import_path4.default.join(this.getDestinationPath(this.currentVersion || void 0), file.fileName);
|
|
2413
2568
|
try {
|
|
2414
2569
|
const rawData = await import_fs4.default.promises.readFile(filePath, "utf8");
|
|
2415
2570
|
return deserializeData(rawData, this.metadata.dataType);
|
|
@@ -2447,7 +2602,7 @@ var FileDatabase = class {
|
|
|
2447
2602
|
let cumulativeRecords = currentFileOffset;
|
|
2448
2603
|
for (let i = currentFileIndex; i < this.metadata.files.length && recordsRead < effectivePageSize; i++) {
|
|
2449
2604
|
const file = this.metadata.files[i];
|
|
2450
|
-
const filePath = import_path4.default.join(this.getDestinationPath(
|
|
2605
|
+
const filePath = import_path4.default.join(this.getDestinationPath(this.currentVersion || void 0), file.fileName);
|
|
2451
2606
|
try {
|
|
2452
2607
|
const rawData = await import_fs4.default.promises.readFile(filePath, "utf8");
|
|
2453
2608
|
const fileData = deserializeData(rawData, this.metadata.dataType);
|
|
@@ -2510,9 +2665,305 @@ var FileDatabase = class {
|
|
|
2510
2665
|
return { ...this.metadata };
|
|
2511
2666
|
}
|
|
2512
2667
|
};
|
|
2668
|
+
|
|
2669
|
+
// src/db/index.ts
|
|
2670
|
+
var import_knex = __toESM(require("knex"), 1);
|
|
2671
|
+
var Db = class {
|
|
2672
|
+
knexInstance = null;
|
|
2673
|
+
config;
|
|
2674
|
+
logger;
|
|
2675
|
+
queriesLog = [];
|
|
2676
|
+
isConnected = false;
|
|
2677
|
+
constructor(config2) {
|
|
2678
|
+
if (!config2.connectionString) {
|
|
2679
|
+
throw new ParamError("Db: connectionString is required");
|
|
2680
|
+
}
|
|
2681
|
+
this.config = {
|
|
2682
|
+
testConnection: true,
|
|
2683
|
+
profile: false,
|
|
2684
|
+
pool: { min: 2, max: 10 },
|
|
2685
|
+
acquireConnectionTimeout: 1e4,
|
|
2686
|
+
ssl: { rejectUnauthorized: false },
|
|
2687
|
+
logger: console,
|
|
2688
|
+
name: "default",
|
|
2689
|
+
...config2
|
|
2690
|
+
};
|
|
2691
|
+
this.logger = this.config.logger;
|
|
2692
|
+
const instance2 = this;
|
|
2693
|
+
const callableWrapper = function(...args) {
|
|
2694
|
+
throw new Error("This should never be called directly");
|
|
2695
|
+
};
|
|
2696
|
+
callableWrapper._instance = instance2;
|
|
2697
|
+
return new Proxy(callableWrapper, {
|
|
2698
|
+
// Intercept function calls: db('table')
|
|
2699
|
+
apply: (target, thisArg, argumentsList) => {
|
|
2700
|
+
const inst = target._instance;
|
|
2701
|
+
if (!inst.knexInstance) {
|
|
2702
|
+
throw new Error("Db: Not connected. Call connect() first.");
|
|
2703
|
+
}
|
|
2704
|
+
return inst.knexInstance(...argumentsList);
|
|
2705
|
+
},
|
|
2706
|
+
// Intercept property access: db.schema, db.raw, etc.
|
|
2707
|
+
get: (target, prop) => {
|
|
2708
|
+
if (prop === "_instance") {
|
|
2709
|
+
return target._instance;
|
|
2710
|
+
}
|
|
2711
|
+
const instance3 = target._instance;
|
|
2712
|
+
const ownMethods = [
|
|
2713
|
+
"connect",
|
|
2714
|
+
"disconnect",
|
|
2715
|
+
"testConnection",
|
|
2716
|
+
"tableExists",
|
|
2717
|
+
"getQueryLog",
|
|
2718
|
+
"getKnex",
|
|
2719
|
+
"isConnectedToDb",
|
|
2720
|
+
"getErrorMessage",
|
|
2721
|
+
"detectClient",
|
|
2722
|
+
"attachProfiler"
|
|
2723
|
+
];
|
|
2724
|
+
if (prop in instance3) {
|
|
2725
|
+
const value = instance3[prop];
|
|
2726
|
+
if (typeof value === "function" && ownMethods.includes(prop)) {
|
|
2727
|
+
return value.bind(instance3);
|
|
2728
|
+
}
|
|
2729
|
+
if (typeof value !== "function") {
|
|
2730
|
+
return value;
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
if (instance3.knexInstance) {
|
|
2734
|
+
const knexProp = instance3.knexInstance[prop];
|
|
2735
|
+
if (typeof knexProp === "function") {
|
|
2736
|
+
return knexProp.bind(instance3.knexInstance);
|
|
2737
|
+
}
|
|
2738
|
+
return knexProp;
|
|
2739
|
+
}
|
|
2740
|
+
if (prop in instance3) {
|
|
2741
|
+
const method = instance3[prop];
|
|
2742
|
+
if (typeof method === "function") {
|
|
2743
|
+
return method.bind(instance3);
|
|
2744
|
+
}
|
|
2745
|
+
return method;
|
|
2746
|
+
}
|
|
2747
|
+
return void 0;
|
|
2748
|
+
}
|
|
2749
|
+
});
|
|
2750
|
+
}
|
|
2751
|
+
/**
|
|
2752
|
+
* Detect database client type from connection string
|
|
2753
|
+
*/
|
|
2754
|
+
detectClient(connectionString) {
|
|
2755
|
+
if (connectionString.match(/^postgresql/)) {
|
|
2756
|
+
return "pg";
|
|
2757
|
+
}
|
|
2758
|
+
if (connectionString.match(/^mysql/)) {
|
|
2759
|
+
return "mysql2";
|
|
2760
|
+
}
|
|
2761
|
+
return null;
|
|
2762
|
+
}
|
|
2763
|
+
/**
|
|
2764
|
+
* Connect to the database
|
|
2765
|
+
*/
|
|
2766
|
+
async connect() {
|
|
2767
|
+
if (this.isConnected && this.knexInstance) {
|
|
2768
|
+
this.logger.warn?.("[Db] Already connected");
|
|
2769
|
+
return;
|
|
2770
|
+
}
|
|
2771
|
+
const client = this.detectClient(this.config.connectionString);
|
|
2772
|
+
if (!client) {
|
|
2773
|
+
throw new ParamError(
|
|
2774
|
+
`Db: Cannot determine client type from connection string. Expected postgresql:// or mysql://`
|
|
2775
|
+
);
|
|
2776
|
+
}
|
|
2777
|
+
try {
|
|
2778
|
+
const connectionConfig = {
|
|
2779
|
+
connectionString: this.config.connectionString,
|
|
2780
|
+
family: 4
|
|
2781
|
+
// Force IPv4 only (disable IPv6)
|
|
2782
|
+
};
|
|
2783
|
+
this.knexInstance = (0, import_knex.default)({
|
|
2784
|
+
client,
|
|
2785
|
+
connection: connectionConfig,
|
|
2786
|
+
pool: this.config.pool,
|
|
2787
|
+
acquireConnectionTimeout: this.config.acquireConnectionTimeout,
|
|
2788
|
+
...this.config.ssl && { ssl: this.config.ssl }
|
|
2789
|
+
});
|
|
2790
|
+
if (this.config.profile) {
|
|
2791
|
+
this.attachProfiler();
|
|
2792
|
+
}
|
|
2793
|
+
if (this.config.testConnection) {
|
|
2794
|
+
await this.testConnection();
|
|
2795
|
+
}
|
|
2796
|
+
this.isConnected = true;
|
|
2797
|
+
this.logger.debug?.(`[Db] Connected to database "${this.config.name || this.config.connectionString}"`);
|
|
2798
|
+
} catch (error) {
|
|
2799
|
+
if (error instanceof ParamError) {
|
|
2800
|
+
throw error;
|
|
2801
|
+
}
|
|
2802
|
+
const errorMsg = this.getErrorMessage(error);
|
|
2803
|
+
throw new ParamError(`Db: Connection failed - ${errorMsg}`);
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
/**
|
|
2807
|
+
* Disconnect from the database
|
|
2808
|
+
*/
|
|
2809
|
+
async disconnect() {
|
|
2810
|
+
if (!this.knexInstance) {
|
|
2811
|
+
return;
|
|
2812
|
+
}
|
|
2813
|
+
try {
|
|
2814
|
+
await this.knexInstance.destroy();
|
|
2815
|
+
this.knexInstance = null;
|
|
2816
|
+
this.isConnected = false;
|
|
2817
|
+
this.queriesLog = [];
|
|
2818
|
+
this.logger.debug?.(`[Db] Disconnected from database "${this.config.name || this.config.connectionString}"`);
|
|
2819
|
+
} catch (error) {
|
|
2820
|
+
const errorMsg = this.getErrorMessage(error);
|
|
2821
|
+
this.logger.error?.(`[Db] Error disconnecting: ${errorMsg}`);
|
|
2822
|
+
throw error;
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
/**
|
|
2826
|
+
* Extract error message from various error types
|
|
2827
|
+
*/
|
|
2828
|
+
getErrorMessage(error) {
|
|
2829
|
+
if (error instanceof AggregateError) {
|
|
2830
|
+
const errors = error.errors || [];
|
|
2831
|
+
if (errors.length > 0) {
|
|
2832
|
+
const firstError = errors[0];
|
|
2833
|
+
const firstErrorMsg = firstError instanceof Error ? firstError.message : String(firstError);
|
|
2834
|
+
const allSimilar = errors.every((e) => {
|
|
2835
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2836
|
+
const codeMatch = msg.match(/^(\w+)\s/);
|
|
2837
|
+
const firstCodeMatch = firstErrorMsg.match(/^(\w+)\s/);
|
|
2838
|
+
return codeMatch && firstCodeMatch && codeMatch[1] === firstCodeMatch[1];
|
|
2839
|
+
});
|
|
2840
|
+
if (allSimilar && errors.length > 1) {
|
|
2841
|
+
const addresses = errors.map((e) => {
|
|
2842
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2843
|
+
const addrMatch = msg.match(/([:\d.]+:\d+)/);
|
|
2844
|
+
return addrMatch ? addrMatch[1] : null;
|
|
2845
|
+
}).filter(Boolean);
|
|
2846
|
+
if (addresses.length > 0) {
|
|
2847
|
+
const codeMatch = firstErrorMsg.match(/^(\w+)\s/);
|
|
2848
|
+
const code = codeMatch ? codeMatch[1] : "Connection error";
|
|
2849
|
+
return `${code} (tried: ${addresses.join(", ")})`;
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
const uniqueMessages = [...new Set(errors.map((e) => {
|
|
2853
|
+
return e instanceof Error ? e.message : String(e);
|
|
2854
|
+
}))];
|
|
2855
|
+
if (uniqueMessages.length === 1) {
|
|
2856
|
+
return uniqueMessages[0];
|
|
2857
|
+
}
|
|
2858
|
+
return uniqueMessages.join("; ");
|
|
2859
|
+
}
|
|
2860
|
+
return error.message || "Multiple errors occurred";
|
|
2861
|
+
}
|
|
2862
|
+
if (error instanceof Error) {
|
|
2863
|
+
const errorWithCode = error;
|
|
2864
|
+
if (errorWithCode.code) {
|
|
2865
|
+
return `${errorWithCode.code}: ${error.message || String(error)}`;
|
|
2866
|
+
}
|
|
2867
|
+
return error.message || String(error);
|
|
2868
|
+
}
|
|
2869
|
+
if (typeof error === "string") {
|
|
2870
|
+
return error;
|
|
2871
|
+
}
|
|
2872
|
+
if (error?.message) {
|
|
2873
|
+
const msg = String(error.message);
|
|
2874
|
+
const errorWithCode = error;
|
|
2875
|
+
if (errorWithCode.code) {
|
|
2876
|
+
return `${errorWithCode.code}: ${msg}`;
|
|
2877
|
+
}
|
|
2878
|
+
return msg;
|
|
2879
|
+
}
|
|
2880
|
+
return String(error) || "Unknown error";
|
|
2881
|
+
}
|
|
2882
|
+
/**
|
|
2883
|
+
* Test database connection
|
|
2884
|
+
*/
|
|
2885
|
+
async testConnection() {
|
|
2886
|
+
if (!this.knexInstance) {
|
|
2887
|
+
throw new Error("Db: Not connected. Call connect() first.");
|
|
2888
|
+
}
|
|
2889
|
+
try {
|
|
2890
|
+
const result = await this.knexInstance.raw("SELECT 2+3 AS result");
|
|
2891
|
+
const isOk = result.rows?.[0]?.result === 5 || result[0]?.[0]?.result === 5;
|
|
2892
|
+
this.logger.debug?.(`[Db] Connection test: ${isOk ? "OK" : "FAILED"}`);
|
|
2893
|
+
return isOk;
|
|
2894
|
+
} catch (error) {
|
|
2895
|
+
const errorMsg = this.getErrorMessage(error);
|
|
2896
|
+
this.logger.error?.(`[Db] Connection test failed: ${errorMsg}`);
|
|
2897
|
+
throw new ParamError(`Db: Connection test failed - ${errorMsg}`);
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
/**
|
|
2901
|
+
* Attach query profiler to log all queries
|
|
2902
|
+
*/
|
|
2903
|
+
attachProfiler() {
|
|
2904
|
+
if (!this.knexInstance) {
|
|
2905
|
+
return;
|
|
2906
|
+
}
|
|
2907
|
+
this.queriesLog = [];
|
|
2908
|
+
this.knexInstance.queriesLog = this.queriesLog;
|
|
2909
|
+
this.knexInstance.on("query", (query) => {
|
|
2910
|
+
query.__startTime = process.hrtime();
|
|
2911
|
+
});
|
|
2912
|
+
this.knexInstance.on("query-response", (response, query) => {
|
|
2913
|
+
const [seconds, nanoseconds] = process.hrtime(query.__startTime);
|
|
2914
|
+
const executionTimeMs = (seconds * 1e3 + nanoseconds / 1e6).toFixed(2);
|
|
2915
|
+
const logEntry = {
|
|
2916
|
+
sql: query.sql,
|
|
2917
|
+
bindings: query.bindings || [],
|
|
2918
|
+
executionTimeMs
|
|
2919
|
+
};
|
|
2920
|
+
this.queriesLog.push(logEntry);
|
|
2921
|
+
this.logger.debug?.(`[Db] Query: ${query.sql} | Duration: ${executionTimeMs}ms`);
|
|
2922
|
+
});
|
|
2923
|
+
this.knexInstance.on("query-error", (error, query) => {
|
|
2924
|
+
this.logger.error?.(`[Db] Query failed: ${query.sql}`, error);
|
|
2925
|
+
});
|
|
2926
|
+
}
|
|
2927
|
+
/**
|
|
2928
|
+
* Get query log (only available if profiling is enabled)
|
|
2929
|
+
*/
|
|
2930
|
+
getQueryLog() {
|
|
2931
|
+
return [...this.queriesLog];
|
|
2932
|
+
}
|
|
2933
|
+
/**
|
|
2934
|
+
* Check if a table exists
|
|
2935
|
+
*/
|
|
2936
|
+
async tableExists(tableName) {
|
|
2937
|
+
if (!this.knexInstance) {
|
|
2938
|
+
throw new Error("Db: Not connected. Call connect() first.");
|
|
2939
|
+
}
|
|
2940
|
+
try {
|
|
2941
|
+
return await this.knexInstance.schema.hasTable(tableName);
|
|
2942
|
+
} catch (error) {
|
|
2943
|
+
this.logger.error?.(`[Db] Error checking table existence: ${error.message}`);
|
|
2944
|
+
throw error;
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
/**
|
|
2948
|
+
* Get the underlying Knex instance (for advanced usage)
|
|
2949
|
+
*/
|
|
2950
|
+
getKnex() {
|
|
2951
|
+
if (!this.knexInstance) {
|
|
2952
|
+
throw new Error("Db: Not connected. Call connect() first.");
|
|
2953
|
+
}
|
|
2954
|
+
return this.knexInstance;
|
|
2955
|
+
}
|
|
2956
|
+
/**
|
|
2957
|
+
* Get connection status
|
|
2958
|
+
*/
|
|
2959
|
+
isConnectedToDb() {
|
|
2960
|
+
return this.isConnected && this.knexInstance !== null;
|
|
2961
|
+
}
|
|
2962
|
+
};
|
|
2513
2963
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2514
2964
|
0 && (module.exports = {
|
|
2515
2965
|
Args,
|
|
2966
|
+
Db,
|
|
2516
2967
|
Divider,
|
|
2517
2968
|
FileDatabase,
|
|
2518
2969
|
FileDatabaseError,
|