@nmakarov/cli-toolkit 0.14.2 → 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/args.cjs +49 -9
- package/dist/args.cjs.map +1 -1
- package/dist/args.js +49 -9
- package/dist/args.js.map +1 -1
- 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/db.cjs +9 -2
- package/dist/db.cjs.map +1 -1
- package/dist/db.js +9 -2
- package/dist/db.js.map +1 -1
- package/dist/errors.cjs +17 -0
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.js +15 -0
- package/dist/errors.js.map +1 -1
- package/dist/filedatabase.cjs +110 -37
- package/dist/filedatabase.cjs.map +1 -1
- package/dist/filedatabase.js +110 -36
- package/dist/filedatabase.js.map +1 -1
- package/dist/http-client.cjs +44 -34
- package/dist/http-client.cjs.map +1 -1
- package/dist/http-client.js +44 -34
- package/dist/http-client.js.map +1 -1
- package/dist/http-client2.cjs +1728 -0
- package/dist/http-client2.cjs.map +1 -0
- package/dist/http-client2.js +1690 -0
- package/dist/http-client2.js.map +1 -0
- package/dist/index.cjs +1456 -112
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1436 -110
- package/dist/index.js.map +1 -1
- package/dist/init.cjs +199 -82
- package/dist/init.cjs.map +1 -1
- package/dist/init.js +199 -82
- package/dist/init.js.map +1 -1
- package/dist/logger.cjs +28 -42
- package/dist/logger.cjs.map +1 -1
- package/dist/logger.js +28 -42
- package/dist/logger.js.map +1 -1
- package/dist/mock-server.cjs +205 -359
- package/dist/mock-server.cjs.map +1 -1
- package/dist/mock-server.js +203 -359
- package/dist/mock-server.js.map +1 -1
- package/dist/params.cjs +114 -15
- package/dist/params.cjs.map +1 -1
- package/dist/params.js +114 -15
- package/dist/params.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 +18 -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);
|
|
@@ -161,15 +166,15 @@ var ParamError = class extends FrameworkError {
|
|
|
161
166
|
this.name = "ParamError";
|
|
162
167
|
}
|
|
163
168
|
};
|
|
164
|
-
|
|
165
|
-
// src/filedatabase/index.ts
|
|
166
|
-
var FileDatabaseError = class extends Error {
|
|
169
|
+
var FileDatabaseError = class extends FrameworkError {
|
|
167
170
|
constructor(message) {
|
|
168
171
|
super(message);
|
|
169
172
|
this.name = "FileDatabaseError";
|
|
170
173
|
}
|
|
171
174
|
};
|
|
172
|
-
|
|
175
|
+
|
|
176
|
+
// src/filedatabase/index.ts
|
|
177
|
+
var FileDatabase = class _FileDatabase {
|
|
173
178
|
basePath;
|
|
174
179
|
namespace;
|
|
175
180
|
tableName = null;
|
|
@@ -206,18 +211,8 @@ var FileDatabase = class {
|
|
|
206
211
|
maxVersions: "number default 5",
|
|
207
212
|
pageSize: "number default 5000"
|
|
208
213
|
};
|
|
209
|
-
const
|
|
210
|
-
config = {
|
|
211
|
-
basePath: opts.basePath ?? paramsConfig.basePath,
|
|
212
|
-
namespace: opts.namespace ?? paramsConfig.namespace,
|
|
213
|
-
tableName: opts.tableName ?? paramsConfig.tableName ?? null,
|
|
214
|
-
versioned: opts.versioned ?? true,
|
|
215
|
-
maxVersions: opts.maxVersions ?? paramsConfig.maxVersions,
|
|
216
|
-
pageSize: opts.pageSize ?? paramsConfig.pageSize,
|
|
217
|
-
useMetadata: opts.useMetadata ?? true,
|
|
218
|
-
freeSpaceThreshold: opts.freeSpaceThreshold ?? 100 * 1024 * 1024,
|
|
219
|
-
logger: context.logger
|
|
220
|
-
};
|
|
214
|
+
const discovered = context.params.getAllForModule(defs);
|
|
215
|
+
config = { ...discovered, ...opts, logger: context.logger };
|
|
221
216
|
} else {
|
|
222
217
|
config = contextOrConfig;
|
|
223
218
|
}
|
|
@@ -235,6 +230,13 @@ var FileDatabase = class {
|
|
|
235
230
|
this.logger = config.logger || console;
|
|
236
231
|
this.metadata = this.getDefaultMetadata();
|
|
237
232
|
}
|
|
233
|
+
/**
|
|
234
|
+
* Initialize FileDatabase from context and options.
|
|
235
|
+
* Params are read via getAllForModule("filedatabase", defs) for --showUsedParams grouping.
|
|
236
|
+
*/
|
|
237
|
+
static init(context, options) {
|
|
238
|
+
return new _FileDatabase(context, options ?? {});
|
|
239
|
+
}
|
|
238
240
|
/**
|
|
239
241
|
* Get default metadata structure
|
|
240
242
|
*/
|
|
@@ -302,7 +304,7 @@ var FileDatabase = class {
|
|
|
302
304
|
const versions = await this.getVersions();
|
|
303
305
|
while (versions.length > this.maxVersions) {
|
|
304
306
|
const versionToDelete = import_path3.default.resolve(this.getDestinationPath(), versions.shift());
|
|
305
|
-
this.logger.
|
|
307
|
+
this.logger.silly?.(`[FileDatabase] Deleting old version: ${versionToDelete}`);
|
|
306
308
|
await import_fs3.default.promises.rm(versionToDelete, { recursive: true, force: true });
|
|
307
309
|
}
|
|
308
310
|
return versionName;
|
|
@@ -580,7 +582,7 @@ var FileDatabase = class {
|
|
|
580
582
|
};
|
|
581
583
|
this.metadata.files.push(fileEntry);
|
|
582
584
|
this.lastFileData = null;
|
|
583
|
-
this.logger.
|
|
585
|
+
this.logger.silly?.(`[FileDatabase] Created new file: ${fileEntry.fileName}, fileNumber: ${this.currentFileNumber}`);
|
|
584
586
|
}
|
|
585
587
|
/**
|
|
586
588
|
* Figure out what data to write and which file to use (for pagination)
|
|
@@ -613,7 +615,7 @@ var FileDatabase = class {
|
|
|
613
615
|
const filesBeforeCreate = this.metadata.files.length;
|
|
614
616
|
this.makeNewFile();
|
|
615
617
|
newlyCreatedFileIndex = filesBeforeCreate;
|
|
616
|
-
this.logger.
|
|
618
|
+
this.logger.silly?.(`[FileDatabase] Creating new file for unique custom metadata combination, fileNumber: ${this.currentFileNumber}`);
|
|
617
619
|
} else if (this.metadata.files.length === 0) {
|
|
618
620
|
this.makeNewFile();
|
|
619
621
|
}
|
|
@@ -662,7 +664,7 @@ var FileDatabase = class {
|
|
|
662
664
|
dataLeftOver = null;
|
|
663
665
|
}
|
|
664
666
|
const fileName = this.metadata.files[this.metadata.files.length - 1].fileName;
|
|
665
|
-
this.logger.
|
|
667
|
+
this.logger.silly?.(
|
|
666
668
|
`[FileDatabase] figureOutDataAndFileToWrite: filename=${fileName}, forceNewFile=${forceNewFile}, targetFileIndex=${targetFileIndex}, dataToWrite.length=${Array.isArray(dataToWrite) ? dataToWrite.length : "N/A"}, lastFileRecordsCount=${lastFileRecordsCount}`
|
|
667
669
|
);
|
|
668
670
|
return { dataToWrite, dataLeftOver, fileName };
|
|
@@ -717,7 +719,7 @@ var FileDatabase = class {
|
|
|
717
719
|
this.metadata.modifiedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
718
720
|
this.metadata.dataType = detectDataType(dataToWrite);
|
|
719
721
|
this.metadata.totalRecords = this.metadata.files.reduce((sum, file) => sum + (file.recordsCount || 0), 0);
|
|
720
|
-
this.logger.
|
|
722
|
+
this.logger.silly?.(
|
|
721
723
|
`[FileDatabase] Updated metadata for file ${currentFile.fileName}: recordsCount=${recordsCount}, totalRecords=${this.metadata.totalRecords}`
|
|
722
724
|
);
|
|
723
725
|
}
|
|
@@ -741,7 +743,7 @@ var FileDatabase = class {
|
|
|
741
743
|
}
|
|
742
744
|
try {
|
|
743
745
|
await import_fs3.default.promises.writeFile(filePath, serializedData, "utf8");
|
|
744
|
-
this.logger.
|
|
746
|
+
this.logger.silly?.(`[FileDatabase] Wrote ${bytesToHumanReadable(requiredBytes)} to ${filePath}`);
|
|
745
747
|
} catch (error) {
|
|
746
748
|
throw new FileDatabaseError(`Failed to write file ${filePath}: ${error.message}`);
|
|
747
749
|
}
|
|
@@ -823,7 +825,8 @@ var FileDatabase = class {
|
|
|
823
825
|
this.useMetadata = format.hasMetadata;
|
|
824
826
|
}
|
|
825
827
|
if (this.useMetadata) {
|
|
826
|
-
const
|
|
828
|
+
const destPath = this.getDestinationPath();
|
|
829
|
+
const metadataPath = import_path3.default.join(destPath, "metadata.json");
|
|
827
830
|
if (import_fs3.default.existsSync(metadataPath)) {
|
|
828
831
|
try {
|
|
829
832
|
const rawData = await import_fs3.default.promises.readFile(metadataPath, "utf8");
|
|
@@ -837,7 +840,9 @@ var FileDatabase = class {
|
|
|
837
840
|
throw new FileDatabaseError(`Failed to read metadata: ${e.message}`);
|
|
838
841
|
}
|
|
839
842
|
} else {
|
|
840
|
-
throw new FileDatabaseError(
|
|
843
|
+
throw new FileDatabaseError(
|
|
844
|
+
`[FileDatabase] No metadata found in non-versioned mode. Looked for: ${metadataPath} (table path: ${destPath})`
|
|
845
|
+
);
|
|
841
846
|
}
|
|
842
847
|
} else {
|
|
843
848
|
this.metadata = await this.figureMetadataFromVersionFiles("");
|
|
@@ -854,6 +859,13 @@ var FileDatabase = class {
|
|
|
854
859
|
* Write data to the file database
|
|
855
860
|
*/
|
|
856
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
|
+
}
|
|
857
869
|
if (options.forceNewVersion && !this.versioned) {
|
|
858
870
|
throw new FileDatabaseError("Cannot use forceNewVersion in non-versioned mode");
|
|
859
871
|
}
|
|
@@ -877,17 +889,17 @@ var FileDatabase = class {
|
|
|
877
889
|
});
|
|
878
890
|
if (matches) {
|
|
879
891
|
targetFileIndex = i;
|
|
880
|
-
this.logger.
|
|
892
|
+
this.logger.silly?.(`[FileDatabase] Found existing file with matching custom metadata: ${fileEntry.fileName}, metadata: ${JSON.stringify(options.customMetadata)}`);
|
|
881
893
|
break;
|
|
882
894
|
} else {
|
|
883
|
-
this.logger.
|
|
895
|
+
this.logger.silly?.(`[FileDatabase] File ${fileEntry.fileName} does not match custom metadata: ${JSON.stringify(options.customMetadata)}`);
|
|
884
896
|
}
|
|
885
897
|
}
|
|
886
898
|
if (targetFileIndex === null) {
|
|
887
|
-
this.logger.
|
|
899
|
+
this.logger.silly?.(`[FileDatabase] No existing file found with custom metadata: ${JSON.stringify(options.customMetadata)}, will create new file`);
|
|
888
900
|
}
|
|
889
901
|
} else {
|
|
890
|
-
this.logger.
|
|
902
|
+
this.logger.silly?.(`[FileDatabase] No custom metadata provided, will create new file`);
|
|
891
903
|
}
|
|
892
904
|
if (targetFileIndex !== null) {
|
|
893
905
|
const targetFile = this.metadata.files[targetFileIndex];
|
|
@@ -916,7 +928,17 @@ var FileDatabase = class {
|
|
|
916
928
|
* Read data from the file database
|
|
917
929
|
*/
|
|
918
930
|
async read(options = {}) {
|
|
919
|
-
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
|
+
}
|
|
920
942
|
await this.prepare({ read: true, version });
|
|
921
943
|
const isNonPaginatedData = this.metadata.dataType === "text" || this.metadata.dataType === "xml" || this.metadata.dataType === "json-object";
|
|
922
944
|
if (isNonPaginatedData) {
|
|
@@ -997,6 +1019,67 @@ var FileDatabase = class {
|
|
|
997
1019
|
this.currentRecord = 0;
|
|
998
1020
|
this.hasReadFirstPage = false;
|
|
999
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
|
+
}
|
|
1000
1083
|
/**
|
|
1001
1084
|
* Set file-level synopsis calculation function
|
|
1002
1085
|
*/
|
|
@@ -1080,335 +1163,106 @@ var FileDatabase = class {
|
|
|
1080
1163
|
}
|
|
1081
1164
|
};
|
|
1082
1165
|
|
|
1083
|
-
// src/mock-server/
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
return `[md5:${(0, import_crypto.createHash)("md5").update(value).digest("hex")}]`;
|
|
1092
|
-
}
|
|
1093
|
-
function sanitizeUrlEncodedString(input, keysToSanitize) {
|
|
1094
|
-
const parsed = (0, import_querystring.parse)(input);
|
|
1095
|
-
const sanitized = sanitizeObject(parsed, keysToSanitize);
|
|
1096
|
-
return (0, import_querystring.stringify)(sanitized);
|
|
1097
|
-
}
|
|
1098
|
-
function sanitizeObject(obj, keysToSanitize) {
|
|
1099
|
-
if (Array.isArray(obj)) {
|
|
1100
|
-
return obj;
|
|
1101
|
-
}
|
|
1102
|
-
const result = {};
|
|
1103
|
-
for (const [key, value] of Object.entries(obj || {})) {
|
|
1104
|
-
if (keysToSanitize.includes(key.toLowerCase())) {
|
|
1105
|
-
result[key] = maskValue2(String(value));
|
|
1106
|
-
} else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
1107
|
-
result[key] = sanitizeObject(value, keysToSanitize);
|
|
1108
|
-
} else {
|
|
1109
|
-
result[key] = value;
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1112
|
-
return result;
|
|
1113
|
-
}
|
|
1114
|
-
function sanitizeHeaders(headers, additionalKeys = []) {
|
|
1115
|
-
const sensitiveKeys = ["authorization", "x-api-key", "api-key", "bearer", ...additionalKeys.map((k) => k.toLowerCase())];
|
|
1116
|
-
const sanitized = { ...headers };
|
|
1117
|
-
for (const [key, value] of Object.entries(sanitized)) {
|
|
1118
|
-
if (sensitiveKeys.includes(key.toLowerCase())) {
|
|
1119
|
-
sanitized[key] = maskValue2(value);
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
1122
|
-
return sanitized;
|
|
1123
|
-
}
|
|
1124
|
-
function sanitizeRequestData(data, keysToSanitize) {
|
|
1125
|
-
if (typeof data === "string") {
|
|
1126
|
-
if (data.includes("=")) {
|
|
1127
|
-
try {
|
|
1128
|
-
const parsed = (0, import_querystring.parse)(data);
|
|
1129
|
-
return (0, import_querystring.stringify)(sanitizeObject(parsed, keysToSanitize));
|
|
1130
|
-
} catch {
|
|
1131
|
-
return data;
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
1134
|
-
return data;
|
|
1135
|
-
} else if (typeof data === "object" && data !== null) {
|
|
1136
|
-
return sanitizeObject(data, keysToSanitize);
|
|
1137
|
-
}
|
|
1138
|
-
return data;
|
|
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(",") + "}";
|
|
1139
1174
|
}
|
|
1140
|
-
function
|
|
1141
|
-
if (
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
return
|
|
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();
|
|
1145
1180
|
}
|
|
1146
|
-
function
|
|
1147
|
-
const { query, requestData, headers = {}, data } = params;
|
|
1181
|
+
function buildCriteria(method, host, pathname, query, requestData) {
|
|
1148
1182
|
return {
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1183
|
+
method: method.toUpperCase(),
|
|
1184
|
+
host,
|
|
1185
|
+
pathname: pathname || "/",
|
|
1186
|
+
query: normalizeQuery(query),
|
|
1187
|
+
requestBody: requestData != null ? stableStringify(requestData) : ""
|
|
1153
1188
|
};
|
|
1154
1189
|
}
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
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 {
|
|
1158
1196
|
fileDb;
|
|
1159
|
-
catalogPath;
|
|
1160
1197
|
logger;
|
|
1161
|
-
constructor(
|
|
1162
|
-
this.
|
|
1163
|
-
this.
|
|
1164
|
-
|
|
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
|
+
});
|
|
1165
1208
|
}
|
|
1166
1209
|
/**
|
|
1167
|
-
* Store a mock response
|
|
1210
|
+
* Store a mock response.
|
|
1168
1211
|
*/
|
|
1169
|
-
async
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
query: url.search.slice(1),
|
|
1174
|
-
requestData,
|
|
1175
|
-
headers: {},
|
|
1176
|
-
data: responseData.data
|
|
1177
|
-
}, sensitiveKeys);
|
|
1178
|
-
const entry = {
|
|
1179
|
-
method: "GET",
|
|
1180
|
-
// Will be passed from caller
|
|
1181
|
-
host: url.host,
|
|
1182
|
-
pathname: url.pathname,
|
|
1183
|
-
query: sanitized.query,
|
|
1184
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1185
|
-
...sanitized.requestData !== void 0 ? { requestData: sanitized.requestData } : {},
|
|
1186
|
-
...operationId ? { operationId } : {},
|
|
1187
|
-
...mockName ? { mockName } : {}
|
|
1188
|
-
};
|
|
1189
|
-
const filename = this.generateFilename(entry);
|
|
1190
|
-
const catalogFilePath = path4.join(this.catalogPath, `${filename}.json`);
|
|
1191
|
-
const responseFilename = `response_${filename}.json`;
|
|
1192
|
-
await this.fileDb.write(responseData, { filename: responseFilename });
|
|
1193
|
-
entry.file = responseFilename;
|
|
1194
|
-
await fs4.mkdir(path4.dirname(catalogFilePath), { recursive: true });
|
|
1195
|
-
await fs4.writeFile(catalogFilePath, JSON.stringify(entry, null, 2), "utf-8");
|
|
1196
|
-
return filename;
|
|
1197
|
-
} catch (error) {
|
|
1198
|
-
this.logger.error?.("Error storing mock:", error);
|
|
1199
|
-
throw error;
|
|
1200
|
-
}
|
|
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 });
|
|
1201
1216
|
}
|
|
1202
1217
|
/**
|
|
1203
|
-
* Find mock response
|
|
1218
|
+
* Find a mock response by request criteria. Returns null if not found.
|
|
1204
1219
|
*/
|
|
1205
|
-
async
|
|
1220
|
+
async find(method, host, pathname, query, requestData) {
|
|
1221
|
+
const criteria = buildCriteria(method, host, pathname, query, requestData);
|
|
1206
1222
|
try {
|
|
1207
|
-
const
|
|
1208
|
-
if (
|
|
1209
|
-
|
|
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;
|
|
1210
1229
|
}
|
|
1211
|
-
|
|
1212
|
-
if (fuzzyMatch) {
|
|
1213
|
-
return fuzzyMatch;
|
|
1214
|
-
}
|
|
1215
|
-
return null;
|
|
1216
|
-
} catch (error) {
|
|
1217
|
-
this.logger.error?.("Error finding mock:", error);
|
|
1218
|
-
return null;
|
|
1230
|
+
throw err;
|
|
1219
1231
|
}
|
|
1220
1232
|
}
|
|
1221
1233
|
/**
|
|
1222
|
-
*
|
|
1234
|
+
* List all stored mock keys (opaque identifiers for remove)
|
|
1223
1235
|
*/
|
|
1224
|
-
async
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
for (const entry of entries) {
|
|
1228
|
-
if (this.matchesCriteria(entry, criteria)) {
|
|
1229
|
-
return await this.loadResponseData(entry.file);
|
|
1230
|
-
}
|
|
1231
|
-
}
|
|
1232
|
-
return null;
|
|
1233
|
-
} catch (error) {
|
|
1234
|
-
this.logger.error?.("Error in exact match:", error);
|
|
1235
|
-
return null;
|
|
1236
|
-
}
|
|
1236
|
+
async listKeys() {
|
|
1237
|
+
const files = await this.fileDb.listFilenames();
|
|
1238
|
+
return files.sort();
|
|
1237
1239
|
}
|
|
1238
1240
|
/**
|
|
1239
|
-
*
|
|
1241
|
+
* Remove a mock by key (from listKeys)
|
|
1240
1242
|
*/
|
|
1241
|
-
async
|
|
1243
|
+
async remove(fileName) {
|
|
1244
|
+
const name = fileName.endsWith(".json") ? fileName : `${fileName}.json`;
|
|
1242
1245
|
try {
|
|
1243
|
-
|
|
1244
|
-
...criteria,
|
|
1245
|
-
query: this.stripCommonParams(criteria.query)
|
|
1246
|
-
};
|
|
1247
|
-
return this.findExactMatch(fuzzyCriteria);
|
|
1248
|
-
} catch (error) {
|
|
1249
|
-
this.logger.error?.("Error in fuzzy match:", error);
|
|
1250
|
-
return null;
|
|
1251
|
-
}
|
|
1252
|
-
}
|
|
1253
|
-
/**
|
|
1254
|
-
* Check if a catalog entry matches the request criteria
|
|
1255
|
-
*/
|
|
1256
|
-
matchesCriteria(entry, criteria) {
|
|
1257
|
-
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);
|
|
1258
|
-
}
|
|
1259
|
-
/**
|
|
1260
|
-
* Check if query strings match (with sanitization)
|
|
1261
|
-
*/
|
|
1262
|
-
queriesMatch(storedQuery, requestQuery) {
|
|
1263
|
-
return storedQuery === requestQuery;
|
|
1264
|
-
}
|
|
1265
|
-
/**
|
|
1266
|
-
* Check if request data matches
|
|
1267
|
-
*/
|
|
1268
|
-
requestDataMatches(storedData, requestData) {
|
|
1269
|
-
if (!storedData && !requestData) {
|
|
1246
|
+
await this.fileDb.removeFileEntry(name);
|
|
1270
1247
|
return true;
|
|
1271
|
-
}
|
|
1272
|
-
if (!storedData || !requestData) {
|
|
1248
|
+
} catch {
|
|
1273
1249
|
return false;
|
|
1274
1250
|
}
|
|
1275
|
-
return JSON.stringify(storedData) === JSON.stringify(requestData);
|
|
1276
|
-
}
|
|
1277
|
-
/**
|
|
1278
|
-
* Load response data from file
|
|
1279
|
-
*/
|
|
1280
|
-
async loadResponseData(filename) {
|
|
1281
|
-
try {
|
|
1282
|
-
const data = await this.fileDb.read({ filename });
|
|
1283
|
-
return data;
|
|
1284
|
-
} catch (error) {
|
|
1285
|
-
this.logger.error?.("Error loading response data:", error);
|
|
1286
|
-
return null;
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1289
|
-
/**
|
|
1290
|
-
* Strip common query parameters for fuzzy matching
|
|
1291
|
-
*/
|
|
1292
|
-
stripCommonParams(query) {
|
|
1293
|
-
const commonParams = ["timestamp", "nonce", "cache", "_"];
|
|
1294
|
-
const params = new URLSearchParams(query);
|
|
1295
|
-
for (const param of commonParams) {
|
|
1296
|
-
params.delete(param);
|
|
1297
|
-
}
|
|
1298
|
-
return params.toString();
|
|
1299
|
-
}
|
|
1300
|
-
/**
|
|
1301
|
-
* Generate unique filename for catalog entry
|
|
1302
|
-
*/
|
|
1303
|
-
generateFilename(entry) {
|
|
1304
|
-
const timestamp = Date.now();
|
|
1305
|
-
const hash = this.simpleHash(`${entry.method}${entry.host}${entry.pathname}${entry.query}${entry.operationId || ""}`);
|
|
1306
|
-
return `mock_${timestamp}_${hash}`;
|
|
1307
|
-
}
|
|
1308
|
-
/**
|
|
1309
|
-
* Simple hash function for filename generation
|
|
1310
|
-
*/
|
|
1311
|
-
simpleHash(str) {
|
|
1312
|
-
let hash = 0;
|
|
1313
|
-
for (let i = 0; i < str.length; i++) {
|
|
1314
|
-
const char = str.charCodeAt(i);
|
|
1315
|
-
hash = (hash << 5) - hash + char;
|
|
1316
|
-
hash = hash & hash;
|
|
1317
|
-
}
|
|
1318
|
-
return Math.abs(hash).toString(36);
|
|
1319
1251
|
}
|
|
1320
1252
|
/**
|
|
1321
|
-
*
|
|
1253
|
+
* Remove a mock by request criteria (method, host, pathname, query, requestData)
|
|
1322
1254
|
*/
|
|
1323
|
-
async
|
|
1255
|
+
async removeByCriteria(method, host, pathname, query, requestData) {
|
|
1256
|
+
const criteria = buildCriteria(method, host, pathname, query, requestData);
|
|
1324
1257
|
try {
|
|
1325
|
-
const
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
for (const file of jsonFiles) {
|
|
1329
|
-
try {
|
|
1330
|
-
const filePath = path4.join(this.catalogPath, file);
|
|
1331
|
-
const content = await fs4.readFile(filePath, "utf-8");
|
|
1332
|
-
const entry = JSON.parse(content);
|
|
1333
|
-
entries.push(entry);
|
|
1334
|
-
} catch (error) {
|
|
1335
|
-
this.logger.warn?.(`Error reading catalog entry ${file}:`, error);
|
|
1336
|
-
}
|
|
1337
|
-
}
|
|
1338
|
-
return entries;
|
|
1339
|
-
} catch (error) {
|
|
1340
|
-
this.logger.error?.("Error listing catalog entries:", error);
|
|
1341
|
-
return [];
|
|
1342
|
-
}
|
|
1343
|
-
}
|
|
1344
|
-
/**
|
|
1345
|
-
* Remove a catalog entry and its response data
|
|
1346
|
-
*/
|
|
1347
|
-
async removeEntry(filename) {
|
|
1348
|
-
try {
|
|
1349
|
-
const catalogFilePath = path4.join(this.catalogPath, `${filename}.json`);
|
|
1350
|
-
const responseFilename = `response_${filename}.json`;
|
|
1351
|
-
try {
|
|
1352
|
-
await fs4.unlink(catalogFilePath);
|
|
1353
|
-
} catch (error) {
|
|
1354
|
-
this.logger.warn?.(`Could not remove catalog file ${catalogFilePath}:`, error);
|
|
1355
|
-
}
|
|
1356
|
-
try {
|
|
1357
|
-
const responseFilePath = path4.join(this.catalogPath, responseFilename);
|
|
1358
|
-
await fs4.unlink(responseFilePath);
|
|
1359
|
-
} catch (error) {
|
|
1360
|
-
this.logger.warn?.(`Could not remove response file ${responseFilename}:`, error);
|
|
1361
|
-
}
|
|
1258
|
+
const results = await this.fileDb.findData(criteria);
|
|
1259
|
+
if (results.length === 0) return false;
|
|
1260
|
+
await this.fileDb.removeFileEntry(results[0].fileName);
|
|
1362
1261
|
return true;
|
|
1363
|
-
} catch
|
|
1364
|
-
this.logger.error?.("Error removing entry:", error);
|
|
1262
|
+
} catch {
|
|
1365
1263
|
return false;
|
|
1366
1264
|
}
|
|
1367
1265
|
}
|
|
1368
|
-
/**
|
|
1369
|
-
* Clean up orphaned files and invalid catalog entries
|
|
1370
|
-
*/
|
|
1371
|
-
async maintenance() {
|
|
1372
|
-
try {
|
|
1373
|
-
let cleaned = 0;
|
|
1374
|
-
const files = await fs4.readdir(this.catalogPath).catch(() => []);
|
|
1375
|
-
const catalogFiles = files.filter((file) => file.endsWith(".json") && !file.startsWith("response_"));
|
|
1376
|
-
const responseFiles = files.filter((file) => file.startsWith("response_"));
|
|
1377
|
-
for (const catalogFile of catalogFiles) {
|
|
1378
|
-
try {
|
|
1379
|
-
const catalogPath = path4.join(this.catalogPath, catalogFile);
|
|
1380
|
-
const content = await fs4.readFile(catalogPath, "utf-8");
|
|
1381
|
-
const entry = JSON.parse(content);
|
|
1382
|
-
const responseFile = entry.file;
|
|
1383
|
-
const responsePath = path4.join(this.catalogPath, responseFile);
|
|
1384
|
-
try {
|
|
1385
|
-
await fs4.access(responsePath);
|
|
1386
|
-
} catch {
|
|
1387
|
-
await fs4.unlink(catalogPath);
|
|
1388
|
-
cleaned++;
|
|
1389
|
-
this.logger.info?.(`Removed orphaned catalog entry: ${catalogFile}`);
|
|
1390
|
-
}
|
|
1391
|
-
} catch (error) {
|
|
1392
|
-
this.logger.warn?.(`Error processing catalog file ${catalogFile}:`, error);
|
|
1393
|
-
}
|
|
1394
|
-
}
|
|
1395
|
-
const catalogResponseFiles = catalogFiles.map(
|
|
1396
|
-
(file) => `response_${file.replace(".json", "")}.json`
|
|
1397
|
-
);
|
|
1398
|
-
for (const responseFile of responseFiles) {
|
|
1399
|
-
if (!catalogResponseFiles.includes(responseFile)) {
|
|
1400
|
-
const responsePath = path4.join(this.catalogPath, responseFile);
|
|
1401
|
-
await fs4.unlink(responsePath);
|
|
1402
|
-
cleaned++;
|
|
1403
|
-
this.logger.info?.(`Removed orphaned response file: ${responseFile}`);
|
|
1404
|
-
}
|
|
1405
|
-
}
|
|
1406
|
-
return { cleaned };
|
|
1407
|
-
} catch (error) {
|
|
1408
|
-
this.logger.error?.("Error during maintenance:", error);
|
|
1409
|
-
return { cleaned: 0 };
|
|
1410
|
-
}
|
|
1411
|
-
}
|
|
1412
1266
|
};
|
|
1413
1267
|
|
|
1414
1268
|
// src/mock-server/index.ts
|
|
@@ -1427,8 +1281,7 @@ var MockServer = class {
|
|
|
1427
1281
|
config;
|
|
1428
1282
|
app;
|
|
1429
1283
|
server = null;
|
|
1430
|
-
|
|
1431
|
-
catalog;
|
|
1284
|
+
storage;
|
|
1432
1285
|
stats;
|
|
1433
1286
|
startTime;
|
|
1434
1287
|
constructor(config) {
|
|
@@ -1454,15 +1307,7 @@ var MockServer = class {
|
|
|
1454
1307
|
errors: 0,
|
|
1455
1308
|
uptime: 0
|
|
1456
1309
|
};
|
|
1457
|
-
this.
|
|
1458
|
-
basePath: this.config.basePath,
|
|
1459
|
-
namespace: this.config.namespace,
|
|
1460
|
-
tableName: this.config.tableName,
|
|
1461
|
-
versioned: false,
|
|
1462
|
-
// Mock responses are typically not versioned
|
|
1463
|
-
logger: this.config.logger
|
|
1464
|
-
});
|
|
1465
|
-
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 });
|
|
1466
1311
|
this.setupMiddleware();
|
|
1467
1312
|
this.setupRoutes();
|
|
1468
1313
|
}
|
|
@@ -1605,14 +1450,7 @@ var MockServer = class {
|
|
|
1605
1450
|
const pathname = url.pathname;
|
|
1606
1451
|
const query = url.search.slice(1);
|
|
1607
1452
|
const requestData = this.extractRequestData(req);
|
|
1608
|
-
|
|
1609
|
-
method,
|
|
1610
|
-
host: url.host,
|
|
1611
|
-
pathname,
|
|
1612
|
-
query,
|
|
1613
|
-
requestData
|
|
1614
|
-
};
|
|
1615
|
-
return await this.catalog.findMock(criteria);
|
|
1453
|
+
return await this.storage.find(method, url.host, pathname, query, requestData);
|
|
1616
1454
|
} catch (error) {
|
|
1617
1455
|
this.config.logger.error?.("Error finding mock response:", error);
|
|
1618
1456
|
return null;
|
|
@@ -1633,33 +1471,39 @@ var MockServer = class {
|
|
|
1633
1471
|
/**
|
|
1634
1472
|
* Store a mock response from an HTTP request/response
|
|
1635
1473
|
*/
|
|
1636
|
-
async storeMock(requestUrl, requestData, responseData,
|
|
1637
|
-
|
|
1638
|
-
requestUrl,
|
|
1639
|
-
requestData,
|
|
1640
|
-
responseData,
|
|
1641
|
-
operationId,
|
|
1642
|
-
mockName,
|
|
1643
|
-
this.config.sensitiveKeys
|
|
1644
|
-
);
|
|
1474
|
+
async storeMock(requestUrl, requestData, responseData, method = "GET") {
|
|
1475
|
+
await this.storage.store(method, requestUrl, requestData, responseData);
|
|
1645
1476
|
}
|
|
1646
1477
|
/**
|
|
1647
|
-
* List all stored mock
|
|
1478
|
+
* List all stored mock keys
|
|
1648
1479
|
*/
|
|
1649
1480
|
async listMocks() {
|
|
1650
|
-
return await this.
|
|
1481
|
+
return await this.storage.listKeys();
|
|
1651
1482
|
}
|
|
1652
1483
|
/**
|
|
1653
|
-
* Remove a mock
|
|
1484
|
+
* Remove a mock by key (from listMocks)
|
|
1654
1485
|
*/
|
|
1655
|
-
async removeMock(
|
|
1656
|
-
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
|
+
);
|
|
1657
1501
|
}
|
|
1658
1502
|
/**
|
|
1659
|
-
*
|
|
1503
|
+
* No-op (mocks are self-contained, no orphans)
|
|
1660
1504
|
*/
|
|
1661
1505
|
async maintenance() {
|
|
1662
|
-
return
|
|
1506
|
+
return { cleaned: 0 };
|
|
1663
1507
|
}
|
|
1664
1508
|
/**
|
|
1665
1509
|
* Get current configuration
|
|
@@ -1684,6 +1528,8 @@ async function createMockServer(config) {
|
|
|
1684
1528
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1685
1529
|
0 && (module.exports = {
|
|
1686
1530
|
MockServer,
|
|
1531
|
+
MockStorage,
|
|
1532
|
+
computeMockKey,
|
|
1687
1533
|
createMockServer
|
|
1688
1534
|
});
|
|
1689
1535
|
//# sourceMappingURL=mock-server.cjs.map
|