@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.
- package/dist/cli-runner.cjs +5006 -0
- package/dist/cli-runner.cjs.map +1 -0
- package/dist/cli-runner.js +4989 -0
- package/dist/cli-runner.js.map +1 -0
- package/dist/filedatabase.cjs +94 -13
- package/dist/filedatabase.cjs.map +1 -1
- package/dist/filedatabase.js +94 -13
- package/dist/filedatabase.js.map +1 -1
- package/dist/http-client2.cjs +1369 -9
- package/dist/http-client2.cjs.map +1 -1
- package/dist/http-client2.js +1359 -9
- package/dist/http-client2.js.map +1 -1
- package/dist/index.cjs +1244 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1224 -20
- package/dist/index.js.map +1 -1
- package/dist/init.cjs +9 -6
- package/dist/init.cjs.map +1 -1
- package/dist/init.js +9 -6
- package/dist/init.js.map +1 -1
- package/dist/logger.cjs +9 -6
- package/dist/logger.cjs.map +1 -1
- package/dist/logger.js +9 -6
- package/dist/logger.js.map +1 -1
- package/dist/mock-server.cjs +192 -343
- package/dist/mock-server.cjs.map +1 -1
- package/dist/mock-server.js +190 -343
- package/dist/mock-server.js.map +1 -1
- package/dist/tasks.cjs +2295 -0
- package/dist/tasks.cjs.map +1 -0
- package/dist/tasks.js +2240 -0
- package/dist/tasks.js.map +1 -0
- package/dist/utils.cjs +15 -2
- package/dist/utils.cjs.map +1 -1
- package/dist/utils.js +12 -1
- package/dist/utils.js.map +1 -1
- package/package.json +11 -3
package/dist/mock-server.cjs
CHANGED
|
@@ -31,6 +31,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var mock_server_exports = {};
|
|
32
32
|
__export(mock_server_exports, {
|
|
33
33
|
MockServer: () => MockServer,
|
|
34
|
+
MockStorage: () => MockStorage,
|
|
35
|
+
computeMockKey: () => computeMockKey,
|
|
34
36
|
createMockServer: () => createMockServer
|
|
35
37
|
});
|
|
36
38
|
module.exports = __toCommonJS(mock_server_exports);
|
|
@@ -40,6 +42,9 @@ var import_express = __toESM(require("express"), 1);
|
|
|
40
42
|
var import_http = __toESM(require("http"), 1);
|
|
41
43
|
var import_morgan = __toESM(require("morgan"), 1);
|
|
42
44
|
|
|
45
|
+
// src/mock-server/mock-storage.ts
|
|
46
|
+
var import_crypto = require("crypto");
|
|
47
|
+
|
|
43
48
|
// src/filedatabase/index.ts
|
|
44
49
|
var import_fs3 = __toESM(require("fs"), 1);
|
|
45
50
|
var import_path3 = __toESM(require("path"), 1);
|
|
@@ -299,7 +304,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
299
304
|
const versions = await this.getVersions();
|
|
300
305
|
while (versions.length > this.maxVersions) {
|
|
301
306
|
const versionToDelete = import_path3.default.resolve(this.getDestinationPath(), versions.shift());
|
|
302
|
-
this.logger.
|
|
307
|
+
this.logger.silly?.(`[FileDatabase] Deleting old version: ${versionToDelete}`);
|
|
303
308
|
await import_fs3.default.promises.rm(versionToDelete, { recursive: true, force: true });
|
|
304
309
|
}
|
|
305
310
|
return versionName;
|
|
@@ -577,7 +582,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
577
582
|
};
|
|
578
583
|
this.metadata.files.push(fileEntry);
|
|
579
584
|
this.lastFileData = null;
|
|
580
|
-
this.logger.
|
|
585
|
+
this.logger.silly?.(`[FileDatabase] Created new file: ${fileEntry.fileName}, fileNumber: ${this.currentFileNumber}`);
|
|
581
586
|
}
|
|
582
587
|
/**
|
|
583
588
|
* Figure out what data to write and which file to use (for pagination)
|
|
@@ -610,7 +615,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
610
615
|
const filesBeforeCreate = this.metadata.files.length;
|
|
611
616
|
this.makeNewFile();
|
|
612
617
|
newlyCreatedFileIndex = filesBeforeCreate;
|
|
613
|
-
this.logger.
|
|
618
|
+
this.logger.silly?.(`[FileDatabase] Creating new file for unique custom metadata combination, fileNumber: ${this.currentFileNumber}`);
|
|
614
619
|
} else if (this.metadata.files.length === 0) {
|
|
615
620
|
this.makeNewFile();
|
|
616
621
|
}
|
|
@@ -659,7 +664,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
659
664
|
dataLeftOver = null;
|
|
660
665
|
}
|
|
661
666
|
const fileName = this.metadata.files[this.metadata.files.length - 1].fileName;
|
|
662
|
-
this.logger.
|
|
667
|
+
this.logger.silly?.(
|
|
663
668
|
`[FileDatabase] figureOutDataAndFileToWrite: filename=${fileName}, forceNewFile=${forceNewFile}, targetFileIndex=${targetFileIndex}, dataToWrite.length=${Array.isArray(dataToWrite) ? dataToWrite.length : "N/A"}, lastFileRecordsCount=${lastFileRecordsCount}`
|
|
664
669
|
);
|
|
665
670
|
return { dataToWrite, dataLeftOver, fileName };
|
|
@@ -714,7 +719,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
714
719
|
this.metadata.modifiedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
715
720
|
this.metadata.dataType = detectDataType(dataToWrite);
|
|
716
721
|
this.metadata.totalRecords = this.metadata.files.reduce((sum, file) => sum + (file.recordsCount || 0), 0);
|
|
717
|
-
this.logger.
|
|
722
|
+
this.logger.silly?.(
|
|
718
723
|
`[FileDatabase] Updated metadata for file ${currentFile.fileName}: recordsCount=${recordsCount}, totalRecords=${this.metadata.totalRecords}`
|
|
719
724
|
);
|
|
720
725
|
}
|
|
@@ -738,7 +743,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
738
743
|
}
|
|
739
744
|
try {
|
|
740
745
|
await import_fs3.default.promises.writeFile(filePath, serializedData, "utf8");
|
|
741
|
-
this.logger.
|
|
746
|
+
this.logger.silly?.(`[FileDatabase] Wrote ${bytesToHumanReadable(requiredBytes)} to ${filePath}`);
|
|
742
747
|
} catch (error) {
|
|
743
748
|
throw new FileDatabaseError(`Failed to write file ${filePath}: ${error.message}`);
|
|
744
749
|
}
|
|
@@ -820,7 +825,8 @@ var FileDatabase = class _FileDatabase {
|
|
|
820
825
|
this.useMetadata = format.hasMetadata;
|
|
821
826
|
}
|
|
822
827
|
if (this.useMetadata) {
|
|
823
|
-
const
|
|
828
|
+
const destPath = this.getDestinationPath();
|
|
829
|
+
const metadataPath = import_path3.default.join(destPath, "metadata.json");
|
|
824
830
|
if (import_fs3.default.existsSync(metadataPath)) {
|
|
825
831
|
try {
|
|
826
832
|
const rawData = await import_fs3.default.promises.readFile(metadataPath, "utf8");
|
|
@@ -834,7 +840,9 @@ var FileDatabase = class _FileDatabase {
|
|
|
834
840
|
throw new FileDatabaseError(`Failed to read metadata: ${e.message}`);
|
|
835
841
|
}
|
|
836
842
|
} else {
|
|
837
|
-
throw new FileDatabaseError(
|
|
843
|
+
throw new FileDatabaseError(
|
|
844
|
+
`[FileDatabase] No metadata found in non-versioned mode. Looked for: ${metadataPath} (table path: ${destPath})`
|
|
845
|
+
);
|
|
838
846
|
}
|
|
839
847
|
} else {
|
|
840
848
|
this.metadata = await this.figureMetadataFromVersionFiles("");
|
|
@@ -851,6 +859,13 @@ var FileDatabase = class _FileDatabase {
|
|
|
851
859
|
* Write data to the file database
|
|
852
860
|
*/
|
|
853
861
|
async write(data, options = {}) {
|
|
862
|
+
if (options.filename) {
|
|
863
|
+
const destPath2 = this.getDestinationPath();
|
|
864
|
+
await ensurePath(destPath2);
|
|
865
|
+
const filePath = import_path3.default.join(destPath2, options.filename);
|
|
866
|
+
await this.safeWrite(filePath, data);
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
854
869
|
if (options.forceNewVersion && !this.versioned) {
|
|
855
870
|
throw new FileDatabaseError("Cannot use forceNewVersion in non-versioned mode");
|
|
856
871
|
}
|
|
@@ -874,17 +889,17 @@ var FileDatabase = class _FileDatabase {
|
|
|
874
889
|
});
|
|
875
890
|
if (matches) {
|
|
876
891
|
targetFileIndex = i;
|
|
877
|
-
this.logger.
|
|
892
|
+
this.logger.silly?.(`[FileDatabase] Found existing file with matching custom metadata: ${fileEntry.fileName}, metadata: ${JSON.stringify(options.customMetadata)}`);
|
|
878
893
|
break;
|
|
879
894
|
} else {
|
|
880
|
-
this.logger.
|
|
895
|
+
this.logger.silly?.(`[FileDatabase] File ${fileEntry.fileName} does not match custom metadata: ${JSON.stringify(options.customMetadata)}`);
|
|
881
896
|
}
|
|
882
897
|
}
|
|
883
898
|
if (targetFileIndex === null) {
|
|
884
|
-
this.logger.
|
|
899
|
+
this.logger.silly?.(`[FileDatabase] No existing file found with custom metadata: ${JSON.stringify(options.customMetadata)}, will create new file`);
|
|
885
900
|
}
|
|
886
901
|
} else {
|
|
887
|
-
this.logger.
|
|
902
|
+
this.logger.silly?.(`[FileDatabase] No custom metadata provided, will create new file`);
|
|
888
903
|
}
|
|
889
904
|
if (targetFileIndex !== null) {
|
|
890
905
|
const targetFile = this.metadata.files[targetFileIndex];
|
|
@@ -913,7 +928,17 @@ var FileDatabase = class _FileDatabase {
|
|
|
913
928
|
* Read data from the file database
|
|
914
929
|
*/
|
|
915
930
|
async read(options = {}) {
|
|
916
|
-
const { version, nextPage = false, pageSize } = options;
|
|
931
|
+
const { version, nextPage = false, pageSize, filename } = options;
|
|
932
|
+
if (filename) {
|
|
933
|
+
const destPath = this.getDestinationPath(version);
|
|
934
|
+
const filePath = import_path3.default.join(destPath, filename);
|
|
935
|
+
try {
|
|
936
|
+
const rawData = await import_fs3.default.promises.readFile(filePath, "utf8");
|
|
937
|
+
return JSON.parse(rawData);
|
|
938
|
+
} catch (error) {
|
|
939
|
+
throw new FileDatabaseError(`Failed to read file ${filename}: ${error.message}`);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
917
942
|
await this.prepare({ read: true, version });
|
|
918
943
|
const isNonPaginatedData = this.metadata.dataType === "text" || this.metadata.dataType === "xml" || this.metadata.dataType === "json-object";
|
|
919
944
|
if (isNonPaginatedData) {
|
|
@@ -994,6 +1019,67 @@ var FileDatabase = class _FileDatabase {
|
|
|
994
1019
|
this.currentRecord = 0;
|
|
995
1020
|
this.hasReadFirstPage = false;
|
|
996
1021
|
}
|
|
1022
|
+
/**
|
|
1023
|
+
* List filenames in the table directory.
|
|
1024
|
+
* For catalog/key-value usage (files written with { filename }).
|
|
1025
|
+
* Returns data file names (.json, .txt, .xml) excluding metadata.json.
|
|
1026
|
+
*/
|
|
1027
|
+
async listFilenames() {
|
|
1028
|
+
const destPath = this.versioned && this.currentVersion ? import_path3.default.join(this.getDestinationPath(), this.currentVersion) : this.getDestinationPath();
|
|
1029
|
+
try {
|
|
1030
|
+
const entries = await import_fs3.default.promises.readdir(destPath, { withFileTypes: true });
|
|
1031
|
+
return entries.filter((e) => e.isFile() && e.name !== "metadata.json" && /\.(json|txt|xml)$/i.test(e.name)).map((e) => e.name);
|
|
1032
|
+
} catch (err) {
|
|
1033
|
+
if (err?.code === "ENOENT") return [];
|
|
1034
|
+
throw new FileDatabaseError(`Failed to list files: ${err.message}`);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
/**
|
|
1038
|
+
* Remove a file from the table directory (catalog mode).
|
|
1039
|
+
* Use with listFilenames() to manage individual files.
|
|
1040
|
+
*/
|
|
1041
|
+
async removeFile(filename) {
|
|
1042
|
+
const destPath = this.versioned && this.currentVersion ? import_path3.default.join(this.getDestinationPath(), this.currentVersion) : this.getDestinationPath();
|
|
1043
|
+
const filePath = import_path3.default.join(destPath, filename);
|
|
1044
|
+
try {
|
|
1045
|
+
await import_fs3.default.promises.unlink(filePath);
|
|
1046
|
+
} catch (err) {
|
|
1047
|
+
if (err?.code === "ENOENT") return;
|
|
1048
|
+
throw new FileDatabaseError(`Failed to remove file ${filename}: ${err.message}`);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
/**
|
|
1052
|
+
* Remove a file and its metadata entry (non-versioned mode with useMetadata).
|
|
1053
|
+
* Use with findData() to get fileName, then call removeFileEntry to delete.
|
|
1054
|
+
*/
|
|
1055
|
+
async removeFileEntry(filename) {
|
|
1056
|
+
if (this.versioned) {
|
|
1057
|
+
throw new FileDatabaseError("removeFileEntry is only supported in non-versioned mode");
|
|
1058
|
+
}
|
|
1059
|
+
await this.prepare({ read: true });
|
|
1060
|
+
const idx = this.metadata.files.findIndex((f) => f.fileName === filename);
|
|
1061
|
+
if (idx === -1) {
|
|
1062
|
+
throw new FileDatabaseError(`File entry ${filename} not found in metadata`);
|
|
1063
|
+
}
|
|
1064
|
+
const entry = this.metadata.files[idx];
|
|
1065
|
+
const recordsCount = entry.recordsCount || 0;
|
|
1066
|
+
this.metadata.files.splice(idx, 1);
|
|
1067
|
+
this.metadata.totalRecords = Math.max(0, (this.metadata.totalRecords || 0) - recordsCount);
|
|
1068
|
+
const destPath = this.getDestinationPath();
|
|
1069
|
+
const filePath = import_path3.default.join(destPath, filename);
|
|
1070
|
+
try {
|
|
1071
|
+
await import_fs3.default.promises.unlink(filePath);
|
|
1072
|
+
} catch (err) {
|
|
1073
|
+
if (err?.code === "ENOENT") {
|
|
1074
|
+
this.logger.warn?.(`[FileDatabase] File ${filename} already missing on disk`);
|
|
1075
|
+
} else {
|
|
1076
|
+
throw new FileDatabaseError(`Failed to remove file ${filename}: ${err.message}`);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
if (this.useMetadata) {
|
|
1080
|
+
await this.saveVersionMetadata(this.metadata);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
997
1083
|
/**
|
|
998
1084
|
* Set file-level synopsis calculation function
|
|
999
1085
|
*/
|
|
@@ -1077,335 +1163,106 @@ var FileDatabase = class _FileDatabase {
|
|
|
1077
1163
|
}
|
|
1078
1164
|
};
|
|
1079
1165
|
|
|
1080
|
-
// src/mock-server/
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
return `[md5:${(0, import_crypto.createHash)("md5").update(value).digest("hex")}]`;
|
|
1089
|
-
}
|
|
1090
|
-
function sanitizeUrlEncodedString(input, keysToSanitize) {
|
|
1091
|
-
const parsed = (0, import_querystring.parse)(input);
|
|
1092
|
-
const sanitized = sanitizeObject(parsed, keysToSanitize);
|
|
1093
|
-
return (0, import_querystring.stringify)(sanitized);
|
|
1094
|
-
}
|
|
1095
|
-
function sanitizeObject(obj, keysToSanitize) {
|
|
1096
|
-
if (Array.isArray(obj)) {
|
|
1097
|
-
return obj;
|
|
1098
|
-
}
|
|
1099
|
-
const result = {};
|
|
1100
|
-
for (const [key, value] of Object.entries(obj || {})) {
|
|
1101
|
-
if (keysToSanitize.includes(key.toLowerCase())) {
|
|
1102
|
-
result[key] = maskValue2(String(value));
|
|
1103
|
-
} else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
1104
|
-
result[key] = sanitizeObject(value, keysToSanitize);
|
|
1105
|
-
} else {
|
|
1106
|
-
result[key] = value;
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
return result;
|
|
1110
|
-
}
|
|
1111
|
-
function sanitizeHeaders(headers, additionalKeys = []) {
|
|
1112
|
-
const sensitiveKeys = ["authorization", "x-api-key", "api-key", "bearer", ...additionalKeys.map((k) => k.toLowerCase())];
|
|
1113
|
-
const sanitized = { ...headers };
|
|
1114
|
-
for (const [key, value] of Object.entries(sanitized)) {
|
|
1115
|
-
if (sensitiveKeys.includes(key.toLowerCase())) {
|
|
1116
|
-
sanitized[key] = maskValue2(value);
|
|
1117
|
-
}
|
|
1118
|
-
}
|
|
1119
|
-
return sanitized;
|
|
1166
|
+
// src/mock-server/mock-storage.ts
|
|
1167
|
+
function stableStringify(obj) {
|
|
1168
|
+
if (obj === null) return "null";
|
|
1169
|
+
if (obj === void 0) return "undefined";
|
|
1170
|
+
if (typeof obj !== "object") return JSON.stringify(obj);
|
|
1171
|
+
if (Array.isArray(obj)) return "[" + obj.map(stableStringify).join(",") + "]";
|
|
1172
|
+
const keys = Object.keys(obj).sort();
|
|
1173
|
+
return "{" + keys.map((k) => JSON.stringify(k) + ":" + stableStringify(obj[k])).join(",") + "}";
|
|
1120
1174
|
}
|
|
1121
|
-
function
|
|
1122
|
-
if (
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
return (0, import_querystring.stringify)(sanitizeObject(parsed, keysToSanitize));
|
|
1127
|
-
} catch {
|
|
1128
|
-
return data;
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1131
|
-
return data;
|
|
1132
|
-
} else if (typeof data === "object" && data !== null) {
|
|
1133
|
-
return sanitizeObject(data, keysToSanitize);
|
|
1134
|
-
}
|
|
1135
|
-
return data;
|
|
1175
|
+
function normalizeQuery(query) {
|
|
1176
|
+
if (!query) return "";
|
|
1177
|
+
const params = new URLSearchParams(query);
|
|
1178
|
+
const sorted = [...params.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
|
1179
|
+
return new URLSearchParams(sorted).toString();
|
|
1136
1180
|
}
|
|
1137
|
-
function
|
|
1138
|
-
if (typeof data === "object" && data !== null) {
|
|
1139
|
-
return sanitizeObject(data, keysToSanitize);
|
|
1140
|
-
}
|
|
1141
|
-
return data;
|
|
1142
|
-
}
|
|
1143
|
-
function sanitizeRequest(params, sensitiveKeys) {
|
|
1144
|
-
const { query, requestData, headers = {}, data } = params;
|
|
1181
|
+
function buildCriteria(method, host, pathname, query, requestData) {
|
|
1145
1182
|
return {
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1183
|
+
method: method.toUpperCase(),
|
|
1184
|
+
host,
|
|
1185
|
+
pathname: pathname || "/",
|
|
1186
|
+
query: normalizeQuery(query),
|
|
1187
|
+
requestBody: requestData != null ? stableStringify(requestData) : ""
|
|
1150
1188
|
};
|
|
1151
1189
|
}
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1190
|
+
function computeMockKey(method, host, pathname, query, requestData) {
|
|
1191
|
+
const criteria = buildCriteria(method, host, pathname, query, requestData);
|
|
1192
|
+
const str = JSON.stringify(criteria);
|
|
1193
|
+
return (0, import_crypto.createHash)("sha256").update(str).digest("hex").slice(0, 32);
|
|
1194
|
+
}
|
|
1195
|
+
var MockStorage = class {
|
|
1155
1196
|
fileDb;
|
|
1156
|
-
catalogPath;
|
|
1157
1197
|
logger;
|
|
1158
|
-
constructor(
|
|
1159
|
-
this.
|
|
1160
|
-
this.
|
|
1161
|
-
|
|
1198
|
+
constructor(config) {
|
|
1199
|
+
this.logger = config.logger ?? console;
|
|
1200
|
+
this.fileDb = new FileDatabase({
|
|
1201
|
+
basePath: config.basePath,
|
|
1202
|
+
namespace: "mocks",
|
|
1203
|
+
tableName: "responses",
|
|
1204
|
+
versioned: false,
|
|
1205
|
+
useMetadata: true,
|
|
1206
|
+
logger: this.logger
|
|
1207
|
+
});
|
|
1162
1208
|
}
|
|
1163
1209
|
/**
|
|
1164
|
-
* Store a mock response
|
|
1210
|
+
* Store a mock response.
|
|
1165
1211
|
*/
|
|
1166
|
-
async
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
query: url.search.slice(1),
|
|
1171
|
-
requestData,
|
|
1172
|
-
headers: {},
|
|
1173
|
-
data: responseData.data
|
|
1174
|
-
}, sensitiveKeys);
|
|
1175
|
-
const entry = {
|
|
1176
|
-
method: "GET",
|
|
1177
|
-
// Will be passed from caller
|
|
1178
|
-
host: url.host,
|
|
1179
|
-
pathname: url.pathname,
|
|
1180
|
-
query: sanitized.query,
|
|
1181
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1182
|
-
...sanitized.requestData !== void 0 ? { requestData: sanitized.requestData } : {},
|
|
1183
|
-
...operationId ? { operationId } : {},
|
|
1184
|
-
...mockName ? { mockName } : {}
|
|
1185
|
-
};
|
|
1186
|
-
const filename = this.generateFilename(entry);
|
|
1187
|
-
const catalogFilePath = path4.join(this.catalogPath, `${filename}.json`);
|
|
1188
|
-
const responseFilename = `response_${filename}.json`;
|
|
1189
|
-
await this.fileDb.write(responseData, { filename: responseFilename });
|
|
1190
|
-
entry.file = responseFilename;
|
|
1191
|
-
await fs4.mkdir(path4.dirname(catalogFilePath), { recursive: true });
|
|
1192
|
-
await fs4.writeFile(catalogFilePath, JSON.stringify(entry, null, 2), "utf-8");
|
|
1193
|
-
return filename;
|
|
1194
|
-
} catch (error) {
|
|
1195
|
-
this.logger.error?.("Error storing mock:", error);
|
|
1196
|
-
throw error;
|
|
1197
|
-
}
|
|
1212
|
+
async store(method, requestUrl, requestData, responseData) {
|
|
1213
|
+
const url = new URL(requestUrl);
|
|
1214
|
+
const criteria = buildCriteria(method, url.host, url.pathname, url.search.slice(1), requestData);
|
|
1215
|
+
await this.fileDb.write(responseData, { customMetadata: criteria });
|
|
1198
1216
|
}
|
|
1199
1217
|
/**
|
|
1200
|
-
* Find mock response
|
|
1218
|
+
* Find a mock response by request criteria. Returns null if not found.
|
|
1201
1219
|
*/
|
|
1202
|
-
async
|
|
1220
|
+
async find(method, host, pathname, query, requestData) {
|
|
1221
|
+
const criteria = buildCriteria(method, host, pathname, query, requestData);
|
|
1203
1222
|
try {
|
|
1204
|
-
const
|
|
1205
|
-
if (
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
return fuzzyMatch;
|
|
1223
|
+
const results = await this.fileDb.findData(criteria);
|
|
1224
|
+
if (results.length === 0) return null;
|
|
1225
|
+
return results[0].data;
|
|
1226
|
+
} catch (err) {
|
|
1227
|
+
if (err instanceof FileDatabaseError && /No metadata found/.test(err.message)) {
|
|
1228
|
+
return null;
|
|
1211
1229
|
}
|
|
1212
|
-
|
|
1213
|
-
} catch (error) {
|
|
1214
|
-
this.logger.error?.("Error finding mock:", error);
|
|
1215
|
-
return null;
|
|
1230
|
+
throw err;
|
|
1216
1231
|
}
|
|
1217
1232
|
}
|
|
1218
1233
|
/**
|
|
1219
|
-
*
|
|
1234
|
+
* List all stored mock keys (opaque identifiers for remove)
|
|
1220
1235
|
*/
|
|
1221
|
-
async
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
for (const entry of entries) {
|
|
1225
|
-
if (this.matchesCriteria(entry, criteria)) {
|
|
1226
|
-
return await this.loadResponseData(entry.file);
|
|
1227
|
-
}
|
|
1228
|
-
}
|
|
1229
|
-
return null;
|
|
1230
|
-
} catch (error) {
|
|
1231
|
-
this.logger.error?.("Error in exact match:", error);
|
|
1232
|
-
return null;
|
|
1233
|
-
}
|
|
1236
|
+
async listKeys() {
|
|
1237
|
+
const files = await this.fileDb.listFilenames();
|
|
1238
|
+
return files.sort();
|
|
1234
1239
|
}
|
|
1235
1240
|
/**
|
|
1236
|
-
*
|
|
1241
|
+
* Remove a mock by key (from listKeys)
|
|
1237
1242
|
*/
|
|
1238
|
-
async
|
|
1243
|
+
async remove(fileName) {
|
|
1244
|
+
const name = fileName.endsWith(".json") ? fileName : `${fileName}.json`;
|
|
1239
1245
|
try {
|
|
1240
|
-
|
|
1241
|
-
...criteria,
|
|
1242
|
-
query: this.stripCommonParams(criteria.query)
|
|
1243
|
-
};
|
|
1244
|
-
return this.findExactMatch(fuzzyCriteria);
|
|
1245
|
-
} catch (error) {
|
|
1246
|
-
this.logger.error?.("Error in fuzzy match:", error);
|
|
1247
|
-
return null;
|
|
1248
|
-
}
|
|
1249
|
-
}
|
|
1250
|
-
/**
|
|
1251
|
-
* Check if a catalog entry matches the request criteria
|
|
1252
|
-
*/
|
|
1253
|
-
matchesCriteria(entry, criteria) {
|
|
1254
|
-
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);
|
|
1255
|
-
}
|
|
1256
|
-
/**
|
|
1257
|
-
* Check if query strings match (with sanitization)
|
|
1258
|
-
*/
|
|
1259
|
-
queriesMatch(storedQuery, requestQuery) {
|
|
1260
|
-
return storedQuery === requestQuery;
|
|
1261
|
-
}
|
|
1262
|
-
/**
|
|
1263
|
-
* Check if request data matches
|
|
1264
|
-
*/
|
|
1265
|
-
requestDataMatches(storedData, requestData) {
|
|
1266
|
-
if (!storedData && !requestData) {
|
|
1246
|
+
await this.fileDb.removeFileEntry(name);
|
|
1267
1247
|
return true;
|
|
1268
|
-
}
|
|
1269
|
-
if (!storedData || !requestData) {
|
|
1248
|
+
} catch {
|
|
1270
1249
|
return false;
|
|
1271
1250
|
}
|
|
1272
|
-
return JSON.stringify(storedData) === JSON.stringify(requestData);
|
|
1273
1251
|
}
|
|
1274
1252
|
/**
|
|
1275
|
-
*
|
|
1253
|
+
* Remove a mock by request criteria (method, host, pathname, query, requestData)
|
|
1276
1254
|
*/
|
|
1277
|
-
async
|
|
1255
|
+
async removeByCriteria(method, host, pathname, query, requestData) {
|
|
1256
|
+
const criteria = buildCriteria(method, host, pathname, query, requestData);
|
|
1278
1257
|
try {
|
|
1279
|
-
const
|
|
1280
|
-
return
|
|
1281
|
-
|
|
1282
|
-
this.logger.error?.("Error loading response data:", error);
|
|
1283
|
-
return null;
|
|
1284
|
-
}
|
|
1285
|
-
}
|
|
1286
|
-
/**
|
|
1287
|
-
* Strip common query parameters for fuzzy matching
|
|
1288
|
-
*/
|
|
1289
|
-
stripCommonParams(query) {
|
|
1290
|
-
const commonParams = ["timestamp", "nonce", "cache", "_"];
|
|
1291
|
-
const params = new URLSearchParams(query);
|
|
1292
|
-
for (const param of commonParams) {
|
|
1293
|
-
params.delete(param);
|
|
1294
|
-
}
|
|
1295
|
-
return params.toString();
|
|
1296
|
-
}
|
|
1297
|
-
/**
|
|
1298
|
-
* Generate unique filename for catalog entry
|
|
1299
|
-
*/
|
|
1300
|
-
generateFilename(entry) {
|
|
1301
|
-
const timestamp = Date.now();
|
|
1302
|
-
const hash = this.simpleHash(`${entry.method}${entry.host}${entry.pathname}${entry.query}${entry.operationId || ""}`);
|
|
1303
|
-
return `mock_${timestamp}_${hash}`;
|
|
1304
|
-
}
|
|
1305
|
-
/**
|
|
1306
|
-
* Simple hash function for filename generation
|
|
1307
|
-
*/
|
|
1308
|
-
simpleHash(str) {
|
|
1309
|
-
let hash = 0;
|
|
1310
|
-
for (let i = 0; i < str.length; i++) {
|
|
1311
|
-
const char = str.charCodeAt(i);
|
|
1312
|
-
hash = (hash << 5) - hash + char;
|
|
1313
|
-
hash = hash & hash;
|
|
1314
|
-
}
|
|
1315
|
-
return Math.abs(hash).toString(36);
|
|
1316
|
-
}
|
|
1317
|
-
/**
|
|
1318
|
-
* List all catalog entries
|
|
1319
|
-
*/
|
|
1320
|
-
async listEntries() {
|
|
1321
|
-
try {
|
|
1322
|
-
const entries = [];
|
|
1323
|
-
const files = await fs4.readdir(this.catalogPath).catch(() => []);
|
|
1324
|
-
const jsonFiles = files.filter((file) => file.endsWith(".json") && !file.startsWith("response_"));
|
|
1325
|
-
for (const file of jsonFiles) {
|
|
1326
|
-
try {
|
|
1327
|
-
const filePath = path4.join(this.catalogPath, file);
|
|
1328
|
-
const content = await fs4.readFile(filePath, "utf-8");
|
|
1329
|
-
const entry = JSON.parse(content);
|
|
1330
|
-
entries.push(entry);
|
|
1331
|
-
} catch (error) {
|
|
1332
|
-
this.logger.warn?.(`Error reading catalog entry ${file}:`, error);
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1335
|
-
return entries;
|
|
1336
|
-
} catch (error) {
|
|
1337
|
-
this.logger.error?.("Error listing catalog entries:", error);
|
|
1338
|
-
return [];
|
|
1339
|
-
}
|
|
1340
|
-
}
|
|
1341
|
-
/**
|
|
1342
|
-
* Remove a catalog entry and its response data
|
|
1343
|
-
*/
|
|
1344
|
-
async removeEntry(filename) {
|
|
1345
|
-
try {
|
|
1346
|
-
const catalogFilePath = path4.join(this.catalogPath, `${filename}.json`);
|
|
1347
|
-
const responseFilename = `response_${filename}.json`;
|
|
1348
|
-
try {
|
|
1349
|
-
await fs4.unlink(catalogFilePath);
|
|
1350
|
-
} catch (error) {
|
|
1351
|
-
this.logger.warn?.(`Could not remove catalog file ${catalogFilePath}:`, error);
|
|
1352
|
-
}
|
|
1353
|
-
try {
|
|
1354
|
-
const responseFilePath = path4.join(this.catalogPath, responseFilename);
|
|
1355
|
-
await fs4.unlink(responseFilePath);
|
|
1356
|
-
} catch (error) {
|
|
1357
|
-
this.logger.warn?.(`Could not remove response file ${responseFilename}:`, error);
|
|
1358
|
-
}
|
|
1258
|
+
const results = await this.fileDb.findData(criteria);
|
|
1259
|
+
if (results.length === 0) return false;
|
|
1260
|
+
await this.fileDb.removeFileEntry(results[0].fileName);
|
|
1359
1261
|
return true;
|
|
1360
|
-
} catch
|
|
1361
|
-
this.logger.error?.("Error removing entry:", error);
|
|
1262
|
+
} catch {
|
|
1362
1263
|
return false;
|
|
1363
1264
|
}
|
|
1364
1265
|
}
|
|
1365
|
-
/**
|
|
1366
|
-
* Clean up orphaned files and invalid catalog entries
|
|
1367
|
-
*/
|
|
1368
|
-
async maintenance() {
|
|
1369
|
-
try {
|
|
1370
|
-
let cleaned = 0;
|
|
1371
|
-
const files = await fs4.readdir(this.catalogPath).catch(() => []);
|
|
1372
|
-
const catalogFiles = files.filter((file) => file.endsWith(".json") && !file.startsWith("response_"));
|
|
1373
|
-
const responseFiles = files.filter((file) => file.startsWith("response_"));
|
|
1374
|
-
for (const catalogFile of catalogFiles) {
|
|
1375
|
-
try {
|
|
1376
|
-
const catalogPath = path4.join(this.catalogPath, catalogFile);
|
|
1377
|
-
const content = await fs4.readFile(catalogPath, "utf-8");
|
|
1378
|
-
const entry = JSON.parse(content);
|
|
1379
|
-
const responseFile = entry.file;
|
|
1380
|
-
const responsePath = path4.join(this.catalogPath, responseFile);
|
|
1381
|
-
try {
|
|
1382
|
-
await fs4.access(responsePath);
|
|
1383
|
-
} catch {
|
|
1384
|
-
await fs4.unlink(catalogPath);
|
|
1385
|
-
cleaned++;
|
|
1386
|
-
this.logger.info?.(`Removed orphaned catalog entry: ${catalogFile}`);
|
|
1387
|
-
}
|
|
1388
|
-
} catch (error) {
|
|
1389
|
-
this.logger.warn?.(`Error processing catalog file ${catalogFile}:`, error);
|
|
1390
|
-
}
|
|
1391
|
-
}
|
|
1392
|
-
const catalogResponseFiles = catalogFiles.map(
|
|
1393
|
-
(file) => `response_${file.replace(".json", "")}.json`
|
|
1394
|
-
);
|
|
1395
|
-
for (const responseFile of responseFiles) {
|
|
1396
|
-
if (!catalogResponseFiles.includes(responseFile)) {
|
|
1397
|
-
const responsePath = path4.join(this.catalogPath, responseFile);
|
|
1398
|
-
await fs4.unlink(responsePath);
|
|
1399
|
-
cleaned++;
|
|
1400
|
-
this.logger.info?.(`Removed orphaned response file: ${responseFile}`);
|
|
1401
|
-
}
|
|
1402
|
-
}
|
|
1403
|
-
return { cleaned };
|
|
1404
|
-
} catch (error) {
|
|
1405
|
-
this.logger.error?.("Error during maintenance:", error);
|
|
1406
|
-
return { cleaned: 0 };
|
|
1407
|
-
}
|
|
1408
|
-
}
|
|
1409
1266
|
};
|
|
1410
1267
|
|
|
1411
1268
|
// src/mock-server/index.ts
|
|
@@ -1424,8 +1281,7 @@ var MockServer = class {
|
|
|
1424
1281
|
config;
|
|
1425
1282
|
app;
|
|
1426
1283
|
server = null;
|
|
1427
|
-
|
|
1428
|
-
catalog;
|
|
1284
|
+
storage;
|
|
1429
1285
|
stats;
|
|
1430
1286
|
startTime;
|
|
1431
1287
|
constructor(config) {
|
|
@@ -1451,15 +1307,7 @@ var MockServer = class {
|
|
|
1451
1307
|
errors: 0,
|
|
1452
1308
|
uptime: 0
|
|
1453
1309
|
};
|
|
1454
|
-
this.
|
|
1455
|
-
basePath: this.config.basePath,
|
|
1456
|
-
namespace: this.config.namespace,
|
|
1457
|
-
tableName: this.config.tableName,
|
|
1458
|
-
versioned: false,
|
|
1459
|
-
// Mock responses are typically not versioned
|
|
1460
|
-
logger: this.config.logger
|
|
1461
|
-
});
|
|
1462
|
-
this.catalog = new MockCatalog(this.fileDb, this.config.basePath, this.config.logger);
|
|
1310
|
+
this.storage = new MockStorage({ basePath: this.config.basePath, logger: this.config.logger });
|
|
1463
1311
|
this.setupMiddleware();
|
|
1464
1312
|
this.setupRoutes();
|
|
1465
1313
|
}
|
|
@@ -1602,14 +1450,7 @@ var MockServer = class {
|
|
|
1602
1450
|
const pathname = url.pathname;
|
|
1603
1451
|
const query = url.search.slice(1);
|
|
1604
1452
|
const requestData = this.extractRequestData(req);
|
|
1605
|
-
|
|
1606
|
-
method,
|
|
1607
|
-
host: url.host,
|
|
1608
|
-
pathname,
|
|
1609
|
-
query,
|
|
1610
|
-
requestData
|
|
1611
|
-
};
|
|
1612
|
-
return await this.catalog.findMock(criteria);
|
|
1453
|
+
return await this.storage.find(method, url.host, pathname, query, requestData);
|
|
1613
1454
|
} catch (error) {
|
|
1614
1455
|
this.config.logger.error?.("Error finding mock response:", error);
|
|
1615
1456
|
return null;
|
|
@@ -1630,33 +1471,39 @@ var MockServer = class {
|
|
|
1630
1471
|
/**
|
|
1631
1472
|
* Store a mock response from an HTTP request/response
|
|
1632
1473
|
*/
|
|
1633
|
-
async storeMock(requestUrl, requestData, responseData,
|
|
1634
|
-
|
|
1635
|
-
requestUrl,
|
|
1636
|
-
requestData,
|
|
1637
|
-
responseData,
|
|
1638
|
-
operationId,
|
|
1639
|
-
mockName,
|
|
1640
|
-
this.config.sensitiveKeys
|
|
1641
|
-
);
|
|
1474
|
+
async storeMock(requestUrl, requestData, responseData, method = "GET") {
|
|
1475
|
+
await this.storage.store(method, requestUrl, requestData, responseData);
|
|
1642
1476
|
}
|
|
1643
1477
|
/**
|
|
1644
|
-
* List all stored mock
|
|
1478
|
+
* List all stored mock keys
|
|
1645
1479
|
*/
|
|
1646
1480
|
async listMocks() {
|
|
1647
|
-
return await this.
|
|
1481
|
+
return await this.storage.listKeys();
|
|
1648
1482
|
}
|
|
1649
1483
|
/**
|
|
1650
|
-
* Remove a mock
|
|
1484
|
+
* Remove a mock by key (from listMocks)
|
|
1651
1485
|
*/
|
|
1652
|
-
async removeMock(
|
|
1653
|
-
return await this.
|
|
1486
|
+
async removeMock(key) {
|
|
1487
|
+
return await this.storage.remove(key);
|
|
1488
|
+
}
|
|
1489
|
+
/**
|
|
1490
|
+
* Remove a mock by request criteria (method, requestUrl, requestData)
|
|
1491
|
+
*/
|
|
1492
|
+
async removeMockByCriteria(requestUrl, requestData, method = "GET") {
|
|
1493
|
+
const url = new URL(requestUrl);
|
|
1494
|
+
return await this.storage.removeByCriteria(
|
|
1495
|
+
method,
|
|
1496
|
+
url.host,
|
|
1497
|
+
url.pathname,
|
|
1498
|
+
url.search.slice(1),
|
|
1499
|
+
requestData
|
|
1500
|
+
);
|
|
1654
1501
|
}
|
|
1655
1502
|
/**
|
|
1656
|
-
*
|
|
1503
|
+
* No-op (mocks are self-contained, no orphans)
|
|
1657
1504
|
*/
|
|
1658
1505
|
async maintenance() {
|
|
1659
|
-
return
|
|
1506
|
+
return { cleaned: 0 };
|
|
1660
1507
|
}
|
|
1661
1508
|
/**
|
|
1662
1509
|
* Get current configuration
|
|
@@ -1681,6 +1528,8 @@ async function createMockServer(config) {
|
|
|
1681
1528
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1682
1529
|
0 && (module.exports = {
|
|
1683
1530
|
MockServer,
|
|
1531
|
+
MockStorage,
|
|
1532
|
+
computeMockKey,
|
|
1684
1533
|
createMockServer
|
|
1685
1534
|
});
|
|
1686
1535
|
//# sourceMappingURL=mock-server.cjs.map
|