@nmakarov/cli-toolkit 0.11.3 → 0.14.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
@@ -1110,6 +1110,9 @@ __export(src_exports, {
1110
1110
  buildBreadcrumb: () => buildBreadcrumb,
1111
1111
  buildDetailBreadcrumb: () => buildDetailBreadcrumb,
1112
1112
  buildFooter: () => buildFooter,
1113
+ dbConnect: () => dbConnect,
1114
+ dbFindAndConnect: () => dbFindAndConnect,
1115
+ dbInit: () => dbInit,
1113
1116
  defaultFileSynopsisFunction: () => defaultFileSynopsisFunction,
1114
1117
  defaultVersionSynopsisFunction: () => defaultVersionSynopsisFunction,
1115
1118
  fileDatabaseInit: () => fileDatabaseInit,
@@ -1117,6 +1120,8 @@ __export(src_exports, {
1117
1120
  h: () => import_react5.createElement,
1118
1121
  joiEdateType: () => joiEdateType,
1119
1122
  joiStringArrayType: () => joiStringArrayType,
1123
+ listSources: () => listSources,
1124
+ listTables: () => listTables,
1120
1125
  load: () => load,
1121
1126
  organizeFooterMessages: () => organizeFooterMessages,
1122
1127
  setupContext: () => setupContext,
@@ -2584,20 +2589,48 @@ var FileDatabase = class {
2584
2589
  }
2585
2590
  /**
2586
2591
  * Figure out what data to write and which file to use (for pagination)
2592
+ * @param data - Data to write
2593
+ * @param targetFileIndex - Optional index of existing file to overwrite (when customMetadata matches)
2594
+ * @param forceNewFile - If true, always create a new file (when customMetadata provided but no match)
2587
2595
  */
2588
- figureOutDataAndFileToWrite(data) {
2596
+ figureOutDataAndFileToWrite(data, targetFileIndex = null, forceNewFile = false) {
2589
2597
  let dataToWrite;
2590
2598
  let dataLeftOver;
2591
2599
  const incomingDataType = detectDataType(data);
2592
2600
  if (this.metadata.dataType !== incomingDataType) {
2593
2601
  this.metadata.dataType = incomingDataType;
2594
2602
  }
2595
- if (this.metadata.files.length === 0) {
2603
+ if (targetFileIndex !== null && targetFileIndex < this.metadata.files.length) {
2604
+ const targetFile = this.metadata.files[targetFileIndex];
2605
+ if (!Array.isArray(data)) {
2606
+ dataToWrite = data;
2607
+ dataLeftOver = null;
2608
+ return { dataToWrite, dataLeftOver, fileName: targetFile.fileName };
2609
+ } else {
2610
+ dataToWrite = data.slice(0, this.pageSize);
2611
+ dataLeftOver = data.slice(this.pageSize);
2612
+ this.lastFileData = dataToWrite;
2613
+ return { dataToWrite, dataLeftOver, fileName: targetFile.fileName };
2614
+ }
2615
+ }
2616
+ let newlyCreatedFileIndex = null;
2617
+ if (forceNewFile) {
2618
+ const filesBeforeCreate = this.metadata.files.length;
2619
+ this.makeNewFile();
2620
+ newlyCreatedFileIndex = filesBeforeCreate;
2621
+ this.logger.debug?.(`[FileDatabase] Creating new file for unique custom metadata combination, fileNumber: ${this.currentFileNumber}`);
2622
+ } else if (this.metadata.files.length === 0) {
2596
2623
  this.makeNewFile();
2597
2624
  }
2598
2625
  const lastFile = this.metadata.files[this.metadata.files.length - 1];
2599
2626
  const lastFileRecordsCount = lastFile.recordsCount;
2600
- if (!Array.isArray(data)) {
2627
+ if (forceNewFile && newlyCreatedFileIndex !== null) {
2628
+ const newlyCreatedFile = this.metadata.files[newlyCreatedFileIndex];
2629
+ if (newlyCreatedFile && newlyCreatedFile.fileName !== lastFile.fileName) {
2630
+ this.logger.warn?.(`[FileDatabase] Warning: Newly created file ${newlyCreatedFile.fileName} doesn't match last file ${lastFile.fileName}`);
2631
+ }
2632
+ }
2633
+ if (!Array.isArray(data) && !forceNewFile) {
2601
2634
  const lastFileExtension = import_path4.default.extname(lastFile.fileName);
2602
2635
  const expectedExtension = `.${getFileExtension(incomingDataType)}`;
2603
2636
  if (lastFileExtension !== expectedExtension) {
@@ -2607,24 +2640,35 @@ var FileDatabase = class {
2607
2640
  lastFile.fileName = `${this.currentFileNumber.toString().padStart(6, "0")}.${getFileExtension(incomingDataType)}`;
2608
2641
  }
2609
2642
  }
2643
+ } else if (!Array.isArray(data) && forceNewFile) {
2644
+ const lastFileExtension = import_path4.default.extname(lastFile.fileName);
2645
+ const expectedExtension = `.${getFileExtension(incomingDataType)}`;
2646
+ if (lastFileExtension !== expectedExtension) {
2647
+ lastFile.fileName = `${this.currentFileNumber.toString().padStart(6, "0")}.${getFileExtension(incomingDataType)}`;
2648
+ }
2610
2649
  }
2611
2650
  if (Array.isArray(data)) {
2612
- if (lastFileRecordsCount < this.pageSize) {
2651
+ if (forceNewFile) {
2652
+ dataToWrite = data.slice(0, this.pageSize);
2653
+ dataLeftOver = data.slice(this.pageSize);
2654
+ this.lastFileData = dataToWrite;
2655
+ } else if (lastFileRecordsCount < this.pageSize) {
2613
2656
  dataToWrite = [...this.lastFileData || [], ...data.slice(0, this.pageSize - lastFileRecordsCount)];
2614
2657
  dataLeftOver = data.slice(this.pageSize - lastFileRecordsCount);
2658
+ this.lastFileData = dataToWrite;
2615
2659
  } else {
2616
2660
  this.makeNewFile();
2617
2661
  dataToWrite = data.slice(0, this.pageSize);
2618
2662
  dataLeftOver = data.slice(this.pageSize);
2663
+ this.lastFileData = dataToWrite;
2619
2664
  }
2620
- this.lastFileData = dataToWrite;
2621
2665
  } else {
2622
2666
  dataToWrite = data;
2623
2667
  dataLeftOver = null;
2624
2668
  }
2625
2669
  const fileName = this.metadata.files[this.metadata.files.length - 1].fileName;
2626
2670
  this.logger.debug?.(
2627
- `[FileDatabase] figureOutDataAndFileToWrite: filename=${fileName}, dataToWrite.length=${Array.isArray(dataToWrite) ? dataToWrite.length : "N/A"}, lastFileRecordsCount=${lastFileRecordsCount}`
2671
+ `[FileDatabase] figureOutDataAndFileToWrite: filename=${fileName}, forceNewFile=${forceNewFile}, targetFileIndex=${targetFileIndex}, dataToWrite.length=${Array.isArray(dataToWrite) ? dataToWrite.length : "N/A"}, lastFileRecordsCount=${lastFileRecordsCount}`
2628
2672
  );
2629
2673
  return { dataToWrite, dataLeftOver, fileName };
2630
2674
  }
@@ -2652,7 +2696,7 @@ var FileDatabase = class {
2652
2696
  /**
2653
2697
  * Update metadata after writing data
2654
2698
  */
2655
- updateMetadata(dataToWrite, fileName) {
2699
+ updateMetadata(dataToWrite, fileName, customMetadata) {
2656
2700
  let currentFile;
2657
2701
  if (fileName) {
2658
2702
  const foundFile = this.metadata.files.find((file) => file.fileName === fileName);
@@ -2667,6 +2711,9 @@ var FileDatabase = class {
2667
2711
  }
2668
2712
  const recordsCount = Array.isArray(dataToWrite) ? dataToWrite.length : 1;
2669
2713
  currentFile.recordsCount = recordsCount;
2714
+ if (customMetadata) {
2715
+ Object.assign(currentFile, customMetadata);
2716
+ }
2670
2717
  const fileIndex = this.metadata.files.indexOf(currentFile);
2671
2718
  if (fileIndex !== -1) {
2672
2719
  this.calculateFileSynopsis(dataToWrite, fileIndex);
@@ -2719,6 +2766,11 @@ var FileDatabase = class {
2719
2766
  } else {
2720
2767
  if (!this.metadata.files.length) {
2721
2768
  this.metadata = await this.figureMetadata(this.currentVersion);
2769
+ if (this.metadata.files && this.metadata.files.length > 0) {
2770
+ this.currentFileNumber = Math.max(...this.metadata.files.map((f) => f.number || 0));
2771
+ } else {
2772
+ this.currentFileNumber = 0;
2773
+ }
2722
2774
  }
2723
2775
  }
2724
2776
  } else {
@@ -2729,16 +2781,22 @@ var FileDatabase = class {
2729
2781
  try {
2730
2782
  const rawData = await import_fs4.default.promises.readFile(metadataPath, "utf8");
2731
2783
  this.metadata = JSON.parse(rawData);
2784
+ if (this.metadata.files && this.metadata.files.length > 0) {
2785
+ this.currentFileNumber = Math.max(...this.metadata.files.map((f) => f.number || 0));
2786
+ } else {
2787
+ this.currentFileNumber = 0;
2788
+ }
2732
2789
  } catch (e) {
2733
2790
  this.metadata = this.getDefaultMetadata();
2791
+ this.currentFileNumber = 0;
2734
2792
  }
2735
2793
  } else {
2736
2794
  this.metadata = this.getDefaultMetadata();
2737
- this.makeNewFile();
2795
+ this.currentFileNumber = 0;
2738
2796
  }
2739
2797
  } else {
2740
2798
  this.metadata = this.getDefaultMetadata();
2741
- this.makeNewFile();
2799
+ this.currentFileNumber = 0;
2742
2800
  }
2743
2801
  }
2744
2802
  } else if (read) {
@@ -2757,6 +2815,11 @@ var FileDatabase = class {
2757
2815
  }
2758
2816
  if (!this.metadata.files.length) {
2759
2817
  this.metadata = await this.figureMetadata(this.currentVersion);
2818
+ if (this.metadata.files && this.metadata.files.length > 0) {
2819
+ this.currentFileNumber = Math.max(...this.metadata.files.map((f) => f.number || 0));
2820
+ } else {
2821
+ this.currentFileNumber = 0;
2822
+ }
2760
2823
  }
2761
2824
  } else {
2762
2825
  this.currentVersion = null;
@@ -2770,6 +2833,11 @@ var FileDatabase = class {
2770
2833
  try {
2771
2834
  const rawData = await import_fs4.default.promises.readFile(metadataPath, "utf8");
2772
2835
  this.metadata = JSON.parse(rawData);
2836
+ if (this.metadata.files && this.metadata.files.length > 0) {
2837
+ this.currentFileNumber = Math.max(...this.metadata.files.map((f) => f.number || 0));
2838
+ } else {
2839
+ this.currentFileNumber = 0;
2840
+ }
2773
2841
  } catch (e) {
2774
2842
  throw new FileDatabaseError(`Failed to read metadata: ${e.message}`);
2775
2843
  }
@@ -2778,6 +2846,11 @@ var FileDatabase = class {
2778
2846
  }
2779
2847
  } else {
2780
2848
  this.metadata = await this.figureMetadataFromVersionFiles("");
2849
+ if (this.metadata.files && this.metadata.files.length > 0) {
2850
+ this.currentFileNumber = Math.max(...this.metadata.files.map((f) => f.number || 0));
2851
+ } else {
2852
+ this.currentFileNumber = 0;
2853
+ }
2781
2854
  }
2782
2855
  }
2783
2856
  }
@@ -2799,14 +2872,44 @@ var FileDatabase = class {
2799
2872
  this.metadata.dataType = incomingDataType;
2800
2873
  this.makeNewFile();
2801
2874
  }
2802
- let { dataToWrite, dataLeftOver, fileName } = this.figureOutDataAndFileToWrite(data);
2875
+ let targetFileIndex = null;
2876
+ const hasCustomMetadata = options.customMetadata && Object.keys(options.customMetadata).length > 0;
2877
+ if (hasCustomMetadata) {
2878
+ for (let i = 0; i < this.metadata.files.length; i++) {
2879
+ const fileEntry = this.metadata.files[i];
2880
+ const matches = Object.keys(options.customMetadata).every((key) => {
2881
+ return key in fileEntry && fileEntry[key] === options.customMetadata[key];
2882
+ });
2883
+ if (matches) {
2884
+ targetFileIndex = i;
2885
+ this.logger.debug?.(`[FileDatabase] Found existing file with matching custom metadata: ${fileEntry.fileName}, metadata: ${JSON.stringify(options.customMetadata)}`);
2886
+ break;
2887
+ } else {
2888
+ this.logger.debug?.(`[FileDatabase] File ${fileEntry.fileName} does not match custom metadata: ${JSON.stringify(options.customMetadata)}`);
2889
+ }
2890
+ }
2891
+ if (targetFileIndex === null) {
2892
+ this.logger.debug?.(`[FileDatabase] No existing file found with custom metadata: ${JSON.stringify(options.customMetadata)}, will create new file`);
2893
+ }
2894
+ } else {
2895
+ this.logger.debug?.(`[FileDatabase] No custom metadata provided, will create new file`);
2896
+ }
2897
+ if (targetFileIndex !== null) {
2898
+ const targetFile = this.metadata.files[targetFileIndex];
2899
+ this.currentFileNumber = targetFile.number;
2900
+ this.lastFileData = null;
2901
+ this.currentRecord = 0;
2902
+ this.hasReadFirstPage = false;
2903
+ }
2904
+ const forceNewFile = hasCustomMetadata && targetFileIndex === null;
2905
+ let { dataToWrite, dataLeftOver, fileName } = this.figureOutDataAndFileToWrite(data, targetFileIndex, forceNewFile);
2803
2906
  const destPath = this.getDestinationPath(this.currentVersion || void 0);
2804
2907
  await this.safeWrite(import_path4.default.join(destPath, fileName), dataToWrite);
2805
- this.updateMetadata(dataToWrite, fileName);
2806
- while (dataLeftOver && dataLeftOver.length > 0) {
2908
+ this.updateMetadata(dataToWrite, fileName, options.customMetadata);
2909
+ while (dataLeftOver && dataLeftOver.length > 0 && targetFileIndex === null) {
2807
2910
  const writeContext = this.figureOutDataAndFileToWrite(dataLeftOver);
2808
2911
  await this.safeWrite(import_path4.default.join(destPath, writeContext.fileName), writeContext.dataToWrite);
2809
- this.updateMetadata(writeContext.dataToWrite, writeContext.fileName);
2912
+ this.updateMetadata(writeContext.dataToWrite, writeContext.fileName, options.customMetadata);
2810
2913
  dataLeftOver = writeContext.dataLeftOver;
2811
2914
  }
2812
2915
  this.calculateVersionSynopsis();
@@ -2923,7 +3026,85 @@ var FileDatabase = class {
2923
3026
  getMetadata() {
2924
3027
  return { ...this.metadata };
2925
3028
  }
3029
+ /**
3030
+ * Find data by custom metadata fields
3031
+ * Searches through all versions and files to find entries matching the search criteria
3032
+ *
3033
+ * @param searchCriteria - Object with field names and values to search for (e.g., { ListingKey: "123", id: "456" })
3034
+ * @returns Array of found entries with their file paths and metadata
3035
+ */
3036
+ async findData(searchCriteria) {
3037
+ const results = [];
3038
+ if (!this.versioned) {
3039
+ await this.prepare({ read: true });
3040
+ const metadata = this.getMetadata();
3041
+ for (const fileEntry of metadata.files) {
3042
+ const matches = Object.keys(searchCriteria).every((key) => {
3043
+ return fileEntry[key] === searchCriteria[key];
3044
+ });
3045
+ if (matches) {
3046
+ const destPath = this.getDestinationPath();
3047
+ const filePath = import_path4.default.join(destPath, fileEntry.fileName);
3048
+ const fileData = await import_fs4.default.promises.readFile(filePath, "utf8");
3049
+ const data = deserializeData(fileData, metadata.dataType || "json-object");
3050
+ results.push({
3051
+ filePath,
3052
+ fileName: fileEntry.fileName,
3053
+ version: null,
3054
+ metadata: fileEntry,
3055
+ data
3056
+ });
3057
+ }
3058
+ }
3059
+ } else {
3060
+ const versions = await this.getVersions();
3061
+ for (const version of versions) {
3062
+ await this.prepare({ read: true, version });
3063
+ const metadata = this.getMetadata();
3064
+ for (const fileEntry of metadata.files) {
3065
+ const matches = Object.keys(searchCriteria).every((key) => {
3066
+ return fileEntry[key] === searchCriteria[key];
3067
+ });
3068
+ if (matches) {
3069
+ const destPath = this.getDestinationPath(version);
3070
+ const filePath = import_path4.default.join(destPath, fileEntry.fileName);
3071
+ const fileData = await import_fs4.default.promises.readFile(filePath, "utf8");
3072
+ const data = deserializeData(fileData, metadata.dataType || "json-object");
3073
+ results.push({
3074
+ filePath,
3075
+ fileName: fileEntry.fileName,
3076
+ version,
3077
+ metadata: fileEntry,
3078
+ data
3079
+ });
3080
+ }
3081
+ }
3082
+ }
3083
+ }
3084
+ return results;
3085
+ }
2926
3086
  };
3087
+ function listTables(basePath, namespace) {
3088
+ const namespacePath = import_path4.default.join(basePath, namespace);
3089
+ if (!import_fs4.default.existsSync(namespacePath)) {
3090
+ return [];
3091
+ }
3092
+ try {
3093
+ return import_fs4.default.readdirSync(namespacePath, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name);
3094
+ } catch (error) {
3095
+ return [];
3096
+ }
3097
+ }
3098
+ function listSources(basePath) {
3099
+ if (!import_fs4.default.existsSync(basePath)) {
3100
+ return [];
3101
+ }
3102
+ try {
3103
+ return import_fs4.default.readdirSync(basePath, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name);
3104
+ } catch (error) {
3105
+ return [];
3106
+ }
3107
+ }
2927
3108
  function fileDatabaseInit(context, options = {}) {
2928
3109
  return new FileDatabase(context, options);
2929
3110
  }
@@ -2936,6 +3117,10 @@ var Db = class {
2936
3117
  logger;
2937
3118
  queriesLog = [];
2938
3119
  isConnected = false;
3120
+ /**
3121
+ * Constructor - accepts config object
3122
+ * Use dbInit() function to initialize with Context
3123
+ */
2939
3124
  constructor(config2) {
2940
3125
  if (!config2.connectionString) {
2941
3126
  throw new ParamError("Db: connectionString is required");
@@ -3222,6 +3407,91 @@ var Db = class {
3222
3407
  return this.isConnected && this.knexInstance !== null;
3223
3408
  }
3224
3409
  };
3410
+ function capitalizeFirstLetter(str) {
3411
+ return str.charAt(0).toUpperCase() + str.slice(1);
3412
+ }
3413
+ async function dbConnect(context, connectionString, name, dbProfile) {
3414
+ const defs = {
3415
+ testDbConnection: "boolean default true",
3416
+ name: "string",
3417
+ poolMin: "number default 2",
3418
+ poolMax: "number default 10",
3419
+ acquireConnectionTimeout: "number default 10000",
3420
+ sslRejectUnauthorized: "boolean default false"
3421
+ };
3422
+ const paramsConfig = context.params.getAll(defs);
3423
+ const config2 = {
3424
+ connectionString,
3425
+ name: paramsConfig.name || name || "default",
3426
+ testConnection: paramsConfig.testDbConnection,
3427
+ profile: dbProfile ?? false,
3428
+ pool: {
3429
+ min: paramsConfig.poolMin,
3430
+ max: paramsConfig.poolMax
3431
+ },
3432
+ acquireConnectionTimeout: paramsConfig.acquireConnectionTimeout,
3433
+ ssl: {
3434
+ rejectUnauthorized: paramsConfig.sslRejectUnauthorized
3435
+ },
3436
+ logger: context.logger
3437
+ };
3438
+ try {
3439
+ const db = new Db(config2);
3440
+ context.registerCleanup(async () => {
3441
+ await db.disconnect();
3442
+ context.logger.debug(`[Db] instance "${name || connectionString}" destroyed`);
3443
+ });
3444
+ await db.connect();
3445
+ context.logger.debug(`[Db] instance "${name || connectionString}" initialized`);
3446
+ return db;
3447
+ } catch (error) {
3448
+ if (error instanceof ParamError) {
3449
+ throw error;
3450
+ }
3451
+ const errorMsg = error instanceof Error ? error.message : String(error);
3452
+ throw new ParamError(`[Db] connect error: ${errorMsg}`);
3453
+ }
3454
+ }
3455
+ async function dbFindAndConnect(context, dbNameOrConnectionString) {
3456
+ let dbName;
3457
+ let dbConnectionString;
3458
+ let dbProfile;
3459
+ if (dbNameOrConnectionString) {
3460
+ if (dbNameOrConnectionString.match(/^(postgresql|mysql):\/\/[^\s]+:[^\s]+@[^\s]+:\d+\/[^\s]+$/)) {
3461
+ dbName = void 0;
3462
+ dbConnectionString = dbNameOrConnectionString;
3463
+ } else {
3464
+ dbName = dbNameOrConnectionString;
3465
+ }
3466
+ } else {
3467
+ const defs = {
3468
+ dbName: "string",
3469
+ dbConnectionString: "string",
3470
+ dbProfile: "boolean default false"
3471
+ };
3472
+ const paramsConfig = context.params.getAll(defs);
3473
+ dbName = paramsConfig.dbName;
3474
+ dbConnectionString = paramsConfig.dbConnectionString;
3475
+ dbProfile = paramsConfig.dbProfile;
3476
+ }
3477
+ if (!dbName && !dbConnectionString) {
3478
+ throw new ParamError("Db: either dbName or dbConnectionString must be specified");
3479
+ }
3480
+ if (dbName) {
3481
+ const paramName = `dbConnectionString${capitalizeFirstLetter(dbName)}`;
3482
+ dbConnectionString = await context.params.get(paramName, "string");
3483
+ if (!dbConnectionString) {
3484
+ throw new ParamError(
3485
+ `Db: cannot find dbConnectionString for dbName="${dbName}" (looked for param "${paramName}")`
3486
+ );
3487
+ }
3488
+ }
3489
+ const db = await dbConnect(context, dbConnectionString, dbName, dbProfile);
3490
+ return db;
3491
+ }
3492
+ async function dbInit(context, dbNameOrConnectionString) {
3493
+ return await dbFindAndConnect(context, dbNameOrConnectionString);
3494
+ }
3225
3495
 
3226
3496
  // src/logger/index.ts
3227
3497
  var import_chalk = __toESM(require("chalk"), 1);
@@ -3617,6 +3887,9 @@ function setupContext(opts = {}) {
3617
3887
  buildBreadcrumb,
3618
3888
  buildDetailBreadcrumb,
3619
3889
  buildFooter,
3890
+ dbConnect,
3891
+ dbFindAndConnect,
3892
+ dbInit,
3620
3893
  defaultFileSynopsisFunction,
3621
3894
  defaultVersionSynopsisFunction,
3622
3895
  fileDatabaseInit,
@@ -3624,6 +3897,8 @@ function setupContext(opts = {}) {
3624
3897
  h,
3625
3898
  joiEdateType,
3626
3899
  joiStringArrayType,
3900
+ listSources,
3901
+ listTables,
3627
3902
  load,
3628
3903
  organizeFooterMessages,
3629
3904
  setupContext,