@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.js
CHANGED
|
@@ -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";
|
|
@@ -124,15 +127,15 @@ var ParamError = class extends FrameworkError {
|
|
|
124
127
|
this.name = "ParamError";
|
|
125
128
|
}
|
|
126
129
|
};
|
|
127
|
-
|
|
128
|
-
// src/filedatabase/index.ts
|
|
129
|
-
var FileDatabaseError = class extends Error {
|
|
130
|
+
var FileDatabaseError = class extends FrameworkError {
|
|
130
131
|
constructor(message) {
|
|
131
132
|
super(message);
|
|
132
133
|
this.name = "FileDatabaseError";
|
|
133
134
|
}
|
|
134
135
|
};
|
|
135
|
-
|
|
136
|
+
|
|
137
|
+
// src/filedatabase/index.ts
|
|
138
|
+
var FileDatabase = class _FileDatabase {
|
|
136
139
|
basePath;
|
|
137
140
|
namespace;
|
|
138
141
|
tableName = null;
|
|
@@ -169,18 +172,8 @@ var FileDatabase = class {
|
|
|
169
172
|
maxVersions: "number default 5",
|
|
170
173
|
pageSize: "number default 5000"
|
|
171
174
|
};
|
|
172
|
-
const
|
|
173
|
-
config = {
|
|
174
|
-
basePath: opts.basePath ?? paramsConfig.basePath,
|
|
175
|
-
namespace: opts.namespace ?? paramsConfig.namespace,
|
|
176
|
-
tableName: opts.tableName ?? paramsConfig.tableName ?? null,
|
|
177
|
-
versioned: opts.versioned ?? true,
|
|
178
|
-
maxVersions: opts.maxVersions ?? paramsConfig.maxVersions,
|
|
179
|
-
pageSize: opts.pageSize ?? paramsConfig.pageSize,
|
|
180
|
-
useMetadata: opts.useMetadata ?? true,
|
|
181
|
-
freeSpaceThreshold: opts.freeSpaceThreshold ?? 100 * 1024 * 1024,
|
|
182
|
-
logger: context.logger
|
|
183
|
-
};
|
|
175
|
+
const discovered = context.params.getAllForModule(defs);
|
|
176
|
+
config = { ...discovered, ...opts, logger: context.logger };
|
|
184
177
|
} else {
|
|
185
178
|
config = contextOrConfig;
|
|
186
179
|
}
|
|
@@ -198,6 +191,13 @@ var FileDatabase = class {
|
|
|
198
191
|
this.logger = config.logger || console;
|
|
199
192
|
this.metadata = this.getDefaultMetadata();
|
|
200
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* Initialize FileDatabase from context and options.
|
|
196
|
+
* Params are read via getAllForModule("filedatabase", defs) for --showUsedParams grouping.
|
|
197
|
+
*/
|
|
198
|
+
static init(context, options) {
|
|
199
|
+
return new _FileDatabase(context, options ?? {});
|
|
200
|
+
}
|
|
201
201
|
/**
|
|
202
202
|
* Get default metadata structure
|
|
203
203
|
*/
|
|
@@ -265,7 +265,7 @@ var FileDatabase = class {
|
|
|
265
265
|
const versions = await this.getVersions();
|
|
266
266
|
while (versions.length > this.maxVersions) {
|
|
267
267
|
const versionToDelete = path3.resolve(this.getDestinationPath(), versions.shift());
|
|
268
|
-
this.logger.
|
|
268
|
+
this.logger.silly?.(`[FileDatabase] Deleting old version: ${versionToDelete}`);
|
|
269
269
|
await fs3.promises.rm(versionToDelete, { recursive: true, force: true });
|
|
270
270
|
}
|
|
271
271
|
return versionName;
|
|
@@ -543,7 +543,7 @@ var FileDatabase = class {
|
|
|
543
543
|
};
|
|
544
544
|
this.metadata.files.push(fileEntry);
|
|
545
545
|
this.lastFileData = null;
|
|
546
|
-
this.logger.
|
|
546
|
+
this.logger.silly?.(`[FileDatabase] Created new file: ${fileEntry.fileName}, fileNumber: ${this.currentFileNumber}`);
|
|
547
547
|
}
|
|
548
548
|
/**
|
|
549
549
|
* Figure out what data to write and which file to use (for pagination)
|
|
@@ -576,7 +576,7 @@ var FileDatabase = class {
|
|
|
576
576
|
const filesBeforeCreate = this.metadata.files.length;
|
|
577
577
|
this.makeNewFile();
|
|
578
578
|
newlyCreatedFileIndex = filesBeforeCreate;
|
|
579
|
-
this.logger.
|
|
579
|
+
this.logger.silly?.(`[FileDatabase] Creating new file for unique custom metadata combination, fileNumber: ${this.currentFileNumber}`);
|
|
580
580
|
} else if (this.metadata.files.length === 0) {
|
|
581
581
|
this.makeNewFile();
|
|
582
582
|
}
|
|
@@ -625,7 +625,7 @@ var FileDatabase = class {
|
|
|
625
625
|
dataLeftOver = null;
|
|
626
626
|
}
|
|
627
627
|
const fileName = this.metadata.files[this.metadata.files.length - 1].fileName;
|
|
628
|
-
this.logger.
|
|
628
|
+
this.logger.silly?.(
|
|
629
629
|
`[FileDatabase] figureOutDataAndFileToWrite: filename=${fileName}, forceNewFile=${forceNewFile}, targetFileIndex=${targetFileIndex}, dataToWrite.length=${Array.isArray(dataToWrite) ? dataToWrite.length : "N/A"}, lastFileRecordsCount=${lastFileRecordsCount}`
|
|
630
630
|
);
|
|
631
631
|
return { dataToWrite, dataLeftOver, fileName };
|
|
@@ -680,7 +680,7 @@ var FileDatabase = class {
|
|
|
680
680
|
this.metadata.modifiedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
681
681
|
this.metadata.dataType = detectDataType(dataToWrite);
|
|
682
682
|
this.metadata.totalRecords = this.metadata.files.reduce((sum, file) => sum + (file.recordsCount || 0), 0);
|
|
683
|
-
this.logger.
|
|
683
|
+
this.logger.silly?.(
|
|
684
684
|
`[FileDatabase] Updated metadata for file ${currentFile.fileName}: recordsCount=${recordsCount}, totalRecords=${this.metadata.totalRecords}`
|
|
685
685
|
);
|
|
686
686
|
}
|
|
@@ -704,7 +704,7 @@ var FileDatabase = class {
|
|
|
704
704
|
}
|
|
705
705
|
try {
|
|
706
706
|
await fs3.promises.writeFile(filePath, serializedData, "utf8");
|
|
707
|
-
this.logger.
|
|
707
|
+
this.logger.silly?.(`[FileDatabase] Wrote ${bytesToHumanReadable(requiredBytes)} to ${filePath}`);
|
|
708
708
|
} catch (error) {
|
|
709
709
|
throw new FileDatabaseError(`Failed to write file ${filePath}: ${error.message}`);
|
|
710
710
|
}
|
|
@@ -786,7 +786,8 @@ var FileDatabase = class {
|
|
|
786
786
|
this.useMetadata = format.hasMetadata;
|
|
787
787
|
}
|
|
788
788
|
if (this.useMetadata) {
|
|
789
|
-
const
|
|
789
|
+
const destPath = this.getDestinationPath();
|
|
790
|
+
const metadataPath = path3.join(destPath, "metadata.json");
|
|
790
791
|
if (fs3.existsSync(metadataPath)) {
|
|
791
792
|
try {
|
|
792
793
|
const rawData = await fs3.promises.readFile(metadataPath, "utf8");
|
|
@@ -800,7 +801,9 @@ var FileDatabase = class {
|
|
|
800
801
|
throw new FileDatabaseError(`Failed to read metadata: ${e.message}`);
|
|
801
802
|
}
|
|
802
803
|
} else {
|
|
803
|
-
throw new FileDatabaseError(
|
|
804
|
+
throw new FileDatabaseError(
|
|
805
|
+
`[FileDatabase] No metadata found in non-versioned mode. Looked for: ${metadataPath} (table path: ${destPath})`
|
|
806
|
+
);
|
|
804
807
|
}
|
|
805
808
|
} else {
|
|
806
809
|
this.metadata = await this.figureMetadataFromVersionFiles("");
|
|
@@ -817,6 +820,13 @@ var FileDatabase = class {
|
|
|
817
820
|
* Write data to the file database
|
|
818
821
|
*/
|
|
819
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
|
+
}
|
|
820
830
|
if (options.forceNewVersion && !this.versioned) {
|
|
821
831
|
throw new FileDatabaseError("Cannot use forceNewVersion in non-versioned mode");
|
|
822
832
|
}
|
|
@@ -840,17 +850,17 @@ var FileDatabase = class {
|
|
|
840
850
|
});
|
|
841
851
|
if (matches) {
|
|
842
852
|
targetFileIndex = i;
|
|
843
|
-
this.logger.
|
|
853
|
+
this.logger.silly?.(`[FileDatabase] Found existing file with matching custom metadata: ${fileEntry.fileName}, metadata: ${JSON.stringify(options.customMetadata)}`);
|
|
844
854
|
break;
|
|
845
855
|
} else {
|
|
846
|
-
this.logger.
|
|
856
|
+
this.logger.silly?.(`[FileDatabase] File ${fileEntry.fileName} does not match custom metadata: ${JSON.stringify(options.customMetadata)}`);
|
|
847
857
|
}
|
|
848
858
|
}
|
|
849
859
|
if (targetFileIndex === null) {
|
|
850
|
-
this.logger.
|
|
860
|
+
this.logger.silly?.(`[FileDatabase] No existing file found with custom metadata: ${JSON.stringify(options.customMetadata)}, will create new file`);
|
|
851
861
|
}
|
|
852
862
|
} else {
|
|
853
|
-
this.logger.
|
|
863
|
+
this.logger.silly?.(`[FileDatabase] No custom metadata provided, will create new file`);
|
|
854
864
|
}
|
|
855
865
|
if (targetFileIndex !== null) {
|
|
856
866
|
const targetFile = this.metadata.files[targetFileIndex];
|
|
@@ -879,7 +889,17 @@ var FileDatabase = class {
|
|
|
879
889
|
* Read data from the file database
|
|
880
890
|
*/
|
|
881
891
|
async read(options = {}) {
|
|
882
|
-
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
|
+
}
|
|
883
903
|
await this.prepare({ read: true, version });
|
|
884
904
|
const isNonPaginatedData = this.metadata.dataType === "text" || this.metadata.dataType === "xml" || this.metadata.dataType === "json-object";
|
|
885
905
|
if (isNonPaginatedData) {
|
|
@@ -960,6 +980,67 @@ var FileDatabase = class {
|
|
|
960
980
|
this.currentRecord = 0;
|
|
961
981
|
this.hasReadFirstPage = false;
|
|
962
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
|
+
}
|
|
963
1044
|
/**
|
|
964
1045
|
* Set file-level synopsis calculation function
|
|
965
1046
|
*/
|
|
@@ -1043,335 +1124,106 @@ var FileDatabase = class {
|
|
|
1043
1124
|
}
|
|
1044
1125
|
};
|
|
1045
1126
|
|
|
1046
|
-
// src/mock-server/
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
return `[md5:${createHash("md5").update(value).digest("hex")}]`;
|
|
1055
|
-
}
|
|
1056
|
-
function sanitizeUrlEncodedString(input, keysToSanitize) {
|
|
1057
|
-
const parsed = parse(input);
|
|
1058
|
-
const sanitized = sanitizeObject(parsed, keysToSanitize);
|
|
1059
|
-
return stringify(sanitized);
|
|
1060
|
-
}
|
|
1061
|
-
function sanitizeObject(obj, keysToSanitize) {
|
|
1062
|
-
if (Array.isArray(obj)) {
|
|
1063
|
-
return obj;
|
|
1064
|
-
}
|
|
1065
|
-
const result = {};
|
|
1066
|
-
for (const [key, value] of Object.entries(obj || {})) {
|
|
1067
|
-
if (keysToSanitize.includes(key.toLowerCase())) {
|
|
1068
|
-
result[key] = maskValue2(String(value));
|
|
1069
|
-
} else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
1070
|
-
result[key] = sanitizeObject(value, keysToSanitize);
|
|
1071
|
-
} else {
|
|
1072
|
-
result[key] = value;
|
|
1073
|
-
}
|
|
1074
|
-
}
|
|
1075
|
-
return result;
|
|
1076
|
-
}
|
|
1077
|
-
function sanitizeHeaders(headers, additionalKeys = []) {
|
|
1078
|
-
const sensitiveKeys = ["authorization", "x-api-key", "api-key", "bearer", ...additionalKeys.map((k) => k.toLowerCase())];
|
|
1079
|
-
const sanitized = { ...headers };
|
|
1080
|
-
for (const [key, value] of Object.entries(sanitized)) {
|
|
1081
|
-
if (sensitiveKeys.includes(key.toLowerCase())) {
|
|
1082
|
-
sanitized[key] = maskValue2(value);
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
return sanitized;
|
|
1086
|
-
}
|
|
1087
|
-
function sanitizeRequestData(data, keysToSanitize) {
|
|
1088
|
-
if (typeof data === "string") {
|
|
1089
|
-
if (data.includes("=")) {
|
|
1090
|
-
try {
|
|
1091
|
-
const parsed = parse(data);
|
|
1092
|
-
return stringify(sanitizeObject(parsed, keysToSanitize));
|
|
1093
|
-
} catch {
|
|
1094
|
-
return data;
|
|
1095
|
-
}
|
|
1096
|
-
}
|
|
1097
|
-
return data;
|
|
1098
|
-
} else if (typeof data === "object" && data !== null) {
|
|
1099
|
-
return sanitizeObject(data, keysToSanitize);
|
|
1100
|
-
}
|
|
1101
|
-
return data;
|
|
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(",") + "}";
|
|
1102
1135
|
}
|
|
1103
|
-
function
|
|
1104
|
-
if (
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
return
|
|
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();
|
|
1108
1141
|
}
|
|
1109
|
-
function
|
|
1110
|
-
const { query, requestData, headers = {}, data } = params;
|
|
1142
|
+
function buildCriteria(method, host, pathname, query, requestData) {
|
|
1111
1143
|
return {
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1144
|
+
method: method.toUpperCase(),
|
|
1145
|
+
host,
|
|
1146
|
+
pathname: pathname || "/",
|
|
1147
|
+
query: normalizeQuery(query),
|
|
1148
|
+
requestBody: requestData != null ? stableStringify(requestData) : ""
|
|
1116
1149
|
};
|
|
1117
1150
|
}
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
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 {
|
|
1121
1157
|
fileDb;
|
|
1122
|
-
catalogPath;
|
|
1123
1158
|
logger;
|
|
1124
|
-
constructor(
|
|
1125
|
-
this.
|
|
1126
|
-
this.
|
|
1127
|
-
|
|
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
|
+
});
|
|
1128
1169
|
}
|
|
1129
1170
|
/**
|
|
1130
|
-
* Store a mock response
|
|
1171
|
+
* Store a mock response.
|
|
1131
1172
|
*/
|
|
1132
|
-
async
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
query: url.search.slice(1),
|
|
1137
|
-
requestData,
|
|
1138
|
-
headers: {},
|
|
1139
|
-
data: responseData.data
|
|
1140
|
-
}, sensitiveKeys);
|
|
1141
|
-
const entry = {
|
|
1142
|
-
method: "GET",
|
|
1143
|
-
// Will be passed from caller
|
|
1144
|
-
host: url.host,
|
|
1145
|
-
pathname: url.pathname,
|
|
1146
|
-
query: sanitized.query,
|
|
1147
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1148
|
-
...sanitized.requestData !== void 0 ? { requestData: sanitized.requestData } : {},
|
|
1149
|
-
...operationId ? { operationId } : {},
|
|
1150
|
-
...mockName ? { mockName } : {}
|
|
1151
|
-
};
|
|
1152
|
-
const filename = this.generateFilename(entry);
|
|
1153
|
-
const catalogFilePath = path4.join(this.catalogPath, `${filename}.json`);
|
|
1154
|
-
const responseFilename = `response_${filename}.json`;
|
|
1155
|
-
await this.fileDb.write(responseData, { filename: responseFilename });
|
|
1156
|
-
entry.file = responseFilename;
|
|
1157
|
-
await fs4.mkdir(path4.dirname(catalogFilePath), { recursive: true });
|
|
1158
|
-
await fs4.writeFile(catalogFilePath, JSON.stringify(entry, null, 2), "utf-8");
|
|
1159
|
-
return filename;
|
|
1160
|
-
} catch (error) {
|
|
1161
|
-
this.logger.error?.("Error storing mock:", error);
|
|
1162
|
-
throw error;
|
|
1163
|
-
}
|
|
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 });
|
|
1164
1177
|
}
|
|
1165
1178
|
/**
|
|
1166
|
-
* Find mock response
|
|
1179
|
+
* Find a mock response by request criteria. Returns null if not found.
|
|
1167
1180
|
*/
|
|
1168
|
-
async
|
|
1181
|
+
async find(method, host, pathname, query, requestData) {
|
|
1182
|
+
const criteria = buildCriteria(method, host, pathname, query, requestData);
|
|
1169
1183
|
try {
|
|
1170
|
-
const
|
|
1171
|
-
if (
|
|
1172
|
-
|
|
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;
|
|
1173
1190
|
}
|
|
1174
|
-
|
|
1175
|
-
if (fuzzyMatch) {
|
|
1176
|
-
return fuzzyMatch;
|
|
1177
|
-
}
|
|
1178
|
-
return null;
|
|
1179
|
-
} catch (error) {
|
|
1180
|
-
this.logger.error?.("Error finding mock:", error);
|
|
1181
|
-
return null;
|
|
1191
|
+
throw err;
|
|
1182
1192
|
}
|
|
1183
1193
|
}
|
|
1184
1194
|
/**
|
|
1185
|
-
*
|
|
1195
|
+
* List all stored mock keys (opaque identifiers for remove)
|
|
1186
1196
|
*/
|
|
1187
|
-
async
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
for (const entry of entries) {
|
|
1191
|
-
if (this.matchesCriteria(entry, criteria)) {
|
|
1192
|
-
return await this.loadResponseData(entry.file);
|
|
1193
|
-
}
|
|
1194
|
-
}
|
|
1195
|
-
return null;
|
|
1196
|
-
} catch (error) {
|
|
1197
|
-
this.logger.error?.("Error in exact match:", error);
|
|
1198
|
-
return null;
|
|
1199
|
-
}
|
|
1197
|
+
async listKeys() {
|
|
1198
|
+
const files = await this.fileDb.listFilenames();
|
|
1199
|
+
return files.sort();
|
|
1200
1200
|
}
|
|
1201
1201
|
/**
|
|
1202
|
-
*
|
|
1202
|
+
* Remove a mock by key (from listKeys)
|
|
1203
1203
|
*/
|
|
1204
|
-
async
|
|
1204
|
+
async remove(fileName) {
|
|
1205
|
+
const name = fileName.endsWith(".json") ? fileName : `${fileName}.json`;
|
|
1205
1206
|
try {
|
|
1206
|
-
|
|
1207
|
-
...criteria,
|
|
1208
|
-
query: this.stripCommonParams(criteria.query)
|
|
1209
|
-
};
|
|
1210
|
-
return this.findExactMatch(fuzzyCriteria);
|
|
1211
|
-
} catch (error) {
|
|
1212
|
-
this.logger.error?.("Error in fuzzy match:", error);
|
|
1213
|
-
return null;
|
|
1214
|
-
}
|
|
1215
|
-
}
|
|
1216
|
-
/**
|
|
1217
|
-
* Check if a catalog entry matches the request criteria
|
|
1218
|
-
*/
|
|
1219
|
-
matchesCriteria(entry, criteria) {
|
|
1220
|
-
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);
|
|
1221
|
-
}
|
|
1222
|
-
/**
|
|
1223
|
-
* Check if query strings match (with sanitization)
|
|
1224
|
-
*/
|
|
1225
|
-
queriesMatch(storedQuery, requestQuery) {
|
|
1226
|
-
return storedQuery === requestQuery;
|
|
1227
|
-
}
|
|
1228
|
-
/**
|
|
1229
|
-
* Check if request data matches
|
|
1230
|
-
*/
|
|
1231
|
-
requestDataMatches(storedData, requestData) {
|
|
1232
|
-
if (!storedData && !requestData) {
|
|
1207
|
+
await this.fileDb.removeFileEntry(name);
|
|
1233
1208
|
return true;
|
|
1234
|
-
}
|
|
1235
|
-
if (!storedData || !requestData) {
|
|
1209
|
+
} catch {
|
|
1236
1210
|
return false;
|
|
1237
1211
|
}
|
|
1238
|
-
return JSON.stringify(storedData) === JSON.stringify(requestData);
|
|
1239
|
-
}
|
|
1240
|
-
/**
|
|
1241
|
-
* Load response data from file
|
|
1242
|
-
*/
|
|
1243
|
-
async loadResponseData(filename) {
|
|
1244
|
-
try {
|
|
1245
|
-
const data = await this.fileDb.read({ filename });
|
|
1246
|
-
return data;
|
|
1247
|
-
} catch (error) {
|
|
1248
|
-
this.logger.error?.("Error loading response data:", error);
|
|
1249
|
-
return null;
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
|
-
/**
|
|
1253
|
-
* Strip common query parameters for fuzzy matching
|
|
1254
|
-
*/
|
|
1255
|
-
stripCommonParams(query) {
|
|
1256
|
-
const commonParams = ["timestamp", "nonce", "cache", "_"];
|
|
1257
|
-
const params = new URLSearchParams(query);
|
|
1258
|
-
for (const param of commonParams) {
|
|
1259
|
-
params.delete(param);
|
|
1260
|
-
}
|
|
1261
|
-
return params.toString();
|
|
1262
|
-
}
|
|
1263
|
-
/**
|
|
1264
|
-
* Generate unique filename for catalog entry
|
|
1265
|
-
*/
|
|
1266
|
-
generateFilename(entry) {
|
|
1267
|
-
const timestamp = Date.now();
|
|
1268
|
-
const hash = this.simpleHash(`${entry.method}${entry.host}${entry.pathname}${entry.query}${entry.operationId || ""}`);
|
|
1269
|
-
return `mock_${timestamp}_${hash}`;
|
|
1270
|
-
}
|
|
1271
|
-
/**
|
|
1272
|
-
* Simple hash function for filename generation
|
|
1273
|
-
*/
|
|
1274
|
-
simpleHash(str) {
|
|
1275
|
-
let hash = 0;
|
|
1276
|
-
for (let i = 0; i < str.length; i++) {
|
|
1277
|
-
const char = str.charCodeAt(i);
|
|
1278
|
-
hash = (hash << 5) - hash + char;
|
|
1279
|
-
hash = hash & hash;
|
|
1280
|
-
}
|
|
1281
|
-
return Math.abs(hash).toString(36);
|
|
1282
1212
|
}
|
|
1283
1213
|
/**
|
|
1284
|
-
*
|
|
1214
|
+
* Remove a mock by request criteria (method, host, pathname, query, requestData)
|
|
1285
1215
|
*/
|
|
1286
|
-
async
|
|
1216
|
+
async removeByCriteria(method, host, pathname, query, requestData) {
|
|
1217
|
+
const criteria = buildCriteria(method, host, pathname, query, requestData);
|
|
1287
1218
|
try {
|
|
1288
|
-
const
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
for (const file of jsonFiles) {
|
|
1292
|
-
try {
|
|
1293
|
-
const filePath = path4.join(this.catalogPath, file);
|
|
1294
|
-
const content = await fs4.readFile(filePath, "utf-8");
|
|
1295
|
-
const entry = JSON.parse(content);
|
|
1296
|
-
entries.push(entry);
|
|
1297
|
-
} catch (error) {
|
|
1298
|
-
this.logger.warn?.(`Error reading catalog entry ${file}:`, error);
|
|
1299
|
-
}
|
|
1300
|
-
}
|
|
1301
|
-
return entries;
|
|
1302
|
-
} catch (error) {
|
|
1303
|
-
this.logger.error?.("Error listing catalog entries:", error);
|
|
1304
|
-
return [];
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
/**
|
|
1308
|
-
* Remove a catalog entry and its response data
|
|
1309
|
-
*/
|
|
1310
|
-
async removeEntry(filename) {
|
|
1311
|
-
try {
|
|
1312
|
-
const catalogFilePath = path4.join(this.catalogPath, `${filename}.json`);
|
|
1313
|
-
const responseFilename = `response_${filename}.json`;
|
|
1314
|
-
try {
|
|
1315
|
-
await fs4.unlink(catalogFilePath);
|
|
1316
|
-
} catch (error) {
|
|
1317
|
-
this.logger.warn?.(`Could not remove catalog file ${catalogFilePath}:`, error);
|
|
1318
|
-
}
|
|
1319
|
-
try {
|
|
1320
|
-
const responseFilePath = path4.join(this.catalogPath, responseFilename);
|
|
1321
|
-
await fs4.unlink(responseFilePath);
|
|
1322
|
-
} catch (error) {
|
|
1323
|
-
this.logger.warn?.(`Could not remove response file ${responseFilename}:`, error);
|
|
1324
|
-
}
|
|
1219
|
+
const results = await this.fileDb.findData(criteria);
|
|
1220
|
+
if (results.length === 0) return false;
|
|
1221
|
+
await this.fileDb.removeFileEntry(results[0].fileName);
|
|
1325
1222
|
return true;
|
|
1326
|
-
} catch
|
|
1327
|
-
this.logger.error?.("Error removing entry:", error);
|
|
1223
|
+
} catch {
|
|
1328
1224
|
return false;
|
|
1329
1225
|
}
|
|
1330
1226
|
}
|
|
1331
|
-
/**
|
|
1332
|
-
* Clean up orphaned files and invalid catalog entries
|
|
1333
|
-
*/
|
|
1334
|
-
async maintenance() {
|
|
1335
|
-
try {
|
|
1336
|
-
let cleaned = 0;
|
|
1337
|
-
const files = await fs4.readdir(this.catalogPath).catch(() => []);
|
|
1338
|
-
const catalogFiles = files.filter((file) => file.endsWith(".json") && !file.startsWith("response_"));
|
|
1339
|
-
const responseFiles = files.filter((file) => file.startsWith("response_"));
|
|
1340
|
-
for (const catalogFile of catalogFiles) {
|
|
1341
|
-
try {
|
|
1342
|
-
const catalogPath = path4.join(this.catalogPath, catalogFile);
|
|
1343
|
-
const content = await fs4.readFile(catalogPath, "utf-8");
|
|
1344
|
-
const entry = JSON.parse(content);
|
|
1345
|
-
const responseFile = entry.file;
|
|
1346
|
-
const responsePath = path4.join(this.catalogPath, responseFile);
|
|
1347
|
-
try {
|
|
1348
|
-
await fs4.access(responsePath);
|
|
1349
|
-
} catch {
|
|
1350
|
-
await fs4.unlink(catalogPath);
|
|
1351
|
-
cleaned++;
|
|
1352
|
-
this.logger.info?.(`Removed orphaned catalog entry: ${catalogFile}`);
|
|
1353
|
-
}
|
|
1354
|
-
} catch (error) {
|
|
1355
|
-
this.logger.warn?.(`Error processing catalog file ${catalogFile}:`, error);
|
|
1356
|
-
}
|
|
1357
|
-
}
|
|
1358
|
-
const catalogResponseFiles = catalogFiles.map(
|
|
1359
|
-
(file) => `response_${file.replace(".json", "")}.json`
|
|
1360
|
-
);
|
|
1361
|
-
for (const responseFile of responseFiles) {
|
|
1362
|
-
if (!catalogResponseFiles.includes(responseFile)) {
|
|
1363
|
-
const responsePath = path4.join(this.catalogPath, responseFile);
|
|
1364
|
-
await fs4.unlink(responsePath);
|
|
1365
|
-
cleaned++;
|
|
1366
|
-
this.logger.info?.(`Removed orphaned response file: ${responseFile}`);
|
|
1367
|
-
}
|
|
1368
|
-
}
|
|
1369
|
-
return { cleaned };
|
|
1370
|
-
} catch (error) {
|
|
1371
|
-
this.logger.error?.("Error during maintenance:", error);
|
|
1372
|
-
return { cleaned: 0 };
|
|
1373
|
-
}
|
|
1374
|
-
}
|
|
1375
1227
|
};
|
|
1376
1228
|
|
|
1377
1229
|
// src/mock-server/index.ts
|
|
@@ -1390,8 +1242,7 @@ var MockServer = class {
|
|
|
1390
1242
|
config;
|
|
1391
1243
|
app;
|
|
1392
1244
|
server = null;
|
|
1393
|
-
|
|
1394
|
-
catalog;
|
|
1245
|
+
storage;
|
|
1395
1246
|
stats;
|
|
1396
1247
|
startTime;
|
|
1397
1248
|
constructor(config) {
|
|
@@ -1417,15 +1268,7 @@ var MockServer = class {
|
|
|
1417
1268
|
errors: 0,
|
|
1418
1269
|
uptime: 0
|
|
1419
1270
|
};
|
|
1420
|
-
this.
|
|
1421
|
-
basePath: this.config.basePath,
|
|
1422
|
-
namespace: this.config.namespace,
|
|
1423
|
-
tableName: this.config.tableName,
|
|
1424
|
-
versioned: false,
|
|
1425
|
-
// Mock responses are typically not versioned
|
|
1426
|
-
logger: this.config.logger
|
|
1427
|
-
});
|
|
1428
|
-
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 });
|
|
1429
1272
|
this.setupMiddleware();
|
|
1430
1273
|
this.setupRoutes();
|
|
1431
1274
|
}
|
|
@@ -1568,14 +1411,7 @@ var MockServer = class {
|
|
|
1568
1411
|
const pathname = url.pathname;
|
|
1569
1412
|
const query = url.search.slice(1);
|
|
1570
1413
|
const requestData = this.extractRequestData(req);
|
|
1571
|
-
|
|
1572
|
-
method,
|
|
1573
|
-
host: url.host,
|
|
1574
|
-
pathname,
|
|
1575
|
-
query,
|
|
1576
|
-
requestData
|
|
1577
|
-
};
|
|
1578
|
-
return await this.catalog.findMock(criteria);
|
|
1414
|
+
return await this.storage.find(method, url.host, pathname, query, requestData);
|
|
1579
1415
|
} catch (error) {
|
|
1580
1416
|
this.config.logger.error?.("Error finding mock response:", error);
|
|
1581
1417
|
return null;
|
|
@@ -1596,33 +1432,39 @@ var MockServer = class {
|
|
|
1596
1432
|
/**
|
|
1597
1433
|
* Store a mock response from an HTTP request/response
|
|
1598
1434
|
*/
|
|
1599
|
-
async storeMock(requestUrl, requestData, responseData,
|
|
1600
|
-
|
|
1601
|
-
requestUrl,
|
|
1602
|
-
requestData,
|
|
1603
|
-
responseData,
|
|
1604
|
-
operationId,
|
|
1605
|
-
mockName,
|
|
1606
|
-
this.config.sensitiveKeys
|
|
1607
|
-
);
|
|
1435
|
+
async storeMock(requestUrl, requestData, responseData, method = "GET") {
|
|
1436
|
+
await this.storage.store(method, requestUrl, requestData, responseData);
|
|
1608
1437
|
}
|
|
1609
1438
|
/**
|
|
1610
|
-
* List all stored mock
|
|
1439
|
+
* List all stored mock keys
|
|
1611
1440
|
*/
|
|
1612
1441
|
async listMocks() {
|
|
1613
|
-
return await this.
|
|
1442
|
+
return await this.storage.listKeys();
|
|
1614
1443
|
}
|
|
1615
1444
|
/**
|
|
1616
|
-
* Remove a mock
|
|
1445
|
+
* Remove a mock by key (from listMocks)
|
|
1617
1446
|
*/
|
|
1618
|
-
async removeMock(
|
|
1619
|
-
return await this.
|
|
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
|
+
);
|
|
1620
1462
|
}
|
|
1621
1463
|
/**
|
|
1622
|
-
*
|
|
1464
|
+
* No-op (mocks are self-contained, no orphans)
|
|
1623
1465
|
*/
|
|
1624
1466
|
async maintenance() {
|
|
1625
|
-
return
|
|
1467
|
+
return { cleaned: 0 };
|
|
1626
1468
|
}
|
|
1627
1469
|
/**
|
|
1628
1470
|
* Get current configuration
|
|
@@ -1646,6 +1488,8 @@ async function createMockServer(config) {
|
|
|
1646
1488
|
}
|
|
1647
1489
|
export {
|
|
1648
1490
|
MockServer,
|
|
1491
|
+
MockStorage,
|
|
1492
|
+
computeMockKey,
|
|
1649
1493
|
createMockServer
|
|
1650
1494
|
};
|
|
1651
1495
|
//# sourceMappingURL=mock-server.js.map
|