@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.js CHANGED
@@ -2502,20 +2502,48 @@ var FileDatabase = class {
2502
2502
  }
2503
2503
  /**
2504
2504
  * Figure out what data to write and which file to use (for pagination)
2505
+ * @param data - Data to write
2506
+ * @param targetFileIndex - Optional index of existing file to overwrite (when customMetadata matches)
2507
+ * @param forceNewFile - If true, always create a new file (when customMetadata provided but no match)
2505
2508
  */
2506
- figureOutDataAndFileToWrite(data) {
2509
+ figureOutDataAndFileToWrite(data, targetFileIndex = null, forceNewFile = false) {
2507
2510
  let dataToWrite;
2508
2511
  let dataLeftOver;
2509
2512
  const incomingDataType = detectDataType(data);
2510
2513
  if (this.metadata.dataType !== incomingDataType) {
2511
2514
  this.metadata.dataType = incomingDataType;
2512
2515
  }
2513
- if (this.metadata.files.length === 0) {
2516
+ if (targetFileIndex !== null && targetFileIndex < this.metadata.files.length) {
2517
+ const targetFile = this.metadata.files[targetFileIndex];
2518
+ if (!Array.isArray(data)) {
2519
+ dataToWrite = data;
2520
+ dataLeftOver = null;
2521
+ return { dataToWrite, dataLeftOver, fileName: targetFile.fileName };
2522
+ } else {
2523
+ dataToWrite = data.slice(0, this.pageSize);
2524
+ dataLeftOver = data.slice(this.pageSize);
2525
+ this.lastFileData = dataToWrite;
2526
+ return { dataToWrite, dataLeftOver, fileName: targetFile.fileName };
2527
+ }
2528
+ }
2529
+ let newlyCreatedFileIndex = null;
2530
+ if (forceNewFile) {
2531
+ const filesBeforeCreate = this.metadata.files.length;
2532
+ this.makeNewFile();
2533
+ newlyCreatedFileIndex = filesBeforeCreate;
2534
+ this.logger.debug?.(`[FileDatabase] Creating new file for unique custom metadata combination, fileNumber: ${this.currentFileNumber}`);
2535
+ } else if (this.metadata.files.length === 0) {
2514
2536
  this.makeNewFile();
2515
2537
  }
2516
2538
  const lastFile = this.metadata.files[this.metadata.files.length - 1];
2517
2539
  const lastFileRecordsCount = lastFile.recordsCount;
2518
- if (!Array.isArray(data)) {
2540
+ if (forceNewFile && newlyCreatedFileIndex !== null) {
2541
+ const newlyCreatedFile = this.metadata.files[newlyCreatedFileIndex];
2542
+ if (newlyCreatedFile && newlyCreatedFile.fileName !== lastFile.fileName) {
2543
+ this.logger.warn?.(`[FileDatabase] Warning: Newly created file ${newlyCreatedFile.fileName} doesn't match last file ${lastFile.fileName}`);
2544
+ }
2545
+ }
2546
+ if (!Array.isArray(data) && !forceNewFile) {
2519
2547
  const lastFileExtension = path3.extname(lastFile.fileName);
2520
2548
  const expectedExtension = `.${getFileExtension(incomingDataType)}`;
2521
2549
  if (lastFileExtension !== expectedExtension) {
@@ -2525,24 +2553,35 @@ var FileDatabase = class {
2525
2553
  lastFile.fileName = `${this.currentFileNumber.toString().padStart(6, "0")}.${getFileExtension(incomingDataType)}`;
2526
2554
  }
2527
2555
  }
2556
+ } else if (!Array.isArray(data) && forceNewFile) {
2557
+ const lastFileExtension = path3.extname(lastFile.fileName);
2558
+ const expectedExtension = `.${getFileExtension(incomingDataType)}`;
2559
+ if (lastFileExtension !== expectedExtension) {
2560
+ lastFile.fileName = `${this.currentFileNumber.toString().padStart(6, "0")}.${getFileExtension(incomingDataType)}`;
2561
+ }
2528
2562
  }
2529
2563
  if (Array.isArray(data)) {
2530
- if (lastFileRecordsCount < this.pageSize) {
2564
+ if (forceNewFile) {
2565
+ dataToWrite = data.slice(0, this.pageSize);
2566
+ dataLeftOver = data.slice(this.pageSize);
2567
+ this.lastFileData = dataToWrite;
2568
+ } else if (lastFileRecordsCount < this.pageSize) {
2531
2569
  dataToWrite = [...this.lastFileData || [], ...data.slice(0, this.pageSize - lastFileRecordsCount)];
2532
2570
  dataLeftOver = data.slice(this.pageSize - lastFileRecordsCount);
2571
+ this.lastFileData = dataToWrite;
2533
2572
  } else {
2534
2573
  this.makeNewFile();
2535
2574
  dataToWrite = data.slice(0, this.pageSize);
2536
2575
  dataLeftOver = data.slice(this.pageSize);
2576
+ this.lastFileData = dataToWrite;
2537
2577
  }
2538
- this.lastFileData = dataToWrite;
2539
2578
  } else {
2540
2579
  dataToWrite = data;
2541
2580
  dataLeftOver = null;
2542
2581
  }
2543
2582
  const fileName = this.metadata.files[this.metadata.files.length - 1].fileName;
2544
2583
  this.logger.debug?.(
2545
- `[FileDatabase] figureOutDataAndFileToWrite: filename=${fileName}, dataToWrite.length=${Array.isArray(dataToWrite) ? dataToWrite.length : "N/A"}, lastFileRecordsCount=${lastFileRecordsCount}`
2584
+ `[FileDatabase] figureOutDataAndFileToWrite: filename=${fileName}, forceNewFile=${forceNewFile}, targetFileIndex=${targetFileIndex}, dataToWrite.length=${Array.isArray(dataToWrite) ? dataToWrite.length : "N/A"}, lastFileRecordsCount=${lastFileRecordsCount}`
2546
2585
  );
2547
2586
  return { dataToWrite, dataLeftOver, fileName };
2548
2587
  }
@@ -2570,7 +2609,7 @@ var FileDatabase = class {
2570
2609
  /**
2571
2610
  * Update metadata after writing data
2572
2611
  */
2573
- updateMetadata(dataToWrite, fileName) {
2612
+ updateMetadata(dataToWrite, fileName, customMetadata) {
2574
2613
  let currentFile;
2575
2614
  if (fileName) {
2576
2615
  const foundFile = this.metadata.files.find((file) => file.fileName === fileName);
@@ -2585,6 +2624,9 @@ var FileDatabase = class {
2585
2624
  }
2586
2625
  const recordsCount = Array.isArray(dataToWrite) ? dataToWrite.length : 1;
2587
2626
  currentFile.recordsCount = recordsCount;
2627
+ if (customMetadata) {
2628
+ Object.assign(currentFile, customMetadata);
2629
+ }
2588
2630
  const fileIndex = this.metadata.files.indexOf(currentFile);
2589
2631
  if (fileIndex !== -1) {
2590
2632
  this.calculateFileSynopsis(dataToWrite, fileIndex);
@@ -2637,6 +2679,11 @@ var FileDatabase = class {
2637
2679
  } else {
2638
2680
  if (!this.metadata.files.length) {
2639
2681
  this.metadata = await this.figureMetadata(this.currentVersion);
2682
+ if (this.metadata.files && this.metadata.files.length > 0) {
2683
+ this.currentFileNumber = Math.max(...this.metadata.files.map((f) => f.number || 0));
2684
+ } else {
2685
+ this.currentFileNumber = 0;
2686
+ }
2640
2687
  }
2641
2688
  }
2642
2689
  } else {
@@ -2647,16 +2694,22 @@ var FileDatabase = class {
2647
2694
  try {
2648
2695
  const rawData = await fs3.promises.readFile(metadataPath, "utf8");
2649
2696
  this.metadata = JSON.parse(rawData);
2697
+ if (this.metadata.files && this.metadata.files.length > 0) {
2698
+ this.currentFileNumber = Math.max(...this.metadata.files.map((f) => f.number || 0));
2699
+ } else {
2700
+ this.currentFileNumber = 0;
2701
+ }
2650
2702
  } catch (e) {
2651
2703
  this.metadata = this.getDefaultMetadata();
2704
+ this.currentFileNumber = 0;
2652
2705
  }
2653
2706
  } else {
2654
2707
  this.metadata = this.getDefaultMetadata();
2655
- this.makeNewFile();
2708
+ this.currentFileNumber = 0;
2656
2709
  }
2657
2710
  } else {
2658
2711
  this.metadata = this.getDefaultMetadata();
2659
- this.makeNewFile();
2712
+ this.currentFileNumber = 0;
2660
2713
  }
2661
2714
  }
2662
2715
  } else if (read) {
@@ -2675,6 +2728,11 @@ var FileDatabase = class {
2675
2728
  }
2676
2729
  if (!this.metadata.files.length) {
2677
2730
  this.metadata = await this.figureMetadata(this.currentVersion);
2731
+ if (this.metadata.files && this.metadata.files.length > 0) {
2732
+ this.currentFileNumber = Math.max(...this.metadata.files.map((f) => f.number || 0));
2733
+ } else {
2734
+ this.currentFileNumber = 0;
2735
+ }
2678
2736
  }
2679
2737
  } else {
2680
2738
  this.currentVersion = null;
@@ -2688,6 +2746,11 @@ var FileDatabase = class {
2688
2746
  try {
2689
2747
  const rawData = await fs3.promises.readFile(metadataPath, "utf8");
2690
2748
  this.metadata = JSON.parse(rawData);
2749
+ if (this.metadata.files && this.metadata.files.length > 0) {
2750
+ this.currentFileNumber = Math.max(...this.metadata.files.map((f) => f.number || 0));
2751
+ } else {
2752
+ this.currentFileNumber = 0;
2753
+ }
2691
2754
  } catch (e) {
2692
2755
  throw new FileDatabaseError(`Failed to read metadata: ${e.message}`);
2693
2756
  }
@@ -2696,6 +2759,11 @@ var FileDatabase = class {
2696
2759
  }
2697
2760
  } else {
2698
2761
  this.metadata = await this.figureMetadataFromVersionFiles("");
2762
+ if (this.metadata.files && this.metadata.files.length > 0) {
2763
+ this.currentFileNumber = Math.max(...this.metadata.files.map((f) => f.number || 0));
2764
+ } else {
2765
+ this.currentFileNumber = 0;
2766
+ }
2699
2767
  }
2700
2768
  }
2701
2769
  }
@@ -2717,14 +2785,44 @@ var FileDatabase = class {
2717
2785
  this.metadata.dataType = incomingDataType;
2718
2786
  this.makeNewFile();
2719
2787
  }
2720
- let { dataToWrite, dataLeftOver, fileName } = this.figureOutDataAndFileToWrite(data);
2788
+ let targetFileIndex = null;
2789
+ const hasCustomMetadata = options.customMetadata && Object.keys(options.customMetadata).length > 0;
2790
+ if (hasCustomMetadata) {
2791
+ for (let i = 0; i < this.metadata.files.length; i++) {
2792
+ const fileEntry = this.metadata.files[i];
2793
+ const matches = Object.keys(options.customMetadata).every((key) => {
2794
+ return key in fileEntry && fileEntry[key] === options.customMetadata[key];
2795
+ });
2796
+ if (matches) {
2797
+ targetFileIndex = i;
2798
+ this.logger.debug?.(`[FileDatabase] Found existing file with matching custom metadata: ${fileEntry.fileName}, metadata: ${JSON.stringify(options.customMetadata)}`);
2799
+ break;
2800
+ } else {
2801
+ this.logger.debug?.(`[FileDatabase] File ${fileEntry.fileName} does not match custom metadata: ${JSON.stringify(options.customMetadata)}`);
2802
+ }
2803
+ }
2804
+ if (targetFileIndex === null) {
2805
+ this.logger.debug?.(`[FileDatabase] No existing file found with custom metadata: ${JSON.stringify(options.customMetadata)}, will create new file`);
2806
+ }
2807
+ } else {
2808
+ this.logger.debug?.(`[FileDatabase] No custom metadata provided, will create new file`);
2809
+ }
2810
+ if (targetFileIndex !== null) {
2811
+ const targetFile = this.metadata.files[targetFileIndex];
2812
+ this.currentFileNumber = targetFile.number;
2813
+ this.lastFileData = null;
2814
+ this.currentRecord = 0;
2815
+ this.hasReadFirstPage = false;
2816
+ }
2817
+ const forceNewFile = hasCustomMetadata && targetFileIndex === null;
2818
+ let { dataToWrite, dataLeftOver, fileName } = this.figureOutDataAndFileToWrite(data, targetFileIndex, forceNewFile);
2721
2819
  const destPath = this.getDestinationPath(this.currentVersion || void 0);
2722
2820
  await this.safeWrite(path3.join(destPath, fileName), dataToWrite);
2723
- this.updateMetadata(dataToWrite, fileName);
2724
- while (dataLeftOver && dataLeftOver.length > 0) {
2821
+ this.updateMetadata(dataToWrite, fileName, options.customMetadata);
2822
+ while (dataLeftOver && dataLeftOver.length > 0 && targetFileIndex === null) {
2725
2823
  const writeContext = this.figureOutDataAndFileToWrite(dataLeftOver);
2726
2824
  await this.safeWrite(path3.join(destPath, writeContext.fileName), writeContext.dataToWrite);
2727
- this.updateMetadata(writeContext.dataToWrite, writeContext.fileName);
2825
+ this.updateMetadata(writeContext.dataToWrite, writeContext.fileName, options.customMetadata);
2728
2826
  dataLeftOver = writeContext.dataLeftOver;
2729
2827
  }
2730
2828
  this.calculateVersionSynopsis();
@@ -2841,7 +2939,85 @@ var FileDatabase = class {
2841
2939
  getMetadata() {
2842
2940
  return { ...this.metadata };
2843
2941
  }
2942
+ /**
2943
+ * Find data by custom metadata fields
2944
+ * Searches through all versions and files to find entries matching the search criteria
2945
+ *
2946
+ * @param searchCriteria - Object with field names and values to search for (e.g., { ListingKey: "123", id: "456" })
2947
+ * @returns Array of found entries with their file paths and metadata
2948
+ */
2949
+ async findData(searchCriteria) {
2950
+ const results = [];
2951
+ if (!this.versioned) {
2952
+ await this.prepare({ read: true });
2953
+ const metadata = this.getMetadata();
2954
+ for (const fileEntry of metadata.files) {
2955
+ const matches = Object.keys(searchCriteria).every((key) => {
2956
+ return fileEntry[key] === searchCriteria[key];
2957
+ });
2958
+ if (matches) {
2959
+ const destPath = this.getDestinationPath();
2960
+ const filePath = path3.join(destPath, fileEntry.fileName);
2961
+ const fileData = await fs3.promises.readFile(filePath, "utf8");
2962
+ const data = deserializeData(fileData, metadata.dataType || "json-object");
2963
+ results.push({
2964
+ filePath,
2965
+ fileName: fileEntry.fileName,
2966
+ version: null,
2967
+ metadata: fileEntry,
2968
+ data
2969
+ });
2970
+ }
2971
+ }
2972
+ } else {
2973
+ const versions = await this.getVersions();
2974
+ for (const version of versions) {
2975
+ await this.prepare({ read: true, version });
2976
+ const metadata = this.getMetadata();
2977
+ for (const fileEntry of metadata.files) {
2978
+ const matches = Object.keys(searchCriteria).every((key) => {
2979
+ return fileEntry[key] === searchCriteria[key];
2980
+ });
2981
+ if (matches) {
2982
+ const destPath = this.getDestinationPath(version);
2983
+ const filePath = path3.join(destPath, fileEntry.fileName);
2984
+ const fileData = await fs3.promises.readFile(filePath, "utf8");
2985
+ const data = deserializeData(fileData, metadata.dataType || "json-object");
2986
+ results.push({
2987
+ filePath,
2988
+ fileName: fileEntry.fileName,
2989
+ version,
2990
+ metadata: fileEntry,
2991
+ data
2992
+ });
2993
+ }
2994
+ }
2995
+ }
2996
+ }
2997
+ return results;
2998
+ }
2844
2999
  };
3000
+ function listTables(basePath, namespace) {
3001
+ const namespacePath = path3.join(basePath, namespace);
3002
+ if (!fs3.existsSync(namespacePath)) {
3003
+ return [];
3004
+ }
3005
+ try {
3006
+ return fs3.readdirSync(namespacePath, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name);
3007
+ } catch (error) {
3008
+ return [];
3009
+ }
3010
+ }
3011
+ function listSources(basePath) {
3012
+ if (!fs3.existsSync(basePath)) {
3013
+ return [];
3014
+ }
3015
+ try {
3016
+ return fs3.readdirSync(basePath, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name);
3017
+ } catch (error) {
3018
+ return [];
3019
+ }
3020
+ }
2845
3021
  function fileDatabaseInit(context, options = {}) {
2846
3022
  return new FileDatabase(context, options);
2847
3023
  }
@@ -2854,6 +3030,10 @@ var Db = class {
2854
3030
  logger;
2855
3031
  queriesLog = [];
2856
3032
  isConnected = false;
3033
+ /**
3034
+ * Constructor - accepts config object
3035
+ * Use dbInit() function to initialize with Context
3036
+ */
2857
3037
  constructor(config2) {
2858
3038
  if (!config2.connectionString) {
2859
3039
  throw new ParamError("Db: connectionString is required");
@@ -3140,6 +3320,91 @@ var Db = class {
3140
3320
  return this.isConnected && this.knexInstance !== null;
3141
3321
  }
3142
3322
  };
3323
+ function capitalizeFirstLetter(str) {
3324
+ return str.charAt(0).toUpperCase() + str.slice(1);
3325
+ }
3326
+ async function dbConnect(context, connectionString, name, dbProfile) {
3327
+ const defs = {
3328
+ testDbConnection: "boolean default true",
3329
+ name: "string",
3330
+ poolMin: "number default 2",
3331
+ poolMax: "number default 10",
3332
+ acquireConnectionTimeout: "number default 10000",
3333
+ sslRejectUnauthorized: "boolean default false"
3334
+ };
3335
+ const paramsConfig = context.params.getAll(defs);
3336
+ const config2 = {
3337
+ connectionString,
3338
+ name: paramsConfig.name || name || "default",
3339
+ testConnection: paramsConfig.testDbConnection,
3340
+ profile: dbProfile ?? false,
3341
+ pool: {
3342
+ min: paramsConfig.poolMin,
3343
+ max: paramsConfig.poolMax
3344
+ },
3345
+ acquireConnectionTimeout: paramsConfig.acquireConnectionTimeout,
3346
+ ssl: {
3347
+ rejectUnauthorized: paramsConfig.sslRejectUnauthorized
3348
+ },
3349
+ logger: context.logger
3350
+ };
3351
+ try {
3352
+ const db = new Db(config2);
3353
+ context.registerCleanup(async () => {
3354
+ await db.disconnect();
3355
+ context.logger.debug(`[Db] instance "${name || connectionString}" destroyed`);
3356
+ });
3357
+ await db.connect();
3358
+ context.logger.debug(`[Db] instance "${name || connectionString}" initialized`);
3359
+ return db;
3360
+ } catch (error) {
3361
+ if (error instanceof ParamError) {
3362
+ throw error;
3363
+ }
3364
+ const errorMsg = error instanceof Error ? error.message : String(error);
3365
+ throw new ParamError(`[Db] connect error: ${errorMsg}`);
3366
+ }
3367
+ }
3368
+ async function dbFindAndConnect(context, dbNameOrConnectionString) {
3369
+ let dbName;
3370
+ let dbConnectionString;
3371
+ let dbProfile;
3372
+ if (dbNameOrConnectionString) {
3373
+ if (dbNameOrConnectionString.match(/^(postgresql|mysql):\/\/[^\s]+:[^\s]+@[^\s]+:\d+\/[^\s]+$/)) {
3374
+ dbName = void 0;
3375
+ dbConnectionString = dbNameOrConnectionString;
3376
+ } else {
3377
+ dbName = dbNameOrConnectionString;
3378
+ }
3379
+ } else {
3380
+ const defs = {
3381
+ dbName: "string",
3382
+ dbConnectionString: "string",
3383
+ dbProfile: "boolean default false"
3384
+ };
3385
+ const paramsConfig = context.params.getAll(defs);
3386
+ dbName = paramsConfig.dbName;
3387
+ dbConnectionString = paramsConfig.dbConnectionString;
3388
+ dbProfile = paramsConfig.dbProfile;
3389
+ }
3390
+ if (!dbName && !dbConnectionString) {
3391
+ throw new ParamError("Db: either dbName or dbConnectionString must be specified");
3392
+ }
3393
+ if (dbName) {
3394
+ const paramName = `dbConnectionString${capitalizeFirstLetter(dbName)}`;
3395
+ dbConnectionString = await context.params.get(paramName, "string");
3396
+ if (!dbConnectionString) {
3397
+ throw new ParamError(
3398
+ `Db: cannot find dbConnectionString for dbName="${dbName}" (looked for param "${paramName}")`
3399
+ );
3400
+ }
3401
+ }
3402
+ const db = await dbConnect(context, dbConnectionString, dbName, dbProfile);
3403
+ return db;
3404
+ }
3405
+ async function dbInit(context, dbNameOrConnectionString) {
3406
+ return await dbFindAndConnect(context, dbNameOrConnectionString);
3407
+ }
3143
3408
 
3144
3409
  // src/logger/index.ts
3145
3410
  import chalk from "chalk";
@@ -3534,6 +3799,9 @@ export {
3534
3799
  buildBreadcrumb,
3535
3800
  buildDetailBreadcrumb,
3536
3801
  buildFooter,
3802
+ dbConnect,
3803
+ dbFindAndConnect,
3804
+ dbInit,
3537
3805
  defaultFileSynopsisFunction,
3538
3806
  defaultVersionSynopsisFunction,
3539
3807
  fileDatabaseInit,
@@ -3541,6 +3809,8 @@ export {
3541
3809
  createElement2 as h,
3542
3810
  joiEdateType,
3543
3811
  joiStringArrayType,
3812
+ listSources,
3813
+ listTables,
3544
3814
  load,
3545
3815
  organizeFooterMessages,
3546
3816
  setupContext,