@nmakarov/cli-toolkit 0.2.0 → 0.3.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.
@@ -0,0 +1,1483 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/mock-server.ts
31
+ var mock_server_exports = {};
32
+ __export(mock_server_exports, {
33
+ MockServer: () => MockServer,
34
+ createMockServer: () => createMockServer
35
+ });
36
+ module.exports = __toCommonJS(mock_server_exports);
37
+
38
+ // src/mock-server/index.ts
39
+ var import_express = __toESM(require("express"), 1);
40
+ var import_http = __toESM(require("http"), 1);
41
+ var import_morgan = __toESM(require("morgan"), 1);
42
+
43
+ // src/filedatabase/index.ts
44
+ var import_fs3 = __toESM(require("fs"), 1);
45
+ var import_path3 = __toESM(require("path"), 1);
46
+
47
+ // src/utils/os-utils.ts
48
+ var import_fs = __toESM(require("fs"), 1);
49
+ var import_path = __toESM(require("path"), 1);
50
+ var import_child_process = require("child_process");
51
+ function getFreeDiskSpace(targetPath) {
52
+ try {
53
+ let pathToCheck = targetPath;
54
+ if (!import_fs.default.existsSync(targetPath)) {
55
+ const parentDir = import_path.default.dirname(targetPath);
56
+ if (import_fs.default.existsSync(parentDir)) {
57
+ pathToCheck = parentDir;
58
+ } else {
59
+ pathToCheck = process.platform === "win32" ? "C:\\" : "/";
60
+ }
61
+ }
62
+ if (process.platform === "win32") {
63
+ return null;
64
+ } else {
65
+ const stdout = (0, import_child_process.execSync)(`df -k "${pathToCheck}"`, { encoding: "utf8" });
66
+ const lines = stdout.trim().split("\n");
67
+ const parts = lines[1].split(/\s+/);
68
+ const freeKb = parseInt(parts[3], 10);
69
+ return freeKb * 1024;
70
+ }
71
+ } catch (error) {
72
+ return null;
73
+ }
74
+ }
75
+
76
+ // src/utils/fs-utils.ts
77
+ var import_fs2 = __toESM(require("fs"), 1);
78
+ var import_path2 = __toESM(require("path"), 1);
79
+ async function ensurePath(...pathParts) {
80
+ const fullPath = import_path2.default.resolve(...pathParts);
81
+ if (!import_fs2.default.existsSync(fullPath)) {
82
+ await import_fs2.default.promises.mkdir(fullPath, { recursive: true });
83
+ }
84
+ return fullPath;
85
+ }
86
+ function getFileExtension(dataType) {
87
+ switch (dataType) {
88
+ case "json-array":
89
+ case "json-object":
90
+ return "json";
91
+ case "text":
92
+ return "txt";
93
+ case "xml":
94
+ return "xml";
95
+ default:
96
+ return "json";
97
+ }
98
+ }
99
+
100
+ // src/utils/format-utils.ts
101
+ function bytesToHumanReadable(bytes) {
102
+ if (bytes === 0) return "0 B";
103
+ const k = 1024;
104
+ const sizes = ["B", "KB", "MB", "GB", "TB", "PB"];
105
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
106
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
107
+ }
108
+
109
+ // src/utils/date-utils.ts
110
+ function isTimestampFolder(folderName) {
111
+ const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|\.\d{3}Z)$/;
112
+ if (!isoRegex.test(folderName)) {
113
+ return false;
114
+ }
115
+ const date = new Date(folderName);
116
+ return !isNaN(date.getTime()) && date.getTime() > 0;
117
+ }
118
+
119
+ // src/filedatabase/serializers.ts
120
+ function detectDataType(data) {
121
+ if (Array.isArray(data)) {
122
+ return "json-array";
123
+ } else if (typeof data === "object" && data !== null) {
124
+ return "json-object";
125
+ } else if (typeof data === "string") {
126
+ const trimmed = data.trim();
127
+ if (trimmed.startsWith("<?xml") || trimmed.startsWith("<")) {
128
+ return "xml";
129
+ }
130
+ return "text";
131
+ } else {
132
+ return "text";
133
+ }
134
+ }
135
+ function serializeData(data) {
136
+ const dataType = detectDataType(data);
137
+ if (dataType === "json-array" || dataType === "json-object") {
138
+ return JSON.stringify(data, null, 4);
139
+ } else {
140
+ return String(data);
141
+ }
142
+ }
143
+ function deserializeData(rawData, dataType) {
144
+ if (dataType === "json-array" || dataType === "json-object") {
145
+ return JSON.parse(rawData);
146
+ } else {
147
+ return rawData;
148
+ }
149
+ }
150
+
151
+ // src/errors.ts
152
+ var FrameworkError = class extends Error {
153
+ constructor(message) {
154
+ super(message);
155
+ this.name = "FrameworkError";
156
+ }
157
+ };
158
+ var ParamError = class extends FrameworkError {
159
+ constructor(message) {
160
+ super(message);
161
+ this.name = "ParamError";
162
+ }
163
+ };
164
+
165
+ // src/filedatabase/index.ts
166
+ var FileDatabaseError = class extends Error {
167
+ constructor(message) {
168
+ super(message);
169
+ this.name = "FileDatabaseError";
170
+ }
171
+ };
172
+ var FileDatabase = class {
173
+ basePath;
174
+ namespace;
175
+ tableName = null;
176
+ versioned;
177
+ maxVersions;
178
+ pageSize;
179
+ useMetadata;
180
+ freeSpaceThreshold;
181
+ logger;
182
+ // Current operation state
183
+ currentVersion = null;
184
+ currentVersionFolder = null;
185
+ currentFileNumber = 0;
186
+ currentRecord = 0;
187
+ hasReadFirstPage = false;
188
+ lastFileData = null;
189
+ metadata;
190
+ // Synopsis calculation functions
191
+ fileSynopsisFunction = null;
192
+ versionSynopsisFunction = null;
193
+ constructor(config) {
194
+ if (!config.basePath) {
195
+ throw new ParamError("[FileDatabase] basePath is required");
196
+ }
197
+ this.basePath = config.basePath;
198
+ this.namespace = config.namespace || "default";
199
+ this.tableName = config.tableName || null;
200
+ this.versioned = config.versioned ?? true;
201
+ this.maxVersions = config.maxVersions || 5;
202
+ this.pageSize = config.pageSize || 5e3;
203
+ this.useMetadata = config.useMetadata !== false;
204
+ this.freeSpaceThreshold = config.freeSpaceThreshold || 100 * 1024 * 1024;
205
+ this.logger = config.logger || console;
206
+ this.metadata = this.getDefaultMetadata();
207
+ }
208
+ /**
209
+ * Get default metadata structure
210
+ */
211
+ getDefaultMetadata() {
212
+ return {
213
+ version: this.currentVersion || null,
214
+ files: [],
215
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
216
+ modifiedAt: (/* @__PURE__ */ new Date()).toISOString(),
217
+ totalRecords: 0,
218
+ synopsis: null,
219
+ dataType: null
220
+ };
221
+ }
222
+ /**
223
+ * Get the destination path (basePath/namespace/tableName[/version])
224
+ */
225
+ getDestinationPath(version) {
226
+ const errors = ["basePath", "namespace", "tableName"].filter((prop) => !this[prop]).map((prop) => `${prop} is not set`);
227
+ if (errors.length) {
228
+ throw new FileDatabaseError(`[FileDatabase] ${errors.join("; ")}`);
229
+ }
230
+ let parts = [this.basePath, this.namespace];
231
+ if (this.tableName) {
232
+ parts.push(...this.tableName.split("/"));
233
+ }
234
+ if (this.versioned && version) {
235
+ parts.push(version);
236
+ }
237
+ return import_path3.default.resolve(...parts);
238
+ }
239
+ /**
240
+ * Set current version and version folder
241
+ */
242
+ async setCurrentVersion(version) {
243
+ this.currentVersion = version;
244
+ this.currentVersionFolder = await ensurePath(this.getDestinationPath(), version);
245
+ }
246
+ /**
247
+ * Create a new version folder with comprehensive timestamp logic
248
+ * Only works in versioned mode
249
+ */
250
+ async makeNewVersion() {
251
+ if (!this.versioned) {
252
+ throw new FileDatabaseError("makeNewVersion() only works in versioned mode");
253
+ }
254
+ this.metadata = this.getDefaultMetadata();
255
+ const existingVersions = await this.getVersions();
256
+ let versionName;
257
+ if (existingVersions.length > 0) {
258
+ const maxTimestamp = existingVersions.reduce((max, version) => {
259
+ const versionDate = new Date(version.replace("Z", ""));
260
+ const maxDate2 = new Date(max.replace("Z", ""));
261
+ return versionDate > maxDate2 ? version : max;
262
+ });
263
+ const maxDate = new Date(maxTimestamp.replace("Z", ""));
264
+ const nextDate = new Date(maxDate.getTime() + 1e3);
265
+ versionName = nextDate.toISOString().split(".")[0] + "Z";
266
+ } else {
267
+ const now = /* @__PURE__ */ new Date();
268
+ versionName = now.toISOString().split(".")[0] + "Z";
269
+ }
270
+ await this.setCurrentVersion(versionName);
271
+ this.currentFileNumber = 0;
272
+ const versions = await this.getVersions();
273
+ while (versions.length > this.maxVersions) {
274
+ const versionToDelete = import_path3.default.resolve(this.getDestinationPath(), versions.shift());
275
+ this.logger.debug?.(`[FileDatabase] Deleting old version: ${versionToDelete}`);
276
+ await import_fs3.default.promises.rm(versionToDelete, { recursive: true, force: true });
277
+ }
278
+ return versionName;
279
+ }
280
+ /**
281
+ * Get list of all versions (sorted chronologically)
282
+ * Only works in versioned mode
283
+ */
284
+ async getVersions() {
285
+ if (!this.versioned) {
286
+ return [];
287
+ }
288
+ const destPath = this.getDestinationPath();
289
+ try {
290
+ await ensurePath(destPath);
291
+ const items = await import_fs3.default.promises.readdir(destPath);
292
+ const versions = items.filter((item) => {
293
+ const itemPath = import_path3.default.join(destPath, item);
294
+ const stat = import_fs3.default.statSync(itemPath);
295
+ return stat.isDirectory() && isTimestampFolder(item);
296
+ });
297
+ return versions.sort();
298
+ } catch (error) {
299
+ return [];
300
+ }
301
+ }
302
+ /**
303
+ * Get the latest version (most recent timestamp)
304
+ * Only works in versioned mode
305
+ * @returns Latest version string or null if no versions
306
+ */
307
+ async getLatestVersion() {
308
+ if (!this.versioned) {
309
+ throw new FileDatabaseError("getLatestVersion() only works in versioned mode");
310
+ }
311
+ const versions = await this.getVersions();
312
+ if (versions.length === 0) {
313
+ return null;
314
+ }
315
+ return versions[versions.length - 1];
316
+ }
317
+ /**
318
+ * Check if any data exists in this table
319
+ * Works for both versioned and non-versioned modes
320
+ * @returns true if data exists
321
+ */
322
+ async hasData() {
323
+ const tablePath = this.getDestinationPath();
324
+ if (!import_fs3.default.existsSync(tablePath)) {
325
+ return false;
326
+ }
327
+ if (this.versioned) {
328
+ const versions = await this.getVersions();
329
+ return versions.length > 0;
330
+ } else {
331
+ const items = await import_fs3.default.promises.readdir(tablePath);
332
+ return items.some(
333
+ (item) => item === "metadata.json" || item.match(/^\d{6}\.(json|txt|xml)$/) || item.endsWith(".json")
334
+ );
335
+ }
336
+ }
337
+ /**
338
+ * Auto-detect the data format in this table
339
+ * Used when reading existing data
340
+ * @returns Format detection result
341
+ */
342
+ async detectDataFormat() {
343
+ const tablePath = this.getDestinationPath();
344
+ if (!import_fs3.default.existsSync(tablePath)) {
345
+ return { versioned: false, hasMetadata: false, dataType: null };
346
+ }
347
+ const items = await import_fs3.default.promises.readdir(tablePath);
348
+ if (items.includes("metadata.json")) {
349
+ const metadata = JSON.parse(
350
+ await import_fs3.default.promises.readFile(import_path3.default.join(tablePath, "metadata.json"), "utf8")
351
+ );
352
+ return {
353
+ versioned: false,
354
+ hasMetadata: true,
355
+ dataType: metadata.dataType || null
356
+ };
357
+ }
358
+ const versionFolders = items.filter((item) => {
359
+ const itemPath = import_path3.default.join(tablePath, item);
360
+ const stat = import_fs3.default.statSync(itemPath);
361
+ return stat.isDirectory() && isTimestampFolder(item);
362
+ });
363
+ if (versionFolders.length > 0) {
364
+ const latestVersion = versionFolders.sort().pop();
365
+ const versionMetadataPath = import_path3.default.join(tablePath, latestVersion, "metadata.json");
366
+ return {
367
+ versioned: true,
368
+ hasMetadata: import_fs3.default.existsSync(versionMetadataPath),
369
+ dataType: null
370
+ };
371
+ }
372
+ const dataFiles = items.filter((f) => f.match(/^\d{6}\.(json|txt|xml)$/));
373
+ if (dataFiles.length > 0) {
374
+ return {
375
+ versioned: false,
376
+ hasMetadata: false,
377
+ dataType: null
378
+ };
379
+ }
380
+ return { versioned: false, hasMetadata: false, dataType: null };
381
+ }
382
+ /**
383
+ * Load metadata from JSON file
384
+ */
385
+ async loadMetadataJson(version) {
386
+ const metadataFile = import_path3.default.join(this.getDestinationPath(), version, "metadata.json");
387
+ if (import_fs3.default.existsSync(metadataFile)) {
388
+ try {
389
+ const rawData = await import_fs3.default.promises.readFile(metadataFile, "utf8");
390
+ return JSON.parse(rawData);
391
+ } catch (e) {
392
+ throw new FileDatabaseError(`Failed to read metadata for version "${version}": ${e.message}`);
393
+ }
394
+ }
395
+ return null;
396
+ }
397
+ /**
398
+ * Build metadata by scanning files in a version folder (backward compatibility)
399
+ * Reads all files to get accurate counts - used when synopsis calculation is needed
400
+ */
401
+ async figureMetadataFromVersionFiles(version) {
402
+ const versionPath = import_path3.default.join(this.getDestinationPath(), version);
403
+ if (!import_fs3.default.existsSync(versionPath)) {
404
+ return this.getDefaultMetadata();
405
+ }
406
+ const files = (await import_fs3.default.promises.readdir(versionPath)).filter((file) => file !== "metadata.json" && !file.startsWith(".")).sort();
407
+ const metadata = this.getDefaultMetadata();
408
+ metadata.version = version;
409
+ metadata.files = [];
410
+ let totalRecords = 0;
411
+ let detectedDataType = null;
412
+ for (let i = 0; i < files.length; i++) {
413
+ const fileName = files[i];
414
+ const filePath = import_path3.default.join(versionPath, fileName);
415
+ try {
416
+ const rawData = await import_fs3.default.promises.readFile(filePath, "utf8");
417
+ const extension = import_path3.default.extname(fileName).toLowerCase();
418
+ let dataType = "text";
419
+ if (extension === ".json") {
420
+ dataType = "json-array";
421
+ } else if (extension === ".xml") {
422
+ dataType = "xml";
423
+ }
424
+ const fileData = deserializeData(rawData, dataType);
425
+ const recordsCount = Array.isArray(fileData) ? fileData.length : 1;
426
+ if (detectedDataType === null) {
427
+ detectedDataType = detectDataType(fileData);
428
+ }
429
+ const fileInfo = {
430
+ number: i + 1,
431
+ recordsCount,
432
+ fileName
433
+ };
434
+ metadata.files.push(fileInfo);
435
+ totalRecords += recordsCount;
436
+ } catch (error) {
437
+ this.logger.error?.(`[FileDatabase] Failed to read file ${fileName}: ${error.message}`);
438
+ }
439
+ }
440
+ metadata.totalRecords = totalRecords;
441
+ metadata.dataType = detectedDataType;
442
+ return metadata;
443
+ }
444
+ /**
445
+ * Build metadata optimized - only reads first and last files
446
+ * Assumes all middle files have the same record count as the first file
447
+ * Much faster for large datasets with many files
448
+ */
449
+ async buildMetadataOptimized(version) {
450
+ const versionPath = import_path3.default.join(this.getDestinationPath(), version);
451
+ if (!import_fs3.default.existsSync(versionPath)) {
452
+ return this.getDefaultMetadata();
453
+ }
454
+ const files = (await import_fs3.default.promises.readdir(versionPath)).filter((file) => file !== "metadata.json" && !file.startsWith(".")).sort();
455
+ if (files.length === 0) {
456
+ return this.getDefaultMetadata();
457
+ }
458
+ const metadata = this.getDefaultMetadata();
459
+ metadata.version = version;
460
+ metadata.files = files.map((fileName, index) => ({
461
+ number: index + 1,
462
+ recordsCount: 0,
463
+ fileName
464
+ }));
465
+ const firstFile = metadata.files[0];
466
+ const firstFilePath = import_path3.default.join(versionPath, firstFile.fileName);
467
+ const firstFileRaw = await import_fs3.default.promises.readFile(firstFilePath, "utf8");
468
+ let firstFileData;
469
+ try {
470
+ firstFileData = JSON.parse(firstFileRaw);
471
+ } catch (e) {
472
+ firstFileData = firstFileRaw;
473
+ }
474
+ metadata.dataType = detectDataType(firstFileData);
475
+ if (metadata.dataType === "json-array") {
476
+ const firstFileCount = Array.isArray(firstFileData) ? firstFileData.length : 1;
477
+ firstFile.recordsCount = firstFileCount;
478
+ for (let i = 1; i < metadata.files.length - 1; i++) {
479
+ metadata.files[i].recordsCount = firstFileCount;
480
+ }
481
+ if (files.length > 1) {
482
+ const lastFile = metadata.files[metadata.files.length - 1];
483
+ const lastFilePath = import_path3.default.join(versionPath, lastFile.fileName);
484
+ const lastFileRaw = await import_fs3.default.promises.readFile(lastFilePath, "utf8");
485
+ const lastFileData = deserializeData(lastFileRaw, metadata.dataType);
486
+ lastFile.recordsCount = Array.isArray(lastFileData) ? lastFileData.length : 1;
487
+ }
488
+ metadata.totalRecords = metadata.files.reduce((sum, file) => sum + file.recordsCount, 0);
489
+ } else {
490
+ metadata.files.forEach((file) => {
491
+ file.recordsCount = 1;
492
+ });
493
+ metadata.totalRecords = files.length;
494
+ }
495
+ return metadata;
496
+ }
497
+ /**
498
+ * Figure out metadata - tries JSON first, then builds from files
499
+ * Uses optimized building when no synopsis calculation is needed
500
+ */
501
+ async figureMetadata(version, useOptimized = true) {
502
+ if (this.useMetadata) {
503
+ const metadata = await this.loadMetadataJson(version);
504
+ if (metadata) {
505
+ return metadata;
506
+ }
507
+ }
508
+ if (useOptimized && !this.fileSynopsisFunction && !this.versionSynopsisFunction) {
509
+ return await this.buildMetadataOptimized(version);
510
+ }
511
+ return await this.figureMetadataFromVersionFiles(version);
512
+ }
513
+ /**
514
+ * Load version metadata (main entry point for loading)
515
+ */
516
+ async loadVersionMetadata(version) {
517
+ const metadata = await this.figureMetadata(version);
518
+ this.metadata = metadata;
519
+ return metadata;
520
+ }
521
+ /**
522
+ * Save version metadata to file
523
+ */
524
+ async saveVersionMetadata(metadata) {
525
+ if (!this.useMetadata) {
526
+ return;
527
+ }
528
+ const metadataToSave = metadata || this.metadata;
529
+ let metadataFile;
530
+ if (this.versioned) {
531
+ if (!this.currentVersion) {
532
+ return;
533
+ }
534
+ metadataFile = import_path3.default.join(this.getDestinationPath(), this.currentVersion, "metadata.json");
535
+ } else {
536
+ metadataFile = import_path3.default.join(this.getDestinationPath(), "metadata.json");
537
+ }
538
+ await import_fs3.default.promises.writeFile(metadataFile, JSON.stringify(metadataToSave, null, 4), "utf8");
539
+ }
540
+ /**
541
+ * Create a new file entry in metadata
542
+ */
543
+ makeNewFile() {
544
+ this.currentFileNumber = (this.currentFileNumber || 0) + 1;
545
+ const dataType = this.metadata.dataType || "json-array";
546
+ const fileEntry = {
547
+ number: this.currentFileNumber,
548
+ recordsCount: 0,
549
+ fileName: `${this.currentFileNumber.toString().padStart(6, "0")}.${getFileExtension(dataType)}`
550
+ };
551
+ this.metadata.files.push(fileEntry);
552
+ this.lastFileData = null;
553
+ this.logger.debug?.(`[FileDatabase] Created new file: ${fileEntry.fileName}, fileNumber: ${this.currentFileNumber}`);
554
+ }
555
+ /**
556
+ * Figure out what data to write and which file to use (for pagination)
557
+ */
558
+ figureOutDataAndFileToWrite(data) {
559
+ let dataToWrite;
560
+ let dataLeftOver;
561
+ const lastFile = this.metadata.files[this.metadata.files.length - 1];
562
+ const lastFileRecordsCount = lastFile.recordsCount;
563
+ if (Array.isArray(data)) {
564
+ if (lastFileRecordsCount < this.pageSize) {
565
+ dataToWrite = [...this.lastFileData || [], ...data.slice(0, this.pageSize - lastFileRecordsCount)];
566
+ dataLeftOver = data.slice(this.pageSize - lastFileRecordsCount);
567
+ } else {
568
+ this.makeNewFile();
569
+ dataToWrite = data.slice(0, this.pageSize);
570
+ dataLeftOver = data.slice(this.pageSize);
571
+ }
572
+ this.lastFileData = dataToWrite;
573
+ } else {
574
+ dataToWrite = data;
575
+ dataLeftOver = null;
576
+ }
577
+ const fileName = this.metadata.files[this.metadata.files.length - 1].fileName;
578
+ this.logger.debug?.(
579
+ `[FileDatabase] figureOutDataAndFileToWrite: filename=${fileName}, dataToWrite.length=${Array.isArray(dataToWrite) ? dataToWrite.length : "N/A"}, lastFileRecordsCount=${lastFileRecordsCount}`
580
+ );
581
+ return { dataToWrite, dataLeftOver, fileName };
582
+ }
583
+ /**
584
+ * Calculate file-level synopsis if function is set
585
+ */
586
+ calculateFileSynopsis(data, fileIndex = this.metadata.files.length - 1) {
587
+ if (!this.fileSynopsisFunction) {
588
+ return;
589
+ }
590
+ const fileInfo = this.metadata.files[fileIndex];
591
+ const enhancedFileInfo = this.fileSynopsisFunction(fileInfo, data);
592
+ this.metadata.files[fileIndex] = enhancedFileInfo;
593
+ }
594
+ /**
595
+ * Calculate version-level synopsis if function is set
596
+ */
597
+ calculateVersionSynopsis() {
598
+ if (!this.versionSynopsisFunction) {
599
+ return;
600
+ }
601
+ const enhancedMetadata = this.versionSynopsisFunction(this.metadata);
602
+ this.metadata = enhancedMetadata;
603
+ }
604
+ /**
605
+ * Update metadata after writing data
606
+ */
607
+ updateMetadata(dataToWrite, fileName) {
608
+ let currentFile;
609
+ if (fileName) {
610
+ const foundFile = this.metadata.files.find((file) => file.fileName === fileName);
611
+ if (!foundFile) {
612
+ this.logger.warn?.(`[FileDatabase] File ${fileName} not found in metadata, using last file`);
613
+ currentFile = this.metadata.files[this.metadata.files.length - 1];
614
+ } else {
615
+ currentFile = foundFile;
616
+ }
617
+ } else {
618
+ currentFile = this.metadata.files[this.metadata.files.length - 1];
619
+ }
620
+ const recordsCount = Array.isArray(dataToWrite) ? dataToWrite.length : 1;
621
+ currentFile.recordsCount = recordsCount;
622
+ const fileIndex = this.metadata.files.indexOf(currentFile);
623
+ if (fileIndex !== -1) {
624
+ this.calculateFileSynopsis(dataToWrite, fileIndex);
625
+ }
626
+ this.metadata.version = this.currentVersion;
627
+ this.metadata.modifiedAt = (/* @__PURE__ */ new Date()).toISOString();
628
+ this.metadata.dataType = detectDataType(dataToWrite);
629
+ this.metadata.totalRecords = this.metadata.files.reduce((sum, file) => sum + (file.recordsCount || 0), 0);
630
+ this.logger.debug?.(
631
+ `[FileDatabase] Updated metadata for file ${currentFile.fileName}: recordsCount=${recordsCount}, totalRecords=${this.metadata.totalRecords}`
632
+ );
633
+ }
634
+ /**
635
+ * Safe write with disk space check
636
+ */
637
+ async safeWrite(filePath, data) {
638
+ const serializedData = serializeData(data);
639
+ const dir = import_path3.default.dirname(filePath);
640
+ const requiredBytes = Buffer.byteLength(serializedData, "utf8");
641
+ const freeBytes = getFreeDiskSpace(dir);
642
+ if (freeBytes !== null) {
643
+ if (freeBytes < requiredBytes) {
644
+ throw new FileDatabaseError(
645
+ `Not enough disk space. Required: ${bytesToHumanReadable(requiredBytes)}, Free: ${bytesToHumanReadable(freeBytes)}`
646
+ );
647
+ }
648
+ if (freeBytes < this.freeSpaceThreshold) {
649
+ this.logger.warn?.(`Low disk space warning: only ${bytesToHumanReadable(freeBytes)} left`);
650
+ }
651
+ }
652
+ try {
653
+ await import_fs3.default.promises.writeFile(filePath, serializedData, "utf8");
654
+ this.logger.debug?.(`[FileDatabase] Wrote ${bytesToHumanReadable(requiredBytes)} to ${filePath}`);
655
+ } catch (error) {
656
+ throw new FileDatabaseError(`Failed to write file ${filePath}: ${error.message}`);
657
+ }
658
+ }
659
+ /**
660
+ * Prepare the instance for read or write operations
661
+ * This discovers state and sets up internal members based on mode and current data
662
+ */
663
+ async prepare({ write, read, version }) {
664
+ if (write) {
665
+ if (this.versioned) {
666
+ if (this.currentVersion === null) {
667
+ await this.makeNewVersion();
668
+ this.metadata = this.getDefaultMetadata();
669
+ this.metadata.version = this.currentVersion;
670
+ this.makeNewFile();
671
+ } else {
672
+ if (!this.metadata.files.length) {
673
+ this.metadata = await this.figureMetadata(this.currentVersion);
674
+ }
675
+ }
676
+ } else {
677
+ await ensurePath(this.getDestinationPath());
678
+ if (this.useMetadata === true) {
679
+ const metadataPath = import_path3.default.join(this.getDestinationPath(), "metadata.json");
680
+ if (import_fs3.default.existsSync(metadataPath)) {
681
+ try {
682
+ const rawData = await import_fs3.default.promises.readFile(metadataPath, "utf8");
683
+ this.metadata = JSON.parse(rawData);
684
+ } catch (e) {
685
+ this.metadata = this.getDefaultMetadata();
686
+ }
687
+ } else {
688
+ this.metadata = this.getDefaultMetadata();
689
+ this.makeNewFile();
690
+ }
691
+ } else {
692
+ this.metadata = this.getDefaultMetadata();
693
+ this.makeNewFile();
694
+ }
695
+ }
696
+ } else if (read) {
697
+ if (this.versioned) {
698
+ const versions = await this.getVersions();
699
+ if (versions.length === 0) {
700
+ throw new FileDatabaseError("[FileDatabase] No versions found, cannot read");
701
+ }
702
+ if (version) {
703
+ if (!versions.includes(version)) {
704
+ throw new FileDatabaseError(`[FileDatabase] Version "${version}" not found`);
705
+ }
706
+ await this.setCurrentVersion(version);
707
+ } else {
708
+ await this.setCurrentVersion(versions[versions.length - 1]);
709
+ }
710
+ if (!this.metadata.files.length) {
711
+ this.metadata = await this.figureMetadata(this.currentVersion);
712
+ }
713
+ } else {
714
+ this.currentVersion = null;
715
+ if (this.useMetadata === void 0) {
716
+ const format = await this.detectDataFormat();
717
+ this.useMetadata = format.hasMetadata;
718
+ }
719
+ if (this.useMetadata) {
720
+ const metadataPath = import_path3.default.join(this.getDestinationPath(), "metadata.json");
721
+ if (import_fs3.default.existsSync(metadataPath)) {
722
+ try {
723
+ const rawData = await import_fs3.default.promises.readFile(metadataPath, "utf8");
724
+ this.metadata = JSON.parse(rawData);
725
+ } catch (e) {
726
+ throw new FileDatabaseError(`Failed to read metadata: ${e.message}`);
727
+ }
728
+ } else {
729
+ throw new FileDatabaseError("[FileDatabase] No metadata found in non-versioned mode");
730
+ }
731
+ } else {
732
+ this.metadata = await this.figureMetadataFromVersionFiles("");
733
+ }
734
+ }
735
+ }
736
+ }
737
+ /**
738
+ * Write data to the file database
739
+ */
740
+ async write(data, options = {}) {
741
+ if (options.forceNewVersion && !this.versioned) {
742
+ throw new FileDatabaseError("Cannot use forceNewVersion in non-versioned mode");
743
+ }
744
+ await this.prepare({ write: true });
745
+ if (options.forceNewVersion) {
746
+ await this.makeNewVersion();
747
+ this.metadata = this.getDefaultMetadata();
748
+ this.metadata.version = this.currentVersion;
749
+ this.makeNewFile();
750
+ }
751
+ let { dataToWrite, dataLeftOver, fileName } = this.figureOutDataAndFileToWrite(data);
752
+ const destPath = this.getDestinationPath(this.currentVersion || void 0);
753
+ await this.safeWrite(import_path3.default.join(destPath, fileName), dataToWrite);
754
+ this.updateMetadata(dataToWrite, fileName);
755
+ while (dataLeftOver && dataLeftOver.length > 0) {
756
+ const writeContext = this.figureOutDataAndFileToWrite(dataLeftOver);
757
+ await this.safeWrite(import_path3.default.join(destPath, writeContext.fileName), writeContext.dataToWrite);
758
+ this.updateMetadata(writeContext.dataToWrite, writeContext.fileName);
759
+ dataLeftOver = writeContext.dataLeftOver;
760
+ }
761
+ this.calculateVersionSynopsis();
762
+ if (this.useMetadata) {
763
+ await this.saveVersionMetadata(this.metadata);
764
+ }
765
+ }
766
+ /**
767
+ * Read data from the file database
768
+ */
769
+ async read(options = {}) {
770
+ const { version, nextPage = false, pageSize } = options;
771
+ await this.prepare({ read: true, version });
772
+ const isNonPaginatedData = this.metadata.dataType === "text" || this.metadata.dataType === "xml" || this.metadata.dataType === "json-object";
773
+ if (isNonPaginatedData) {
774
+ const file = this.metadata.files[0];
775
+ const filePath = import_path3.default.join(this.getDestinationPath(this.currentVersion || void 0), file.fileName);
776
+ try {
777
+ const rawData = await import_fs3.default.promises.readFile(filePath, "utf8");
778
+ return deserializeData(rawData, this.metadata.dataType);
779
+ } catch (error) {
780
+ throw new FileDatabaseError(`Failed to read file ${file.fileName}: ${error.message}`);
781
+ }
782
+ }
783
+ let effectivePageSize;
784
+ if (nextPage && this.hasReadFirstPage) {
785
+ effectivePageSize = pageSize || this.pageSize;
786
+ this.currentRecord += effectivePageSize;
787
+ } else if (!nextPage) {
788
+ effectivePageSize = pageSize !== void 0 ? pageSize : this.metadata.totalRecords;
789
+ this.currentRecord = 0;
790
+ } else {
791
+ effectivePageSize = pageSize || this.pageSize;
792
+ }
793
+ if (this.currentRecord >= this.metadata.totalRecords) {
794
+ return [];
795
+ }
796
+ const result = [];
797
+ let recordsRead = 0;
798
+ let currentFileIndex = 0;
799
+ let currentFileOffset = 0;
800
+ let totalRecords = 0;
801
+ for (let i = 0; i < this.metadata.files.length; i++) {
802
+ const file = this.metadata.files[i];
803
+ if (this.currentRecord < totalRecords + file.recordsCount) {
804
+ currentFileIndex = i;
805
+ currentFileOffset = totalRecords;
806
+ break;
807
+ }
808
+ totalRecords += file.recordsCount;
809
+ }
810
+ let cumulativeRecords = currentFileOffset;
811
+ for (let i = currentFileIndex; i < this.metadata.files.length && recordsRead < effectivePageSize; i++) {
812
+ const file = this.metadata.files[i];
813
+ const filePath = import_path3.default.join(this.getDestinationPath(this.currentVersion || void 0), file.fileName);
814
+ try {
815
+ const rawData = await import_fs3.default.promises.readFile(filePath, "utf8");
816
+ const fileData = deserializeData(rawData, this.metadata.dataType);
817
+ let startIndex = 0;
818
+ if (i === currentFileIndex) {
819
+ startIndex = this.currentRecord - cumulativeRecords;
820
+ }
821
+ const endIndex = Math.min(startIndex + (effectivePageSize - recordsRead), fileData.length);
822
+ const recordsFromThisFile = fileData.slice(startIndex, endIndex);
823
+ result.push(...recordsFromThisFile);
824
+ recordsRead += recordsFromThisFile.length;
825
+ cumulativeRecords += file.recordsCount;
826
+ } catch (error) {
827
+ throw new FileDatabaseError(`Failed to read file ${file.fileName}: ${error.message}`);
828
+ }
829
+ }
830
+ if (result.length > 0) {
831
+ if (nextPage || pageSize !== void 0 && pageSize < this.metadata.totalRecords) {
832
+ this.hasReadFirstPage = true;
833
+ }
834
+ }
835
+ return result;
836
+ }
837
+ /**
838
+ * Set the starting record for pagination (1-based index)
839
+ */
840
+ setStartRecord(startRecord) {
841
+ this.currentRecord = startRecord - 1;
842
+ this.hasReadFirstPage = false;
843
+ }
844
+ /**
845
+ * Reset read pagination state
846
+ */
847
+ resetPagination() {
848
+ this.currentRecord = 0;
849
+ this.hasReadFirstPage = false;
850
+ }
851
+ /**
852
+ * Set file-level synopsis calculation function
853
+ */
854
+ setFileSynopsisFunction(fn) {
855
+ this.fileSynopsisFunction = fn;
856
+ }
857
+ /**
858
+ * Set version-level synopsis calculation function
859
+ */
860
+ setVersionSynopsisFunction(fn) {
861
+ this.versionSynopsisFunction = fn;
862
+ }
863
+ /**
864
+ * Get current version name
865
+ */
866
+ getCurrentVersion() {
867
+ return this.currentVersion;
868
+ }
869
+ /**
870
+ * Get current metadata
871
+ */
872
+ getMetadata() {
873
+ return { ...this.metadata };
874
+ }
875
+ };
876
+
877
+ // src/mock-server/catalog.ts
878
+ var fs4 = __toESM(require("fs/promises"), 1);
879
+ var path4 = __toESM(require("path"), 1);
880
+
881
+ // src/mock-server/sanitization.ts
882
+ var import_crypto = require("crypto");
883
+ var import_querystring = require("querystring");
884
+ function maskValue2(value) {
885
+ return `[md5:${(0, import_crypto.createHash)("md5").update(value).digest("hex")}]`;
886
+ }
887
+ function sanitizeUrlEncodedString(input, keysToSanitize) {
888
+ const parsed = (0, import_querystring.parse)(input);
889
+ const sanitized = sanitizeObject(parsed, keysToSanitize);
890
+ return (0, import_querystring.stringify)(sanitized);
891
+ }
892
+ function sanitizeObject(obj, keysToSanitize) {
893
+ if (Array.isArray(obj)) {
894
+ return obj;
895
+ }
896
+ const result = {};
897
+ for (const [key, value] of Object.entries(obj || {})) {
898
+ if (keysToSanitize.includes(key.toLowerCase())) {
899
+ result[key] = maskValue2(String(value));
900
+ } else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
901
+ result[key] = sanitizeObject(value, keysToSanitize);
902
+ } else {
903
+ result[key] = value;
904
+ }
905
+ }
906
+ return result;
907
+ }
908
+ function sanitizeHeaders(headers, additionalKeys = []) {
909
+ const sensitiveKeys = ["authorization", "x-api-key", "api-key", "bearer", ...additionalKeys.map((k) => k.toLowerCase())];
910
+ const sanitized = { ...headers };
911
+ for (const [key, value] of Object.entries(sanitized)) {
912
+ if (sensitiveKeys.includes(key.toLowerCase())) {
913
+ sanitized[key] = maskValue2(value);
914
+ }
915
+ }
916
+ return sanitized;
917
+ }
918
+ function sanitizeRequestData(data, keysToSanitize) {
919
+ if (typeof data === "string") {
920
+ if (data.includes("=")) {
921
+ try {
922
+ const parsed = (0, import_querystring.parse)(data);
923
+ return (0, import_querystring.stringify)(sanitizeObject(parsed, keysToSanitize));
924
+ } catch {
925
+ return data;
926
+ }
927
+ }
928
+ return data;
929
+ } else if (typeof data === "object" && data !== null) {
930
+ return sanitizeObject(data, keysToSanitize);
931
+ }
932
+ return data;
933
+ }
934
+ function sanitizeResponseData(data, keysToSanitize) {
935
+ if (typeof data === "object" && data !== null) {
936
+ return sanitizeObject(data, keysToSanitize);
937
+ }
938
+ return data;
939
+ }
940
+ function sanitizeRequest(params, sensitiveKeys) {
941
+ const { query, requestData, headers = {}, data } = params;
942
+ return {
943
+ query: sanitizeUrlEncodedString(query, sensitiveKeys),
944
+ requestData: requestData ? sanitizeRequestData(requestData, sensitiveKeys) : void 0,
945
+ headers: sanitizeHeaders(headers, sensitiveKeys),
946
+ data: data ? sanitizeResponseData(data, sensitiveKeys) : data
947
+ };
948
+ }
949
+
950
+ // src/mock-server/catalog.ts
951
+ var MockCatalog = class {
952
+ fileDb;
953
+ catalogPath;
954
+ logger;
955
+ constructor(fileDb, catalogPath, logger = console) {
956
+ this.fileDb = fileDb;
957
+ this.catalogPath = catalogPath;
958
+ this.logger = logger;
959
+ }
960
+ /**
961
+ * Store a mock response in the catalog
962
+ */
963
+ async storeMock(requestUrl, requestData, responseData, operationId, mockName, sensitiveKeys = []) {
964
+ try {
965
+ const url = new URL(requestUrl);
966
+ const sanitized = sanitizeRequest({
967
+ query: url.search.slice(1),
968
+ requestData,
969
+ headers: {},
970
+ data: responseData.data
971
+ }, sensitiveKeys);
972
+ const entry = {
973
+ method: "GET",
974
+ // Will be passed from caller
975
+ host: url.host,
976
+ pathname: url.pathname,
977
+ query: sanitized.query,
978
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
979
+ ...sanitized.requestData !== void 0 ? { requestData: sanitized.requestData } : {},
980
+ ...operationId ? { operationId } : {},
981
+ ...mockName ? { mockName } : {}
982
+ };
983
+ const filename = this.generateFilename(entry);
984
+ const catalogFilePath = path4.join(this.catalogPath, `${filename}.json`);
985
+ const responseFilename = `response_${filename}.json`;
986
+ await this.fileDb.write(responseData, { filename: responseFilename });
987
+ entry.file = responseFilename;
988
+ await fs4.mkdir(path4.dirname(catalogFilePath), { recursive: true });
989
+ await fs4.writeFile(catalogFilePath, JSON.stringify(entry, null, 2), "utf-8");
990
+ return filename;
991
+ } catch (error) {
992
+ this.logger.error?.("Error storing mock:", error);
993
+ throw error;
994
+ }
995
+ }
996
+ /**
997
+ * Find mock response for a request
998
+ */
999
+ async findMock(criteria) {
1000
+ try {
1001
+ const exactMatch = await this.findExactMatch(criteria);
1002
+ if (exactMatch) {
1003
+ return exactMatch;
1004
+ }
1005
+ const fuzzyMatch = await this.findFuzzyMatch(criteria);
1006
+ if (fuzzyMatch) {
1007
+ return fuzzyMatch;
1008
+ }
1009
+ return null;
1010
+ } catch (error) {
1011
+ this.logger.error?.("Error finding mock:", error);
1012
+ return null;
1013
+ }
1014
+ }
1015
+ /**
1016
+ * Find exact match for request criteria
1017
+ */
1018
+ async findExactMatch(criteria) {
1019
+ try {
1020
+ const entries = await this.listEntries();
1021
+ for (const entry of entries) {
1022
+ if (this.matchesCriteria(entry, criteria)) {
1023
+ return await this.loadResponseData(entry.file);
1024
+ }
1025
+ }
1026
+ return null;
1027
+ } catch (error) {
1028
+ this.logger.error?.("Error in exact match:", error);
1029
+ return null;
1030
+ }
1031
+ }
1032
+ /**
1033
+ * Find fuzzy match for request criteria
1034
+ */
1035
+ async findFuzzyMatch(criteria) {
1036
+ try {
1037
+ const fuzzyCriteria = {
1038
+ ...criteria,
1039
+ query: this.stripCommonParams(criteria.query)
1040
+ };
1041
+ return this.findExactMatch(fuzzyCriteria);
1042
+ } catch (error) {
1043
+ this.logger.error?.("Error in fuzzy match:", error);
1044
+ return null;
1045
+ }
1046
+ }
1047
+ /**
1048
+ * Check if a catalog entry matches the request criteria
1049
+ */
1050
+ matchesCriteria(entry, criteria) {
1051
+ return entry.method === criteria.method && entry.host === criteria.host && entry.pathname === criteria.pathname && this.queriesMatch(entry.query, criteria.query) && this.requestDataMatches(entry.requestData, criteria.requestData) && (!criteria.operationId || entry.operationId === criteria.operationId);
1052
+ }
1053
+ /**
1054
+ * Check if query strings match (with sanitization)
1055
+ */
1056
+ queriesMatch(storedQuery, requestQuery) {
1057
+ return storedQuery === requestQuery;
1058
+ }
1059
+ /**
1060
+ * Check if request data matches
1061
+ */
1062
+ requestDataMatches(storedData, requestData) {
1063
+ if (!storedData && !requestData) {
1064
+ return true;
1065
+ }
1066
+ if (!storedData || !requestData) {
1067
+ return false;
1068
+ }
1069
+ return JSON.stringify(storedData) === JSON.stringify(requestData);
1070
+ }
1071
+ /**
1072
+ * Load response data from file
1073
+ */
1074
+ async loadResponseData(filename) {
1075
+ try {
1076
+ const data = await this.fileDb.read({ filename });
1077
+ return data;
1078
+ } catch (error) {
1079
+ this.logger.error?.("Error loading response data:", error);
1080
+ return null;
1081
+ }
1082
+ }
1083
+ /**
1084
+ * Strip common query parameters for fuzzy matching
1085
+ */
1086
+ stripCommonParams(query) {
1087
+ const commonParams = ["timestamp", "nonce", "cache", "_"];
1088
+ const params = new URLSearchParams(query);
1089
+ for (const param of commonParams) {
1090
+ params.delete(param);
1091
+ }
1092
+ return params.toString();
1093
+ }
1094
+ /**
1095
+ * Generate unique filename for catalog entry
1096
+ */
1097
+ generateFilename(entry) {
1098
+ const timestamp = Date.now();
1099
+ const hash = this.simpleHash(`${entry.method}${entry.host}${entry.pathname}${entry.query}${entry.operationId || ""}`);
1100
+ return `mock_${timestamp}_${hash}`;
1101
+ }
1102
+ /**
1103
+ * Simple hash function for filename generation
1104
+ */
1105
+ simpleHash(str) {
1106
+ let hash = 0;
1107
+ for (let i = 0; i < str.length; i++) {
1108
+ const char = str.charCodeAt(i);
1109
+ hash = (hash << 5) - hash + char;
1110
+ hash = hash & hash;
1111
+ }
1112
+ return Math.abs(hash).toString(36);
1113
+ }
1114
+ /**
1115
+ * List all catalog entries
1116
+ */
1117
+ async listEntries() {
1118
+ try {
1119
+ const entries = [];
1120
+ const files = await fs4.readdir(this.catalogPath).catch(() => []);
1121
+ const jsonFiles = files.filter((file) => file.endsWith(".json") && !file.startsWith("response_"));
1122
+ for (const file of jsonFiles) {
1123
+ try {
1124
+ const filePath = path4.join(this.catalogPath, file);
1125
+ const content = await fs4.readFile(filePath, "utf-8");
1126
+ const entry = JSON.parse(content);
1127
+ entries.push(entry);
1128
+ } catch (error) {
1129
+ this.logger.warn?.(`Error reading catalog entry ${file}:`, error);
1130
+ }
1131
+ }
1132
+ return entries;
1133
+ } catch (error) {
1134
+ this.logger.error?.("Error listing catalog entries:", error);
1135
+ return [];
1136
+ }
1137
+ }
1138
+ /**
1139
+ * Remove a catalog entry and its response data
1140
+ */
1141
+ async removeEntry(filename) {
1142
+ try {
1143
+ const catalogFilePath = path4.join(this.catalogPath, `${filename}.json`);
1144
+ const responseFilename = `response_${filename}.json`;
1145
+ try {
1146
+ await fs4.unlink(catalogFilePath);
1147
+ } catch (error) {
1148
+ this.logger.warn?.(`Could not remove catalog file ${catalogFilePath}:`, error);
1149
+ }
1150
+ try {
1151
+ const responseFilePath = path4.join(this.catalogPath, responseFilename);
1152
+ await fs4.unlink(responseFilePath);
1153
+ } catch (error) {
1154
+ this.logger.warn?.(`Could not remove response file ${responseFilename}:`, error);
1155
+ }
1156
+ return true;
1157
+ } catch (error) {
1158
+ this.logger.error?.("Error removing entry:", error);
1159
+ return false;
1160
+ }
1161
+ }
1162
+ /**
1163
+ * Clean up orphaned files and invalid catalog entries
1164
+ */
1165
+ async maintenance() {
1166
+ try {
1167
+ let cleaned = 0;
1168
+ const files = await fs4.readdir(this.catalogPath).catch(() => []);
1169
+ const catalogFiles = files.filter((file) => file.endsWith(".json") && !file.startsWith("response_"));
1170
+ const responseFiles = files.filter((file) => file.startsWith("response_"));
1171
+ for (const catalogFile of catalogFiles) {
1172
+ try {
1173
+ const catalogPath = path4.join(this.catalogPath, catalogFile);
1174
+ const content = await fs4.readFile(catalogPath, "utf-8");
1175
+ const entry = JSON.parse(content);
1176
+ const responseFile = entry.file;
1177
+ const responsePath = path4.join(this.catalogPath, responseFile);
1178
+ try {
1179
+ await fs4.access(responsePath);
1180
+ } catch {
1181
+ await fs4.unlink(catalogPath);
1182
+ cleaned++;
1183
+ this.logger.info?.(`Removed orphaned catalog entry: ${catalogFile}`);
1184
+ }
1185
+ } catch (error) {
1186
+ this.logger.warn?.(`Error processing catalog file ${catalogFile}:`, error);
1187
+ }
1188
+ }
1189
+ const catalogResponseFiles = catalogFiles.map(
1190
+ (file) => `response_${file.replace(".json", "")}.json`
1191
+ );
1192
+ for (const responseFile of responseFiles) {
1193
+ if (!catalogResponseFiles.includes(responseFile)) {
1194
+ const responsePath = path4.join(this.catalogPath, responseFile);
1195
+ await fs4.unlink(responsePath);
1196
+ cleaned++;
1197
+ this.logger.info?.(`Removed orphaned response file: ${responseFile}`);
1198
+ }
1199
+ }
1200
+ return { cleaned };
1201
+ } catch (error) {
1202
+ this.logger.error?.("Error during maintenance:", error);
1203
+ return { cleaned: 0 };
1204
+ }
1205
+ }
1206
+ };
1207
+
1208
+ // src/mock-server/index.ts
1209
+ var DEFAULT_SENSITIVE_KEYS = [
1210
+ "client_id",
1211
+ "client_secret",
1212
+ "access_token",
1213
+ "authorization",
1214
+ "bearer",
1215
+ "api_key",
1216
+ "apikey",
1217
+ "password",
1218
+ "token"
1219
+ ];
1220
+ var MockServer = class {
1221
+ config;
1222
+ app;
1223
+ server = null;
1224
+ fileDb;
1225
+ catalog;
1226
+ stats;
1227
+ startTime;
1228
+ constructor(config) {
1229
+ this.config = {
1230
+ port: 5029,
1231
+ namespace: "mocks",
1232
+ tableName: "responses",
1233
+ sensitiveKeys: DEFAULT_SENSITIVE_KEYS,
1234
+ logger: console,
1235
+ debug: false,
1236
+ middleware: [],
1237
+ ...config
1238
+ };
1239
+ if (!this.config.basePath) {
1240
+ throw new Error("MockServer: basePath is required");
1241
+ }
1242
+ this.app = (0, import_express.default)();
1243
+ this.startTime = Date.now();
1244
+ this.stats = {
1245
+ totalRequests: 0,
1246
+ mockResponses: 0,
1247
+ fallbackResponses: 0,
1248
+ errors: 0,
1249
+ uptime: 0
1250
+ };
1251
+ this.fileDb = this.config.fileDb || new FileDatabase({
1252
+ basePath: this.config.basePath,
1253
+ namespace: this.config.namespace,
1254
+ tableName: this.config.tableName,
1255
+ versioned: false,
1256
+ // Mock responses are typically not versioned
1257
+ logger: this.config.logger
1258
+ });
1259
+ this.catalog = new MockCatalog(this.fileDb, this.config.basePath, this.config.logger);
1260
+ this.setupMiddleware();
1261
+ this.setupRoutes();
1262
+ }
1263
+ /**
1264
+ * Start the mock server
1265
+ */
1266
+ async start() {
1267
+ return new Promise((resolve, reject) => {
1268
+ try {
1269
+ this.server = import_http.default.createServer(this.app);
1270
+ this.server.listen(this.config.port, () => {
1271
+ this.config.logger.info?.(`MockServer listening on port ${this.config.port}`);
1272
+ resolve({
1273
+ server: this.server,
1274
+ port: this.config.port,
1275
+ close: () => this.stop(),
1276
+ getStats: () => this.getCurrentStats()
1277
+ });
1278
+ });
1279
+ this.server.on("error", (error) => {
1280
+ reject(error);
1281
+ });
1282
+ } catch (error) {
1283
+ reject(error);
1284
+ }
1285
+ });
1286
+ }
1287
+ /**
1288
+ * Stop the mock server
1289
+ */
1290
+ async stop() {
1291
+ return new Promise((resolve) => {
1292
+ if (this.server) {
1293
+ this.server.close(() => {
1294
+ this.config.logger.info?.("MockServer stopped");
1295
+ resolve();
1296
+ });
1297
+ } else {
1298
+ resolve();
1299
+ }
1300
+ });
1301
+ }
1302
+ /**
1303
+ * Get current server statistics (internal)
1304
+ */
1305
+ getCurrentStats() {
1306
+ return {
1307
+ ...this.stats,
1308
+ uptime: Date.now() - this.startTime
1309
+ };
1310
+ }
1311
+ /**
1312
+ * Setup Express middleware
1313
+ */
1314
+ setupMiddleware() {
1315
+ this.app.use(import_express.default.urlencoded({ extended: true }));
1316
+ this.app.use(import_express.default.json());
1317
+ import_morgan.default.token("urlDecoded", (req) => {
1318
+ return decodeURIComponent(req.url || "");
1319
+ });
1320
+ this.app.use((0, import_morgan.default)(":method :urlDecoded :status :response-time", {
1321
+ skip: (req, res) => req.url === "/version" || res.statusCode === 429,
1322
+ stream: {
1323
+ write: (message) => {
1324
+ if (this.config.debug) {
1325
+ this.config.logger.debug?.(message.trim());
1326
+ }
1327
+ }
1328
+ }
1329
+ }));
1330
+ for (const middleware of this.config.middleware) {
1331
+ this.app.use(middleware);
1332
+ }
1333
+ }
1334
+ /**
1335
+ * Setup API routes
1336
+ */
1337
+ setupRoutes() {
1338
+ this.app.get("/version", (req, res) => {
1339
+ res.json({ version: "1.0.0", server: "MockServer" });
1340
+ });
1341
+ this.app.get("/404", (req, res) => {
1342
+ res.status(404).json({ code: "not_found", message: "not found" });
1343
+ });
1344
+ this.app.get("/test", async (req, res) => {
1345
+ const delay = req.get("x-axios-delay");
1346
+ const err = req.get("x-axios-err");
1347
+ if (delay) {
1348
+ await new Promise((resolve) => setTimeout(resolve, parseInt(delay)));
1349
+ }
1350
+ if (err) {
1351
+ if (!isNaN(Number(err))) {
1352
+ return res.status(Number(err)).json({
1353
+ code: "error",
1354
+ message: `expected error ${err}`
1355
+ });
1356
+ }
1357
+ if (err === "econnreset") {
1358
+ const connection = req.connection || req.socket;
1359
+ connection?.destroy();
1360
+ return;
1361
+ }
1362
+ }
1363
+ const message = delay ? `success with delay ${delay}` : "success";
1364
+ res.status(200).json({ code: "success", message });
1365
+ });
1366
+ this.app.all("/*", async (req, res) => {
1367
+ this.stats.totalRequests++;
1368
+ try {
1369
+ const mockResponse = await this.findMockResponse(req);
1370
+ if (mockResponse) {
1371
+ this.stats.mockResponses++;
1372
+ const safeHeaders = { ...mockResponse.headers };
1373
+ delete safeHeaders["transfer-encoding"];
1374
+ delete safeHeaders["Transfer-Encoding"];
1375
+ res.set(safeHeaders);
1376
+ res.status(mockResponse.status).send(mockResponse.data);
1377
+ } else {
1378
+ this.stats.fallbackResponses++;
1379
+ res.status(200).json({ code: "ok", message: "catch all" });
1380
+ }
1381
+ } catch (error) {
1382
+ this.stats.errors++;
1383
+ this.config.logger.error?.("MockServer error:", error);
1384
+ res.status(500).json({ code: "server_error", message: "Internal server error" });
1385
+ }
1386
+ });
1387
+ }
1388
+ /**
1389
+ * Find mock response for a request
1390
+ */
1391
+ async findMockResponse(req) {
1392
+ const headerValue = req.get("XAXIOSOrigin");
1393
+ if (!headerValue) {
1394
+ return null;
1395
+ }
1396
+ try {
1397
+ const url = new URL(headerValue);
1398
+ const method = req.method;
1399
+ const pathname = url.pathname;
1400
+ const query = url.search.slice(1);
1401
+ const requestData = this.extractRequestData(req);
1402
+ const criteria = {
1403
+ method,
1404
+ host: url.host,
1405
+ pathname,
1406
+ query,
1407
+ requestData
1408
+ };
1409
+ return await this.catalog.findMock(criteria);
1410
+ } catch (error) {
1411
+ this.config.logger.error?.("Error finding mock response:", error);
1412
+ return null;
1413
+ }
1414
+ }
1415
+ /**
1416
+ * Extract request data from Express request
1417
+ */
1418
+ extractRequestData(req) {
1419
+ const contentType = req.get("content-type");
1420
+ if (contentType?.includes("application/x-www-form-urlencoded")) {
1421
+ return req.body;
1422
+ } else if (contentType?.includes("application/json")) {
1423
+ return req.body;
1424
+ }
1425
+ return req.body;
1426
+ }
1427
+ /**
1428
+ * Store a mock response from an HTTP request/response
1429
+ */
1430
+ async storeMock(requestUrl, requestData, responseData, operationId, mockName) {
1431
+ return await this.catalog.storeMock(
1432
+ requestUrl,
1433
+ requestData,
1434
+ responseData,
1435
+ operationId,
1436
+ mockName,
1437
+ this.config.sensitiveKeys
1438
+ );
1439
+ }
1440
+ /**
1441
+ * List all stored mock responses
1442
+ */
1443
+ async listMocks() {
1444
+ return await this.catalog.listEntries();
1445
+ }
1446
+ /**
1447
+ * Remove a mock response by filename
1448
+ */
1449
+ async removeMock(filename) {
1450
+ return await this.catalog.removeEntry(filename);
1451
+ }
1452
+ /**
1453
+ * Run maintenance to clean up orphaned files
1454
+ */
1455
+ async maintenance() {
1456
+ return await this.catalog.maintenance();
1457
+ }
1458
+ /**
1459
+ * Get current configuration
1460
+ */
1461
+ getConfig() {
1462
+ return { ...this.config };
1463
+ }
1464
+ /**
1465
+ * Get server statistics
1466
+ */
1467
+ getStats() {
1468
+ return {
1469
+ ...this.stats,
1470
+ uptime: Date.now() - this.startTime
1471
+ };
1472
+ }
1473
+ };
1474
+ async function createMockServer(config) {
1475
+ const server = new MockServer(config);
1476
+ return server.start();
1477
+ }
1478
+ // Annotate the CommonJS export names for ESM import in node:
1479
+ 0 && (module.exports = {
1480
+ MockServer,
1481
+ createMockServer
1482
+ });
1483
+ //# sourceMappingURL=mock-server.cjs.map