adfinem 0.0.0 → 0.1.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/CHANGELOG.md +10 -0
- package/CODE_OF_CONDUCT.md +21 -0
- package/CONTRIBUTING.md +29 -0
- package/LICENSE +21 -0
- package/README.md +86 -2
- package/SECURITY.md +13 -0
- package/catalogs/.gitkeep +0 -0
- package/catalogs/api-operations.yaml +21 -0
- package/catalogs/batches.yaml +74 -0
- package/catalogs/queries.yaml +75 -0
- package/config/environments.yaml +13 -0
- package/dist/actions/assert-db.js +3 -0
- package/dist/actions/run-eod.js +3 -0
- package/dist/adapters/api/api-collections.js +296 -0
- package/dist/adapters/api/body-utils.js +9 -0
- package/dist/adapters/api/rest-client.js +557 -0
- package/dist/adapters/api/soap-client.js +5 -0
- package/dist/adapters/db/assertions.js +87 -0
- package/dist/adapters/db/oracle-client.js +115 -0
- package/dist/adapters/db/query-catalog.js +75 -0
- package/dist/adapters/unix/batch-catalog.js +71 -0
- package/dist/adapters/unix/batch-input-files.js +36 -0
- package/dist/adapters/unix/batch-runner.js +382 -0
- package/dist/adapters/unix/ssh-client.js +228 -0
- package/dist/app/server.js +826 -0
- package/dist/cli.js +465 -0
- package/dist/config/environments.js +138 -0
- package/dist/config/registry.js +18 -0
- package/dist/config/secrets.js +123 -0
- package/dist/dsl/parser.js +20 -0
- package/dist/dsl/schema.js +182 -0
- package/dist/dsl/types.js +1 -0
- package/dist/dsl/validator.js +264 -0
- package/dist/engine/captures.js +68 -0
- package/dist/engine/context.js +69 -0
- package/dist/engine/evidence.js +33 -0
- package/dist/engine/known-errors.js +129 -0
- package/dist/engine/retry.js +13 -0
- package/dist/engine/runner.js +710 -0
- package/dist/engine/step-result.js +58 -0
- package/dist/flows/catalog-normalizer.js +72 -0
- package/dist/flows/compiler.js +237 -0
- package/dist/flows/concat.js +130 -0
- package/dist/flows/parser.js +21 -0
- package/dist/flows/schema.js +142 -0
- package/dist/flows/types.js +1 -0
- package/dist/flows/validator.js +470 -0
- package/dist/reports/html-report.js +112 -0
- package/dist/reports/junit-report.js +48 -0
- package/docs/.gitkeep +0 -0
- package/docs/DB_UNIX_OPERATIONS.md +118 -0
- package/docs/FLOW_BUILDER.md +87 -0
- package/flows/account_processing_cycle.flow.yaml +88 -0
- package/flows/new_flow.flow.yaml +22 -0
- package/package.json +92 -7
- package/scenarios/smoke/account-processing-smoke.yaml +44 -0
- package/scenarios/smoke/api-db-batch-check.yaml +40 -0
- package/src/actions/assert-db.ts +6 -0
- package/src/actions/run-eod.ts +6 -0
- package/src/adapters/api/api-collections.ts +375 -0
- package/src/adapters/api/body-utils.ts +10 -0
- package/src/adapters/api/rest-client.ts +587 -0
- package/src/adapters/api/soap-client.ts +7 -0
- package/src/adapters/db/assertions.ts +83 -0
- package/src/adapters/db/oracle-client.ts +133 -0
- package/src/adapters/db/query-catalog.ts +80 -0
- package/src/adapters/unix/batch-catalog.ts +81 -0
- package/src/adapters/unix/batch-input-files.ts +39 -0
- package/src/adapters/unix/batch-runner.ts +456 -0
- package/src/adapters/unix/ssh-client.ts +248 -0
- package/src/app/server.ts +913 -0
- package/src/cli.ts +466 -0
- package/src/config/environments.ts +193 -0
- package/src/config/registry.ts +23 -0
- package/src/config/secrets.ts +128 -0
- package/src/dsl/parser.ts +24 -0
- package/src/dsl/schema.ts +189 -0
- package/src/dsl/types.ts +371 -0
- package/src/dsl/validator.ts +282 -0
- package/src/engine/captures.ts +66 -0
- package/src/engine/context.ts +76 -0
- package/src/engine/evidence.ts +35 -0
- package/src/engine/known-errors.ts +145 -0
- package/src/engine/retry.ts +11 -0
- package/src/engine/runner.ts +746 -0
- package/src/engine/step-result.ts +64 -0
- package/src/flows/catalog-normalizer.ts +86 -0
- package/src/flows/compiler.ts +247 -0
- package/src/flows/concat.ts +149 -0
- package/src/flows/parser.ts +27 -0
- package/src/flows/schema.ts +154 -0
- package/src/flows/types.ts +130 -0
- package/src/flows/validator.ts +468 -0
- package/src/llm/system-prompt.md +9 -0
- package/src/reports/html-report.ts +113 -0
- package/src/reports/junit-report.ts +55 -0
- package/src/types/oracledb.d.ts +1 -0
- package/templates/.gitkeep +0 -0
- package/templates/api/create-test-case.json +5 -0
- package/templates/api/record-test-activity.json +6 -0
- package/tsconfig.json +15 -0
- package/vite.config.ts +17 -0
- package/web/index.html +12 -0
- package/web/src/App.tsx +6588 -0
- package/web/src/main.tsx +10 -0
- package/web/src/styles.css +3147 -0
- package/index.js +0 -1
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import type { ApiOperationEntry, ApiRequestSpec, CatalogParam } from "../../dsl/types.js";
|
|
5
|
+
|
|
6
|
+
export interface ApiCollectionFile {
|
|
7
|
+
version: 1;
|
|
8
|
+
collections: ImportedApiCollection[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ImportedApiCollection {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
importedAt: string;
|
|
15
|
+
variables?: Record<string, unknown>;
|
|
16
|
+
requestCount: number;
|
|
17
|
+
requests: ImportedApiRequest[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ImportedApiRequest {
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
folderPath: string[];
|
|
24
|
+
operationKey: string;
|
|
25
|
+
description?: string;
|
|
26
|
+
request: ApiRequestSpec;
|
|
27
|
+
variables?: Record<string, unknown>;
|
|
28
|
+
variableNames: string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface PostmanPreview {
|
|
32
|
+
name: string;
|
|
33
|
+
id: string;
|
|
34
|
+
requestCount: number;
|
|
35
|
+
folders: string[];
|
|
36
|
+
variables: Record<string, unknown>;
|
|
37
|
+
requests: Array<Pick<ImportedApiRequest, "id" | "name" | "folderPath" | "operationKey" | "variableNames"> & ApiRequestSpec>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface PostmanCollectionLike {
|
|
41
|
+
info?: { name?: string; _postman_id?: string };
|
|
42
|
+
item?: PostmanItemLike[];
|
|
43
|
+
variable?: Array<{ key?: string; value?: unknown; disabled?: boolean }>;
|
|
44
|
+
auth?: unknown;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface PostmanItemLike {
|
|
48
|
+
name?: string;
|
|
49
|
+
item?: PostmanItemLike[];
|
|
50
|
+
request?: PostmanRequestLike | string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface PostmanRequestLike {
|
|
54
|
+
description?: unknown;
|
|
55
|
+
method?: string;
|
|
56
|
+
url?: unknown;
|
|
57
|
+
header?: Array<{ key?: string; value?: unknown; disabled?: boolean }>;
|
|
58
|
+
body?: {
|
|
59
|
+
mode?: string;
|
|
60
|
+
raw?: string;
|
|
61
|
+
urlencoded?: Array<{ key?: string; value?: unknown; disabled?: boolean }>;
|
|
62
|
+
formdata?: Array<{ key?: string; value?: unknown; disabled?: boolean }>;
|
|
63
|
+
};
|
|
64
|
+
auth?: unknown;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function apiCollectionsPath(rootDir: string): string {
|
|
68
|
+
return join(rootDir, "catalogs", "api-collections.json");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function loadApiCollections(rootDir: string): Promise<ApiCollectionFile> {
|
|
72
|
+
try {
|
|
73
|
+
const parsed = JSON.parse(await readFile(apiCollectionsPath(rootDir), "utf8")) as ApiCollectionFile;
|
|
74
|
+
return normalizeCollectionFile(parsed);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
const err = error as NodeJS.ErrnoException;
|
|
77
|
+
if (err.code === "ENOENT") return { version: 1, collections: [] };
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function writeApiCollections(rootDir: string, file: ApiCollectionFile): Promise<void> {
|
|
83
|
+
const outputPath = apiCollectionsPath(rootDir);
|
|
84
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
85
|
+
await writeFile(outputPath, `${JSON.stringify(normalizeCollectionFile(file), null, 2)}\n`, "utf8");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function importPostmanCollection(rootDir: string, payload: unknown): Promise<ImportedApiCollection> {
|
|
89
|
+
const collection = parsePostmanCollection(payload);
|
|
90
|
+
const file = await loadApiCollections(rootDir);
|
|
91
|
+
const remaining = file.collections.filter((item) => item.id !== collection.id);
|
|
92
|
+
await writeApiCollections(rootDir, { version: 1, collections: [...remaining, collection] });
|
|
93
|
+
return collection;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function parsePostmanCollection(payload: unknown): ImportedApiCollection {
|
|
97
|
+
const source = asObject(payload) as PostmanCollectionLike;
|
|
98
|
+
const name = String(source.info?.name || "Imported API Collection").trim();
|
|
99
|
+
const collectionId = sanitizeId(source.info?._postman_id || `${name}_${shortHash(JSON.stringify(payload))}`);
|
|
100
|
+
const variables = postmanVariables(source.variable);
|
|
101
|
+
const requests: ImportedApiRequest[] = [];
|
|
102
|
+
const rootAuth = source.auth;
|
|
103
|
+
|
|
104
|
+
function visit(items: PostmanItemLike[] | undefined, folderPath: string[], inheritedAuth: unknown): void {
|
|
105
|
+
for (const item of items ?? []) {
|
|
106
|
+
if (Array.isArray(item.item)) {
|
|
107
|
+
visit(item.item, [...folderPath, String(item.name || "Folder")], inheritedAuth);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const request = typeof item.request === "string" ? undefined : item.request;
|
|
111
|
+
if (!request) continue;
|
|
112
|
+
const requestId = uniqueRequestId(collectionId, folderPath, item.name || request.method || "request", requests.length);
|
|
113
|
+
const spec = postmanRequestSpec(request, inheritedAuth);
|
|
114
|
+
const variableNames = [...new Set([
|
|
115
|
+
...findTemplateVariables(spec),
|
|
116
|
+
...Object.keys(variables)
|
|
117
|
+
])].sort((a, b) => a.localeCompare(b));
|
|
118
|
+
requests.push({
|
|
119
|
+
id: requestId,
|
|
120
|
+
name: String(item.name || requestId),
|
|
121
|
+
folderPath,
|
|
122
|
+
operationKey: `pm_${collectionId}_${requestId}`,
|
|
123
|
+
description: descriptionText(request.description),
|
|
124
|
+
request: spec,
|
|
125
|
+
variables,
|
|
126
|
+
variableNames
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
visit(source.item, [], rootAuth);
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
id: collectionId,
|
|
135
|
+
name,
|
|
136
|
+
importedAt: new Date().toISOString(),
|
|
137
|
+
variables,
|
|
138
|
+
requestCount: requests.length,
|
|
139
|
+
requests
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function previewPostmanCollection(payload: unknown): PostmanPreview {
|
|
144
|
+
const collection = parsePostmanCollection(payload);
|
|
145
|
+
return {
|
|
146
|
+
name: collection.name,
|
|
147
|
+
id: collection.id,
|
|
148
|
+
requestCount: collection.requestCount,
|
|
149
|
+
variables: collection.variables ?? {},
|
|
150
|
+
folders: [...new Set(collection.requests.map((request) => request.folderPath.join(" / ")).filter(Boolean))].sort(),
|
|
151
|
+
requests: collection.requests.map((request) => ({
|
|
152
|
+
id: request.id,
|
|
153
|
+
name: request.name,
|
|
154
|
+
folderPath: request.folderPath,
|
|
155
|
+
operationKey: request.operationKey,
|
|
156
|
+
variableNames: request.variableNames,
|
|
157
|
+
...request.request
|
|
158
|
+
}))
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function importedOperationsFromCollections(file: ApiCollectionFile): Record<string, ApiOperationEntry> {
|
|
163
|
+
const operations: Record<string, ApiOperationEntry> = {};
|
|
164
|
+
for (const collection of file.collections) {
|
|
165
|
+
for (const request of collection.requests) {
|
|
166
|
+
operations[request.operationKey] = {
|
|
167
|
+
type: "rest",
|
|
168
|
+
description: request.description || `${collection.name}${request.folderPath.length ? ` / ${request.folderPath.join(" / ")}` : ""} / ${request.name}`,
|
|
169
|
+
...request.request,
|
|
170
|
+
params: paramsFromVariables(request.variableNames),
|
|
171
|
+
source: {
|
|
172
|
+
collectionId: collection.id,
|
|
173
|
+
collectionName: collection.name,
|
|
174
|
+
requestId: request.id,
|
|
175
|
+
folderPath: request.folderPath
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return operations;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function operationToRequestSpec(operation: ApiOperationEntry | undefined): ApiRequestSpec {
|
|
184
|
+
if (!operation) return {};
|
|
185
|
+
return {
|
|
186
|
+
method: operation.method,
|
|
187
|
+
path: operation.path,
|
|
188
|
+
headers: operation.headers,
|
|
189
|
+
query: operation.query,
|
|
190
|
+
body: operation.body,
|
|
191
|
+
rawBody: operation.rawBody,
|
|
192
|
+
bodyMode: operation.bodyMode,
|
|
193
|
+
auth: operation.auth,
|
|
194
|
+
acceptStatuses: operation.acceptStatuses
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function mergeApiRequest(base: ApiOperationEntry, override?: ApiRequestSpec): ApiRequestSpec {
|
|
199
|
+
const source = operationToRequestSpec(base);
|
|
200
|
+
return cleanRequest({
|
|
201
|
+
...source,
|
|
202
|
+
...(override ?? {}),
|
|
203
|
+
headers: mergeRecord(source.headers, override?.headers),
|
|
204
|
+
query: mergeRecord(source.query, override?.query),
|
|
205
|
+
acceptStatuses: override?.acceptStatuses ?? source.acceptStatuses
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function postmanRequestSpec(request: PostmanRequestLike, inheritedAuth: unknown): ApiRequestSpec {
|
|
210
|
+
const url = parsePostmanUrl(request.url);
|
|
211
|
+
const body = parsePostmanBody(request.body);
|
|
212
|
+
return cleanRequest({
|
|
213
|
+
method: normalizeMethod(request.method),
|
|
214
|
+
path: url.path,
|
|
215
|
+
query: url.query,
|
|
216
|
+
headers: mergeRecord(generatedPostmanHeaders(body), keyValueRecord(request.header)),
|
|
217
|
+
auth: request.auth ?? inheritedAuth,
|
|
218
|
+
...body
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function parsePostmanUrl(value: unknown): { path?: string; query?: Record<string, unknown> } {
|
|
223
|
+
if (typeof value === "string") return pathAndQueryFromRaw(value);
|
|
224
|
+
const url = asObject(value);
|
|
225
|
+
const raw = optionalString(url.raw);
|
|
226
|
+
const query = keyValueRecord(url.query as Array<{ key?: string; value?: unknown; disabled?: boolean }> | undefined);
|
|
227
|
+
if (raw) {
|
|
228
|
+
const parsed = pathAndQueryFromRaw(raw);
|
|
229
|
+
return { path: parsed.path, query: mergeRecord(parsed.query, query) };
|
|
230
|
+
}
|
|
231
|
+
const pathParts = Array.isArray(url.path) ? url.path.map(String) : [];
|
|
232
|
+
return {
|
|
233
|
+
path: pathParts.length ? `/${pathParts.join("/")}` : undefined,
|
|
234
|
+
query
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function pathAndQueryFromRaw(raw: string): { path?: string; query?: Record<string, unknown> } {
|
|
239
|
+
const trimmed = raw.trim();
|
|
240
|
+
const withoutBase = trimmed.replace(/^\{\{[^}]+\}\}/, "");
|
|
241
|
+
try {
|
|
242
|
+
const url = new URL(trimmed.replace(/^\{\{[^}]+\}\}/, "http://postman.local"));
|
|
243
|
+
return { path: url.pathname, query: Object.fromEntries(url.searchParams.entries()) };
|
|
244
|
+
} catch {
|
|
245
|
+
const [path, qs] = withoutBase.split("?");
|
|
246
|
+
return {
|
|
247
|
+
path: path.startsWith("/") ? path : `/${path}`,
|
|
248
|
+
query: qs ? Object.fromEntries(new URLSearchParams(qs).entries()) : undefined
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function parsePostmanBody(body: PostmanRequestLike["body"]): Pick<ApiRequestSpec, "body" | "rawBody" | "bodyMode"> {
|
|
254
|
+
if (!body?.mode) return { bodyMode: "none" };
|
|
255
|
+
if (body.mode === "raw") {
|
|
256
|
+
const rawBody = body.raw ?? "";
|
|
257
|
+
const trimmed = rawBody.trim();
|
|
258
|
+
if (trimmed && looksLikeJson(trimmed)) {
|
|
259
|
+
return { bodyMode: "json", rawBody };
|
|
260
|
+
}
|
|
261
|
+
return { bodyMode: "raw", rawBody };
|
|
262
|
+
}
|
|
263
|
+
if (body.mode === "urlencoded") {
|
|
264
|
+
return { bodyMode: "urlencoded", body: keyValueRecord(body.urlencoded) ?? {} };
|
|
265
|
+
}
|
|
266
|
+
if (body.mode === "formdata") {
|
|
267
|
+
return { bodyMode: "formdata", body: keyValueRecord(body.formdata) ?? {} };
|
|
268
|
+
}
|
|
269
|
+
return { bodyMode: "raw", rawBody: body.raw ?? "" };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function normalizeMethod(value: unknown): ApiRequestSpec["method"] {
|
|
273
|
+
const method = String(value || "GET").toUpperCase();
|
|
274
|
+
if (["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"].includes(method)) {
|
|
275
|
+
return method as ApiRequestSpec["method"];
|
|
276
|
+
}
|
|
277
|
+
return "GET";
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function keyValueRecord(items: Array<{ key?: string; value?: unknown; disabled?: boolean }> | undefined): Record<string, string> | undefined {
|
|
281
|
+
const entries = (items ?? [])
|
|
282
|
+
.filter((item) => !item.disabled && item.key)
|
|
283
|
+
.map((item) => [String(item.key), String(item.value ?? "")] as const);
|
|
284
|
+
return entries.length ? Object.fromEntries(entries) : undefined;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function generatedPostmanHeaders(body: Pick<ApiRequestSpec, "body" | "rawBody" | "bodyMode">): Record<string, string> | undefined {
|
|
288
|
+
const headers: Record<string, string> = {
|
|
289
|
+
Accept: "*/*",
|
|
290
|
+
"Cache-Control": "no-cache"
|
|
291
|
+
};
|
|
292
|
+
if (body.bodyMode === "json") headers["Content-Type"] = "application/json";
|
|
293
|
+
if (body.bodyMode === "urlencoded") headers["Content-Type"] = "application/x-www-form-urlencoded";
|
|
294
|
+
return Object.keys(headers).length ? headers : undefined;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function postmanVariables(items: Array<{ key?: string; value?: unknown; disabled?: boolean }> | undefined): Record<string, unknown> {
|
|
298
|
+
return Object.fromEntries((items ?? [])
|
|
299
|
+
.filter((item) => !item.disabled && item.key)
|
|
300
|
+
.map((item) => [String(item.key), item.value ?? ""]));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function findTemplateVariables(value: unknown): string[] {
|
|
304
|
+
const names: string[] = [];
|
|
305
|
+
const visit = (current: unknown) => {
|
|
306
|
+
if (typeof current === "string") {
|
|
307
|
+
for (const match of current.matchAll(/\{\{([A-Za-z0-9_.-]+)\}\}/g)) names.push(match[1]);
|
|
308
|
+
for (const match of current.matchAll(/\$\{([A-Za-z0-9_.-]+)\}/g)) names.push(match[1]);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (Array.isArray(current)) current.forEach(visit);
|
|
312
|
+
if (current && typeof current === "object") Object.values(current).forEach(visit);
|
|
313
|
+
};
|
|
314
|
+
visit(value);
|
|
315
|
+
return names;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function paramsFromVariables(variableNames: string[]): Record<string, CatalogParam> | undefined {
|
|
319
|
+
if (!variableNames.length) return undefined;
|
|
320
|
+
return Object.fromEntries(variableNames.map((name) => [name, { required: false, type: "string" } satisfies CatalogParam]));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function normalizeCollectionFile(file: ApiCollectionFile): ApiCollectionFile {
|
|
324
|
+
return {
|
|
325
|
+
version: 1,
|
|
326
|
+
collections: Array.isArray(file.collections) ? file.collections.map((collection) => ({
|
|
327
|
+
...collection,
|
|
328
|
+
requestCount: collection.requests?.length ?? collection.requestCount ?? 0,
|
|
329
|
+
requests: collection.requests ?? []
|
|
330
|
+
})) : []
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function cleanRequest(request: ApiRequestSpec): ApiRequestSpec {
|
|
335
|
+
return Object.fromEntries(Object.entries(request).filter(([, value]) => value !== undefined && value !== null && !(typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0))) as ApiRequestSpec;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function mergeRecord<T>(base: Record<string, T> | undefined, override: Record<string, T> | undefined): Record<string, T> | undefined {
|
|
339
|
+
const merged = { ...(base ?? {}), ...(override ?? {}) };
|
|
340
|
+
return Object.keys(merged).length ? merged : undefined;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function asObject(value: unknown): Record<string, unknown> {
|
|
344
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function optionalString(value: unknown): string | undefined {
|
|
348
|
+
return value === undefined || value === null ? undefined : String(value);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function looksLikeJson(value: string): boolean {
|
|
352
|
+
return value.startsWith("{") || value.startsWith("[");
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function descriptionText(value: unknown): string | undefined {
|
|
356
|
+
if (typeof value === "string") return value;
|
|
357
|
+
const description = asObject(value);
|
|
358
|
+
return optionalString(description.content);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function uniqueRequestId(collectionId: string, folderPath: string[], name: string, index: number): string {
|
|
362
|
+
return sanitizeId(`${folderPath.join("_")}_${name}_${index || ""}`) || `${collectionId}_request_${index + 1}`;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function sanitizeId(value: unknown): string {
|
|
366
|
+
return String(value ?? "")
|
|
367
|
+
.toLowerCase()
|
|
368
|
+
.replace(/[^a-z0-9_-]+/g, "_")
|
|
369
|
+
.replace(/^_+|_+$/g, "")
|
|
370
|
+
.slice(0, 80) || "collection";
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function shortHash(value: string): string {
|
|
374
|
+
return createHash("sha1").update(value).digest("hex").slice(0, 8);
|
|
375
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function normalizeJsonRawBody(rawBody: string): string {
|
|
2
|
+
return stripStandaloneBackslashLines(rawBody);
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function stripStandaloneBackslashLines(value: string): string {
|
|
6
|
+
return value
|
|
7
|
+
.split(/\r?\n/)
|
|
8
|
+
.filter((line) => !/^\s*\\\s*$/.test(line))
|
|
9
|
+
.join("\n");
|
|
10
|
+
}
|