@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/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/filestore/index.ts
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/filestore/serializers.ts
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/filestore/synopsis-functions.ts
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/filestore/index.ts
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
- return import_path4.default.resolve(this.basePath, this.namespace, this.tableName);
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 || !this.currentVersion) {
2317
+ if (!this.useMetadata) {
2219
2318
  return;
2220
2319
  }
2221
2320
  const metadataToSave = metadata || this.metadata;
2222
- const metadataFile = import_path4.default.join(this.getDestinationPath(), this.currentVersion, "metadata.json");
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.currentVersion === null) {
2351
- await this.makeNewVersion();
2352
- this.metadata = this.getDefaultMetadata();
2353
- this.metadata.version = this.currentVersion;
2354
- this.makeNewFile();
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
- if (!this.metadata.files.length) {
2357
- this.metadata = await this.figureMetadata(this.currentVersion);
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
- const versions = await this.getVersions();
2362
- if (versions.length === 0) {
2363
- throw new FileDatabaseError("[FileDatabase] No versions found, cannot read");
2364
- }
2365
- if (version) {
2366
- if (!versions.includes(version)) {
2367
- throw new FileDatabaseError(`[FileDatabase] Version "${version}" not found`);
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
- await this.setCurrentVersion(versions[versions.length - 1]);
2372
- }
2373
- if (!this.metadata.files.length) {
2374
- this.metadata = await this.figureMetadata(this.currentVersion);
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
- await this.safeWrite(import_path4.default.join(this.currentVersionFolder, fileName), dataToWrite);
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(this.currentVersionFolder, writeContext.fileName), writeContext.dataToWrite);
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(), this.currentVersion, file.fileName);
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(), this.currentVersion, file.fileName);
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,