@nmakarov/cli-toolkit 0.16.0 → 0.18.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.
@@ -3,6 +3,9 @@ import express from "express";
3
3
  import http from "http";
4
4
  import morgan from "morgan";
5
5
 
6
+ // src/mock-server/mock-storage.ts
7
+ import { createHash } from "crypto";
8
+
6
9
  // src/filedatabase/index.ts
7
10
  import fs3 from "fs";
8
11
  import path3 from "path";
@@ -262,7 +265,7 @@ var FileDatabase = class _FileDatabase {
262
265
  const versions = await this.getVersions();
263
266
  while (versions.length > this.maxVersions) {
264
267
  const versionToDelete = path3.resolve(this.getDestinationPath(), versions.shift());
265
- this.logger.debug?.(`[FileDatabase] Deleting old version: ${versionToDelete}`);
268
+ this.logger.silly?.(`[FileDatabase] Deleting old version: ${versionToDelete}`);
266
269
  await fs3.promises.rm(versionToDelete, { recursive: true, force: true });
267
270
  }
268
271
  return versionName;
@@ -540,7 +543,7 @@ var FileDatabase = class _FileDatabase {
540
543
  };
541
544
  this.metadata.files.push(fileEntry);
542
545
  this.lastFileData = null;
543
- this.logger.debug?.(`[FileDatabase] Created new file: ${fileEntry.fileName}, fileNumber: ${this.currentFileNumber}`);
546
+ this.logger.silly?.(`[FileDatabase] Created new file: ${fileEntry.fileName}, fileNumber: ${this.currentFileNumber}`);
544
547
  }
545
548
  /**
546
549
  * Figure out what data to write and which file to use (for pagination)
@@ -573,7 +576,7 @@ var FileDatabase = class _FileDatabase {
573
576
  const filesBeforeCreate = this.metadata.files.length;
574
577
  this.makeNewFile();
575
578
  newlyCreatedFileIndex = filesBeforeCreate;
576
- this.logger.debug?.(`[FileDatabase] Creating new file for unique custom metadata combination, fileNumber: ${this.currentFileNumber}`);
579
+ this.logger.silly?.(`[FileDatabase] Creating new file for unique custom metadata combination, fileNumber: ${this.currentFileNumber}`);
577
580
  } else if (this.metadata.files.length === 0) {
578
581
  this.makeNewFile();
579
582
  }
@@ -622,7 +625,7 @@ var FileDatabase = class _FileDatabase {
622
625
  dataLeftOver = null;
623
626
  }
624
627
  const fileName = this.metadata.files[this.metadata.files.length - 1].fileName;
625
- this.logger.debug?.(
628
+ this.logger.silly?.(
626
629
  `[FileDatabase] figureOutDataAndFileToWrite: filename=${fileName}, forceNewFile=${forceNewFile}, targetFileIndex=${targetFileIndex}, dataToWrite.length=${Array.isArray(dataToWrite) ? dataToWrite.length : "N/A"}, lastFileRecordsCount=${lastFileRecordsCount}`
627
630
  );
628
631
  return { dataToWrite, dataLeftOver, fileName };
@@ -677,7 +680,7 @@ var FileDatabase = class _FileDatabase {
677
680
  this.metadata.modifiedAt = (/* @__PURE__ */ new Date()).toISOString();
678
681
  this.metadata.dataType = detectDataType(dataToWrite);
679
682
  this.metadata.totalRecords = this.metadata.files.reduce((sum, file) => sum + (file.recordsCount || 0), 0);
680
- this.logger.debug?.(
683
+ this.logger.silly?.(
681
684
  `[FileDatabase] Updated metadata for file ${currentFile.fileName}: recordsCount=${recordsCount}, totalRecords=${this.metadata.totalRecords}`
682
685
  );
683
686
  }
@@ -701,7 +704,7 @@ var FileDatabase = class _FileDatabase {
701
704
  }
702
705
  try {
703
706
  await fs3.promises.writeFile(filePath, serializedData, "utf8");
704
- this.logger.debug?.(`[FileDatabase] Wrote ${bytesToHumanReadable(requiredBytes)} to ${filePath}`);
707
+ this.logger.silly?.(`[FileDatabase] Wrote ${bytesToHumanReadable(requiredBytes)} to ${filePath}`);
705
708
  } catch (error) {
706
709
  throw new FileDatabaseError(`Failed to write file ${filePath}: ${error.message}`);
707
710
  }
@@ -783,7 +786,8 @@ var FileDatabase = class _FileDatabase {
783
786
  this.useMetadata = format.hasMetadata;
784
787
  }
785
788
  if (this.useMetadata) {
786
- const metadataPath = path3.join(this.getDestinationPath(), "metadata.json");
789
+ const destPath = this.getDestinationPath();
790
+ const metadataPath = path3.join(destPath, "metadata.json");
787
791
  if (fs3.existsSync(metadataPath)) {
788
792
  try {
789
793
  const rawData = await fs3.promises.readFile(metadataPath, "utf8");
@@ -797,7 +801,9 @@ var FileDatabase = class _FileDatabase {
797
801
  throw new FileDatabaseError(`Failed to read metadata: ${e.message}`);
798
802
  }
799
803
  } else {
800
- throw new FileDatabaseError("[FileDatabase] No metadata found in non-versioned mode");
804
+ throw new FileDatabaseError(
805
+ `[FileDatabase] No metadata found in non-versioned mode. Looked for: ${metadataPath} (table path: ${destPath})`
806
+ );
801
807
  }
802
808
  } else {
803
809
  this.metadata = await this.figureMetadataFromVersionFiles("");
@@ -814,6 +820,13 @@ var FileDatabase = class _FileDatabase {
814
820
  * Write data to the file database
815
821
  */
816
822
  async write(data, options = {}) {
823
+ if (options.filename) {
824
+ const destPath2 = this.getDestinationPath();
825
+ await ensurePath(destPath2);
826
+ const filePath = path3.join(destPath2, options.filename);
827
+ await this.safeWrite(filePath, data);
828
+ return;
829
+ }
817
830
  if (options.forceNewVersion && !this.versioned) {
818
831
  throw new FileDatabaseError("Cannot use forceNewVersion in non-versioned mode");
819
832
  }
@@ -837,17 +850,17 @@ var FileDatabase = class _FileDatabase {
837
850
  });
838
851
  if (matches) {
839
852
  targetFileIndex = i;
840
- this.logger.debug?.(`[FileDatabase] Found existing file with matching custom metadata: ${fileEntry.fileName}, metadata: ${JSON.stringify(options.customMetadata)}`);
853
+ this.logger.silly?.(`[FileDatabase] Found existing file with matching custom metadata: ${fileEntry.fileName}, metadata: ${JSON.stringify(options.customMetadata)}`);
841
854
  break;
842
855
  } else {
843
- this.logger.debug?.(`[FileDatabase] File ${fileEntry.fileName} does not match custom metadata: ${JSON.stringify(options.customMetadata)}`);
856
+ this.logger.silly?.(`[FileDatabase] File ${fileEntry.fileName} does not match custom metadata: ${JSON.stringify(options.customMetadata)}`);
844
857
  }
845
858
  }
846
859
  if (targetFileIndex === null) {
847
- this.logger.debug?.(`[FileDatabase] No existing file found with custom metadata: ${JSON.stringify(options.customMetadata)}, will create new file`);
860
+ this.logger.silly?.(`[FileDatabase] No existing file found with custom metadata: ${JSON.stringify(options.customMetadata)}, will create new file`);
848
861
  }
849
862
  } else {
850
- this.logger.debug?.(`[FileDatabase] No custom metadata provided, will create new file`);
863
+ this.logger.silly?.(`[FileDatabase] No custom metadata provided, will create new file`);
851
864
  }
852
865
  if (targetFileIndex !== null) {
853
866
  const targetFile = this.metadata.files[targetFileIndex];
@@ -876,7 +889,17 @@ var FileDatabase = class _FileDatabase {
876
889
  * Read data from the file database
877
890
  */
878
891
  async read(options = {}) {
879
- const { version, nextPage = false, pageSize } = options;
892
+ const { version, nextPage = false, pageSize, filename } = options;
893
+ if (filename) {
894
+ const destPath = this.getDestinationPath(version);
895
+ const filePath = path3.join(destPath, filename);
896
+ try {
897
+ const rawData = await fs3.promises.readFile(filePath, "utf8");
898
+ return JSON.parse(rawData);
899
+ } catch (error) {
900
+ throw new FileDatabaseError(`Failed to read file ${filename}: ${error.message}`);
901
+ }
902
+ }
880
903
  await this.prepare({ read: true, version });
881
904
  const isNonPaginatedData = this.metadata.dataType === "text" || this.metadata.dataType === "xml" || this.metadata.dataType === "json-object";
882
905
  if (isNonPaginatedData) {
@@ -957,6 +980,67 @@ var FileDatabase = class _FileDatabase {
957
980
  this.currentRecord = 0;
958
981
  this.hasReadFirstPage = false;
959
982
  }
983
+ /**
984
+ * List filenames in the table directory.
985
+ * For catalog/key-value usage (files written with { filename }).
986
+ * Returns data file names (.json, .txt, .xml) excluding metadata.json.
987
+ */
988
+ async listFilenames() {
989
+ const destPath = this.versioned && this.currentVersion ? path3.join(this.getDestinationPath(), this.currentVersion) : this.getDestinationPath();
990
+ try {
991
+ const entries = await fs3.promises.readdir(destPath, { withFileTypes: true });
992
+ return entries.filter((e) => e.isFile() && e.name !== "metadata.json" && /\.(json|txt|xml)$/i.test(e.name)).map((e) => e.name);
993
+ } catch (err) {
994
+ if (err?.code === "ENOENT") return [];
995
+ throw new FileDatabaseError(`Failed to list files: ${err.message}`);
996
+ }
997
+ }
998
+ /**
999
+ * Remove a file from the table directory (catalog mode).
1000
+ * Use with listFilenames() to manage individual files.
1001
+ */
1002
+ async removeFile(filename) {
1003
+ const destPath = this.versioned && this.currentVersion ? path3.join(this.getDestinationPath(), this.currentVersion) : this.getDestinationPath();
1004
+ const filePath = path3.join(destPath, filename);
1005
+ try {
1006
+ await fs3.promises.unlink(filePath);
1007
+ } catch (err) {
1008
+ if (err?.code === "ENOENT") return;
1009
+ throw new FileDatabaseError(`Failed to remove file ${filename}: ${err.message}`);
1010
+ }
1011
+ }
1012
+ /**
1013
+ * Remove a file and its metadata entry (non-versioned mode with useMetadata).
1014
+ * Use with findData() to get fileName, then call removeFileEntry to delete.
1015
+ */
1016
+ async removeFileEntry(filename) {
1017
+ if (this.versioned) {
1018
+ throw new FileDatabaseError("removeFileEntry is only supported in non-versioned mode");
1019
+ }
1020
+ await this.prepare({ read: true });
1021
+ const idx = this.metadata.files.findIndex((f) => f.fileName === filename);
1022
+ if (idx === -1) {
1023
+ throw new FileDatabaseError(`File entry ${filename} not found in metadata`);
1024
+ }
1025
+ const entry = this.metadata.files[idx];
1026
+ const recordsCount = entry.recordsCount || 0;
1027
+ this.metadata.files.splice(idx, 1);
1028
+ this.metadata.totalRecords = Math.max(0, (this.metadata.totalRecords || 0) - recordsCount);
1029
+ const destPath = this.getDestinationPath();
1030
+ const filePath = path3.join(destPath, filename);
1031
+ try {
1032
+ await fs3.promises.unlink(filePath);
1033
+ } catch (err) {
1034
+ if (err?.code === "ENOENT") {
1035
+ this.logger.warn?.(`[FileDatabase] File ${filename} already missing on disk`);
1036
+ } else {
1037
+ throw new FileDatabaseError(`Failed to remove file ${filename}: ${err.message}`);
1038
+ }
1039
+ }
1040
+ if (this.useMetadata) {
1041
+ await this.saveVersionMetadata(this.metadata);
1042
+ }
1043
+ }
960
1044
  /**
961
1045
  * Set file-level synopsis calculation function
962
1046
  */
@@ -1040,335 +1124,106 @@ var FileDatabase = class _FileDatabase {
1040
1124
  }
1041
1125
  };
1042
1126
 
1043
- // src/mock-server/catalog.ts
1044
- import * as fs4 from "fs/promises";
1045
- import * as path4 from "path";
1046
-
1047
- // src/mock-server/sanitization.ts
1048
- import { createHash } from "crypto";
1049
- import { parse, stringify } from "querystring";
1050
- function maskValue2(value) {
1051
- return `[md5:${createHash("md5").update(value).digest("hex")}]`;
1052
- }
1053
- function sanitizeUrlEncodedString(input, keysToSanitize) {
1054
- const parsed = parse(input);
1055
- const sanitized = sanitizeObject(parsed, keysToSanitize);
1056
- return stringify(sanitized);
1057
- }
1058
- function sanitizeObject(obj, keysToSanitize) {
1059
- if (Array.isArray(obj)) {
1060
- return obj;
1061
- }
1062
- const result = {};
1063
- for (const [key, value] of Object.entries(obj || {})) {
1064
- if (keysToSanitize.includes(key.toLowerCase())) {
1065
- result[key] = maskValue2(String(value));
1066
- } else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
1067
- result[key] = sanitizeObject(value, keysToSanitize);
1068
- } else {
1069
- result[key] = value;
1070
- }
1071
- }
1072
- return result;
1073
- }
1074
- function sanitizeHeaders(headers, additionalKeys = []) {
1075
- const sensitiveKeys = ["authorization", "x-api-key", "api-key", "bearer", ...additionalKeys.map((k) => k.toLowerCase())];
1076
- const sanitized = { ...headers };
1077
- for (const [key, value] of Object.entries(sanitized)) {
1078
- if (sensitiveKeys.includes(key.toLowerCase())) {
1079
- sanitized[key] = maskValue2(value);
1080
- }
1081
- }
1082
- return sanitized;
1127
+ // src/mock-server/mock-storage.ts
1128
+ function stableStringify(obj) {
1129
+ if (obj === null) return "null";
1130
+ if (obj === void 0) return "undefined";
1131
+ if (typeof obj !== "object") return JSON.stringify(obj);
1132
+ if (Array.isArray(obj)) return "[" + obj.map(stableStringify).join(",") + "]";
1133
+ const keys = Object.keys(obj).sort();
1134
+ return "{" + keys.map((k) => JSON.stringify(k) + ":" + stableStringify(obj[k])).join(",") + "}";
1083
1135
  }
1084
- function sanitizeRequestData(data, keysToSanitize) {
1085
- if (typeof data === "string") {
1086
- if (data.includes("=")) {
1087
- try {
1088
- const parsed = parse(data);
1089
- return stringify(sanitizeObject(parsed, keysToSanitize));
1090
- } catch {
1091
- return data;
1092
- }
1093
- }
1094
- return data;
1095
- } else if (typeof data === "object" && data !== null) {
1096
- return sanitizeObject(data, keysToSanitize);
1097
- }
1098
- return data;
1136
+ function normalizeQuery(query) {
1137
+ if (!query) return "";
1138
+ const params = new URLSearchParams(query);
1139
+ const sorted = [...params.entries()].sort((a, b) => a[0].localeCompare(b[0]));
1140
+ return new URLSearchParams(sorted).toString();
1099
1141
  }
1100
- function sanitizeResponseData(data, keysToSanitize) {
1101
- if (typeof data === "object" && data !== null) {
1102
- return sanitizeObject(data, keysToSanitize);
1103
- }
1104
- return data;
1105
- }
1106
- function sanitizeRequest(params, sensitiveKeys) {
1107
- const { query, requestData, headers = {}, data } = params;
1142
+ function buildCriteria(method, host, pathname, query, requestData) {
1108
1143
  return {
1109
- query: sanitizeUrlEncodedString(query, sensitiveKeys),
1110
- requestData: requestData ? sanitizeRequestData(requestData, sensitiveKeys) : void 0,
1111
- headers: sanitizeHeaders(headers, sensitiveKeys),
1112
- data: data ? sanitizeResponseData(data, sensitiveKeys) : data
1144
+ method: method.toUpperCase(),
1145
+ host,
1146
+ pathname: pathname || "/",
1147
+ query: normalizeQuery(query),
1148
+ requestBody: requestData != null ? stableStringify(requestData) : ""
1113
1149
  };
1114
1150
  }
1115
-
1116
- // src/mock-server/catalog.ts
1117
- var MockCatalog = class {
1151
+ function computeMockKey(method, host, pathname, query, requestData) {
1152
+ const criteria = buildCriteria(method, host, pathname, query, requestData);
1153
+ const str = JSON.stringify(criteria);
1154
+ return createHash("sha256").update(str).digest("hex").slice(0, 32);
1155
+ }
1156
+ var MockStorage = class {
1118
1157
  fileDb;
1119
- catalogPath;
1120
1158
  logger;
1121
- constructor(fileDb, catalogPath, logger = console) {
1122
- this.fileDb = fileDb;
1123
- this.catalogPath = catalogPath;
1124
- this.logger = logger;
1159
+ constructor(config) {
1160
+ this.logger = config.logger ?? console;
1161
+ this.fileDb = new FileDatabase({
1162
+ basePath: config.basePath,
1163
+ namespace: "mocks",
1164
+ tableName: "responses",
1165
+ versioned: false,
1166
+ useMetadata: true,
1167
+ logger: this.logger
1168
+ });
1125
1169
  }
1126
1170
  /**
1127
- * Store a mock response in the catalog
1171
+ * Store a mock response.
1128
1172
  */
1129
- async storeMock(requestUrl, requestData, responseData, operationId, mockName, sensitiveKeys = []) {
1130
- try {
1131
- const url = new URL(requestUrl);
1132
- const sanitized = sanitizeRequest({
1133
- query: url.search.slice(1),
1134
- requestData,
1135
- headers: {},
1136
- data: responseData.data
1137
- }, sensitiveKeys);
1138
- const entry = {
1139
- method: "GET",
1140
- // Will be passed from caller
1141
- host: url.host,
1142
- pathname: url.pathname,
1143
- query: sanitized.query,
1144
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1145
- ...sanitized.requestData !== void 0 ? { requestData: sanitized.requestData } : {},
1146
- ...operationId ? { operationId } : {},
1147
- ...mockName ? { mockName } : {}
1148
- };
1149
- const filename = this.generateFilename(entry);
1150
- const catalogFilePath = path4.join(this.catalogPath, `${filename}.json`);
1151
- const responseFilename = `response_${filename}.json`;
1152
- await this.fileDb.write(responseData, { filename: responseFilename });
1153
- entry.file = responseFilename;
1154
- await fs4.mkdir(path4.dirname(catalogFilePath), { recursive: true });
1155
- await fs4.writeFile(catalogFilePath, JSON.stringify(entry, null, 2), "utf-8");
1156
- return filename;
1157
- } catch (error) {
1158
- this.logger.error?.("Error storing mock:", error);
1159
- throw error;
1160
- }
1173
+ async store(method, requestUrl, requestData, responseData) {
1174
+ const url = new URL(requestUrl);
1175
+ const criteria = buildCriteria(method, url.host, url.pathname, url.search.slice(1), requestData);
1176
+ await this.fileDb.write(responseData, { customMetadata: criteria });
1161
1177
  }
1162
1178
  /**
1163
- * Find mock response for a request
1179
+ * Find a mock response by request criteria. Returns null if not found.
1164
1180
  */
1165
- async findMock(criteria) {
1181
+ async find(method, host, pathname, query, requestData) {
1182
+ const criteria = buildCriteria(method, host, pathname, query, requestData);
1166
1183
  try {
1167
- const exactMatch = await this.findExactMatch(criteria);
1168
- if (exactMatch) {
1169
- return exactMatch;
1170
- }
1171
- const fuzzyMatch = await this.findFuzzyMatch(criteria);
1172
- if (fuzzyMatch) {
1173
- return fuzzyMatch;
1184
+ const results = await this.fileDb.findData(criteria);
1185
+ if (results.length === 0) return null;
1186
+ return results[0].data;
1187
+ } catch (err) {
1188
+ if (err instanceof FileDatabaseError && /No metadata found/.test(err.message)) {
1189
+ return null;
1174
1190
  }
1175
- return null;
1176
- } catch (error) {
1177
- this.logger.error?.("Error finding mock:", error);
1178
- return null;
1191
+ throw err;
1179
1192
  }
1180
1193
  }
1181
1194
  /**
1182
- * Find exact match for request criteria
1195
+ * List all stored mock keys (opaque identifiers for remove)
1183
1196
  */
1184
- async findExactMatch(criteria) {
1185
- try {
1186
- const entries = await this.listEntries();
1187
- for (const entry of entries) {
1188
- if (this.matchesCriteria(entry, criteria)) {
1189
- return await this.loadResponseData(entry.file);
1190
- }
1191
- }
1192
- return null;
1193
- } catch (error) {
1194
- this.logger.error?.("Error in exact match:", error);
1195
- return null;
1196
- }
1197
+ async listKeys() {
1198
+ const files = await this.fileDb.listFilenames();
1199
+ return files.sort();
1197
1200
  }
1198
1201
  /**
1199
- * Find fuzzy match for request criteria
1202
+ * Remove a mock by key (from listKeys)
1200
1203
  */
1201
- async findFuzzyMatch(criteria) {
1204
+ async remove(fileName) {
1205
+ const name = fileName.endsWith(".json") ? fileName : `${fileName}.json`;
1202
1206
  try {
1203
- const fuzzyCriteria = {
1204
- ...criteria,
1205
- query: this.stripCommonParams(criteria.query)
1206
- };
1207
- return this.findExactMatch(fuzzyCriteria);
1208
- } catch (error) {
1209
- this.logger.error?.("Error in fuzzy match:", error);
1210
- return null;
1211
- }
1212
- }
1213
- /**
1214
- * Check if a catalog entry matches the request criteria
1215
- */
1216
- matchesCriteria(entry, criteria) {
1217
- 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);
1218
- }
1219
- /**
1220
- * Check if query strings match (with sanitization)
1221
- */
1222
- queriesMatch(storedQuery, requestQuery) {
1223
- return storedQuery === requestQuery;
1224
- }
1225
- /**
1226
- * Check if request data matches
1227
- */
1228
- requestDataMatches(storedData, requestData) {
1229
- if (!storedData && !requestData) {
1207
+ await this.fileDb.removeFileEntry(name);
1230
1208
  return true;
1231
- }
1232
- if (!storedData || !requestData) {
1209
+ } catch {
1233
1210
  return false;
1234
1211
  }
1235
- return JSON.stringify(storedData) === JSON.stringify(requestData);
1236
1212
  }
1237
1213
  /**
1238
- * Load response data from file
1214
+ * Remove a mock by request criteria (method, host, pathname, query, requestData)
1239
1215
  */
1240
- async loadResponseData(filename) {
1216
+ async removeByCriteria(method, host, pathname, query, requestData) {
1217
+ const criteria = buildCriteria(method, host, pathname, query, requestData);
1241
1218
  try {
1242
- const data = await this.fileDb.read({ filename });
1243
- return data;
1244
- } catch (error) {
1245
- this.logger.error?.("Error loading response data:", error);
1246
- return null;
1247
- }
1248
- }
1249
- /**
1250
- * Strip common query parameters for fuzzy matching
1251
- */
1252
- stripCommonParams(query) {
1253
- const commonParams = ["timestamp", "nonce", "cache", "_"];
1254
- const params = new URLSearchParams(query);
1255
- for (const param of commonParams) {
1256
- params.delete(param);
1257
- }
1258
- return params.toString();
1259
- }
1260
- /**
1261
- * Generate unique filename for catalog entry
1262
- */
1263
- generateFilename(entry) {
1264
- const timestamp = Date.now();
1265
- const hash = this.simpleHash(`${entry.method}${entry.host}${entry.pathname}${entry.query}${entry.operationId || ""}`);
1266
- return `mock_${timestamp}_${hash}`;
1267
- }
1268
- /**
1269
- * Simple hash function for filename generation
1270
- */
1271
- simpleHash(str) {
1272
- let hash = 0;
1273
- for (let i = 0; i < str.length; i++) {
1274
- const char = str.charCodeAt(i);
1275
- hash = (hash << 5) - hash + char;
1276
- hash = hash & hash;
1277
- }
1278
- return Math.abs(hash).toString(36);
1279
- }
1280
- /**
1281
- * List all catalog entries
1282
- */
1283
- async listEntries() {
1284
- try {
1285
- const entries = [];
1286
- const files = await fs4.readdir(this.catalogPath).catch(() => []);
1287
- const jsonFiles = files.filter((file) => file.endsWith(".json") && !file.startsWith("response_"));
1288
- for (const file of jsonFiles) {
1289
- try {
1290
- const filePath = path4.join(this.catalogPath, file);
1291
- const content = await fs4.readFile(filePath, "utf-8");
1292
- const entry = JSON.parse(content);
1293
- entries.push(entry);
1294
- } catch (error) {
1295
- this.logger.warn?.(`Error reading catalog entry ${file}:`, error);
1296
- }
1297
- }
1298
- return entries;
1299
- } catch (error) {
1300
- this.logger.error?.("Error listing catalog entries:", error);
1301
- return [];
1302
- }
1303
- }
1304
- /**
1305
- * Remove a catalog entry and its response data
1306
- */
1307
- async removeEntry(filename) {
1308
- try {
1309
- const catalogFilePath = path4.join(this.catalogPath, `${filename}.json`);
1310
- const responseFilename = `response_${filename}.json`;
1311
- try {
1312
- await fs4.unlink(catalogFilePath);
1313
- } catch (error) {
1314
- this.logger.warn?.(`Could not remove catalog file ${catalogFilePath}:`, error);
1315
- }
1316
- try {
1317
- const responseFilePath = path4.join(this.catalogPath, responseFilename);
1318
- await fs4.unlink(responseFilePath);
1319
- } catch (error) {
1320
- this.logger.warn?.(`Could not remove response file ${responseFilename}:`, error);
1321
- }
1219
+ const results = await this.fileDb.findData(criteria);
1220
+ if (results.length === 0) return false;
1221
+ await this.fileDb.removeFileEntry(results[0].fileName);
1322
1222
  return true;
1323
- } catch (error) {
1324
- this.logger.error?.("Error removing entry:", error);
1223
+ } catch {
1325
1224
  return false;
1326
1225
  }
1327
1226
  }
1328
- /**
1329
- * Clean up orphaned files and invalid catalog entries
1330
- */
1331
- async maintenance() {
1332
- try {
1333
- let cleaned = 0;
1334
- const files = await fs4.readdir(this.catalogPath).catch(() => []);
1335
- const catalogFiles = files.filter((file) => file.endsWith(".json") && !file.startsWith("response_"));
1336
- const responseFiles = files.filter((file) => file.startsWith("response_"));
1337
- for (const catalogFile of catalogFiles) {
1338
- try {
1339
- const catalogPath = path4.join(this.catalogPath, catalogFile);
1340
- const content = await fs4.readFile(catalogPath, "utf-8");
1341
- const entry = JSON.parse(content);
1342
- const responseFile = entry.file;
1343
- const responsePath = path4.join(this.catalogPath, responseFile);
1344
- try {
1345
- await fs4.access(responsePath);
1346
- } catch {
1347
- await fs4.unlink(catalogPath);
1348
- cleaned++;
1349
- this.logger.info?.(`Removed orphaned catalog entry: ${catalogFile}`);
1350
- }
1351
- } catch (error) {
1352
- this.logger.warn?.(`Error processing catalog file ${catalogFile}:`, error);
1353
- }
1354
- }
1355
- const catalogResponseFiles = catalogFiles.map(
1356
- (file) => `response_${file.replace(".json", "")}.json`
1357
- );
1358
- for (const responseFile of responseFiles) {
1359
- if (!catalogResponseFiles.includes(responseFile)) {
1360
- const responsePath = path4.join(this.catalogPath, responseFile);
1361
- await fs4.unlink(responsePath);
1362
- cleaned++;
1363
- this.logger.info?.(`Removed orphaned response file: ${responseFile}`);
1364
- }
1365
- }
1366
- return { cleaned };
1367
- } catch (error) {
1368
- this.logger.error?.("Error during maintenance:", error);
1369
- return { cleaned: 0 };
1370
- }
1371
- }
1372
1227
  };
1373
1228
 
1374
1229
  // src/mock-server/index.ts
@@ -1387,8 +1242,7 @@ var MockServer = class {
1387
1242
  config;
1388
1243
  app;
1389
1244
  server = null;
1390
- fileDb;
1391
- catalog;
1245
+ storage;
1392
1246
  stats;
1393
1247
  startTime;
1394
1248
  constructor(config) {
@@ -1414,15 +1268,7 @@ var MockServer = class {
1414
1268
  errors: 0,
1415
1269
  uptime: 0
1416
1270
  };
1417
- this.fileDb = this.config.fileDb || new FileDatabase({
1418
- basePath: this.config.basePath,
1419
- namespace: this.config.namespace,
1420
- tableName: this.config.tableName,
1421
- versioned: false,
1422
- // Mock responses are typically not versioned
1423
- logger: this.config.logger
1424
- });
1425
- this.catalog = new MockCatalog(this.fileDb, this.config.basePath, this.config.logger);
1271
+ this.storage = new MockStorage({ basePath: this.config.basePath, logger: this.config.logger });
1426
1272
  this.setupMiddleware();
1427
1273
  this.setupRoutes();
1428
1274
  }
@@ -1565,14 +1411,7 @@ var MockServer = class {
1565
1411
  const pathname = url.pathname;
1566
1412
  const query = url.search.slice(1);
1567
1413
  const requestData = this.extractRequestData(req);
1568
- const criteria = {
1569
- method,
1570
- host: url.host,
1571
- pathname,
1572
- query,
1573
- requestData
1574
- };
1575
- return await this.catalog.findMock(criteria);
1414
+ return await this.storage.find(method, url.host, pathname, query, requestData);
1576
1415
  } catch (error) {
1577
1416
  this.config.logger.error?.("Error finding mock response:", error);
1578
1417
  return null;
@@ -1593,33 +1432,39 @@ var MockServer = class {
1593
1432
  /**
1594
1433
  * Store a mock response from an HTTP request/response
1595
1434
  */
1596
- async storeMock(requestUrl, requestData, responseData, operationId, mockName) {
1597
- return await this.catalog.storeMock(
1598
- requestUrl,
1599
- requestData,
1600
- responseData,
1601
- operationId,
1602
- mockName,
1603
- this.config.sensitiveKeys
1604
- );
1435
+ async storeMock(requestUrl, requestData, responseData, method = "GET") {
1436
+ await this.storage.store(method, requestUrl, requestData, responseData);
1605
1437
  }
1606
1438
  /**
1607
- * List all stored mock responses
1439
+ * List all stored mock keys
1608
1440
  */
1609
1441
  async listMocks() {
1610
- return await this.catalog.listEntries();
1442
+ return await this.storage.listKeys();
1611
1443
  }
1612
1444
  /**
1613
- * Remove a mock response by filename
1445
+ * Remove a mock by key (from listMocks)
1614
1446
  */
1615
- async removeMock(filename) {
1616
- return await this.catalog.removeEntry(filename);
1447
+ async removeMock(key) {
1448
+ return await this.storage.remove(key);
1449
+ }
1450
+ /**
1451
+ * Remove a mock by request criteria (method, requestUrl, requestData)
1452
+ */
1453
+ async removeMockByCriteria(requestUrl, requestData, method = "GET") {
1454
+ const url = new URL(requestUrl);
1455
+ return await this.storage.removeByCriteria(
1456
+ method,
1457
+ url.host,
1458
+ url.pathname,
1459
+ url.search.slice(1),
1460
+ requestData
1461
+ );
1617
1462
  }
1618
1463
  /**
1619
- * Run maintenance to clean up orphaned files
1464
+ * No-op (mocks are self-contained, no orphans)
1620
1465
  */
1621
1466
  async maintenance() {
1622
- return await this.catalog.maintenance();
1467
+ return { cleaned: 0 };
1623
1468
  }
1624
1469
  /**
1625
1470
  * Get current configuration
@@ -1643,6 +1488,8 @@ async function createMockServer(config) {
1643
1488
  }
1644
1489
  export {
1645
1490
  MockServer,
1491
+ MockStorage,
1492
+ computeMockKey,
1646
1493
  createMockServer
1647
1494
  };
1648
1495
  //# sourceMappingURL=mock-server.js.map