@kirrosh/zond 0.26.0 → 0.27.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 +59 -0
- package/README.md +22 -21
- package/bin/zond.mjs +23 -0
- package/package.json +13 -16
- package/scripts/npm/postinstall.mjs +76 -0
- package/src/CLAUDE.md +0 -112
- package/src/bun-types.d.ts +0 -5
- package/src/cli/argv.ts +0 -122
- package/src/cli/commands/add-api.ts +0 -146
- package/src/cli/commands/api/annotate/idempotency.ts +0 -59
- package/src/cli/commands/api/annotate/index.ts +0 -880
- package/src/cli/commands/api/annotate/lifecycle.ts +0 -74
- package/src/cli/commands/api/annotate/overlay.ts +0 -206
- package/src/cli/commands/api/annotate/pagination.ts +0 -64
- package/src/cli/commands/api/annotate/prompts.ts +0 -220
- package/src/cli/commands/api/annotate/readback.ts +0 -58
- package/src/cli/commands/api/annotate/resources.ts +0 -91
- package/src/cli/commands/api/annotate/seed-bodies.ts +0 -61
- package/src/cli/commands/audit.ts +0 -786
- package/src/cli/commands/catalog.ts +0 -97
- package/src/cli/commands/check.ts +0 -361
- package/src/cli/commands/checks.ts +0 -1072
- package/src/cli/commands/ci-init.ts +0 -223
- package/src/cli/commands/clean.ts +0 -212
- package/src/cli/commands/cleanup.ts +0 -236
- package/src/cli/commands/completions.ts +0 -192
- package/src/cli/commands/coverage.ts +0 -872
- package/src/cli/commands/db.ts +0 -611
- package/src/cli/commands/describe.ts +0 -120
- package/src/cli/commands/discover.ts +0 -1356
- package/src/cli/commands/doctor.ts +0 -661
- package/src/cli/commands/fixtures.ts +0 -402
- package/src/cli/commands/generate.ts +0 -577
- package/src/cli/commands/init/agents-md.ts +0 -61
- package/src/cli/commands/init/bootstrap.ts +0 -111
- package/src/cli/commands/init/index.ts +0 -244
- package/src/cli/commands/init/skills.ts +0 -141
- package/src/cli/commands/init/templates/agents.md +0 -86
- package/src/cli/commands/init/templates/markdown.d.ts +0 -4
- package/src/cli/commands/init/templates/skills/warm-up-target.md +0 -122
- package/src/cli/commands/init/templates/skills/zond-checks.md +0 -621
- package/src/cli/commands/init/templates/skills/zond-seed.md +0 -114
- package/src/cli/commands/init/templates/skills/zond-triage.md +0 -272
- package/src/cli/commands/init/templates/skills/zond.md +0 -861
- package/src/cli/commands/init/templates/zond-config.yml +0 -14
- package/src/cli/commands/prepare-fixtures.ts +0 -97
- package/src/cli/commands/probe/_seed-bodies.ts +0 -52
- package/src/cli/commands/probe/mass-assignment.ts +0 -594
- package/src/cli/commands/probe/security.ts +0 -537
- package/src/cli/commands/probe/static.ts +0 -255
- package/src/cli/commands/probe/webhooks.ts +0 -163
- package/src/cli/commands/probe.ts +0 -535
- package/src/cli/commands/reference.ts +0 -87
- package/src/cli/commands/refresh-api.ts +0 -227
- package/src/cli/commands/remove-api.ts +0 -150
- package/src/cli/commands/report-bundle.ts +0 -310
- package/src/cli/commands/report.ts +0 -241
- package/src/cli/commands/request.ts +0 -548
- package/src/cli/commands/run.ts +0 -1142
- package/src/cli/commands/schema-from-runs.ts +0 -128
- package/src/cli/commands/secrets.ts +0 -133
- package/src/cli/commands/session.ts +0 -244
- package/src/cli/commands/use.ts +0 -74
- package/src/cli/index.ts +0 -55
- package/src/cli/json-envelope.ts +0 -108
- package/src/cli/json-schemas.ts +0 -314
- package/src/cli/output.ts +0 -40
- package/src/cli/program.ts +0 -219
- package/src/cli/resolve.ts +0 -105
- package/src/cli/runtime.ts +0 -7
- package/src/cli/safe-live.ts +0 -24
- package/src/cli/status-filter.ts +0 -114
- package/src/cli/util/api-context.ts +0 -85
- package/src/cli/version.ts +0 -8
- package/src/core/audit/persist.ts +0 -183
- package/src/core/checks/budget.ts +0 -59
- package/src/core/checks/checks/_crud-helpers.ts +0 -133
- package/src/core/checks/checks/_negative_mutator.ts +0 -133
- package/src/core/checks/checks/_readback-helpers.ts +0 -133
- package/src/core/checks/checks/content_type_conformance.ts +0 -39
- package/src/core/checks/checks/cross_call_references.ts +0 -147
- package/src/core/checks/checks/cursor_boundary_fuzzing.ts +0 -219
- package/src/core/checks/checks/ensure_resource_availability.ts +0 -62
- package/src/core/checks/checks/idempotency_replay.ts +0 -242
- package/src/core/checks/checks/ignored_auth.ts +0 -254
- package/src/core/checks/checks/index.ts +0 -68
- package/src/core/checks/checks/lifecycle_transitions.ts +0 -416
- package/src/core/checks/checks/missing_required_header.ts +0 -40
- package/src/core/checks/checks/negative_data_rejection.ts +0 -148
- package/src/core/checks/checks/not_a_server_error.ts +0 -35
- package/src/core/checks/checks/open_cors_on_sensitive.ts +0 -160
- package/src/core/checks/checks/pagination_invariants.ts +0 -419
- package/src/core/checks/checks/positive_data_acceptance.ts +0 -33
- package/src/core/checks/checks/rate_limit_headers_absent.ts +0 -77
- package/src/core/checks/checks/response_headers_conformance.ts +0 -74
- package/src/core/checks/checks/response_schema_conformance.ts +0 -30
- package/src/core/checks/checks/status_code_conformance.ts +0 -132
- package/src/core/checks/checks/unsupported_method.ts +0 -63
- package/src/core/checks/checks/use_after_free.ts +0 -78
- package/src/core/checks/index.ts +0 -30
- package/src/core/checks/mode.ts +0 -82
- package/src/core/checks/recommended-action.ts +0 -68
- package/src/core/checks/registry.ts +0 -78
- package/src/core/checks/runner.ts +0 -1461
- package/src/core/checks/sarif.ts +0 -230
- package/src/core/checks/spec-findings.ts +0 -308
- package/src/core/checks/stateful.ts +0 -121
- package/src/core/checks/types.ts +0 -305
- package/src/core/checks/zond-extensions.ts +0 -73
- package/src/core/classifier/recommended-action.ts +0 -251
- package/src/core/context/current.ts +0 -51
- package/src/core/context/session.ts +0 -78
- package/src/core/coverage/loader.ts +0 -216
- package/src/core/coverage/reasons.ts +0 -300
- package/src/core/diagnostics/db-analysis.ts +0 -666
- package/src/core/diagnostics/failure-class.ts +0 -140
- package/src/core/diagnostics/failure-hints.ts +0 -112
- package/src/core/diagnostics/spec-pointer.ts +0 -99
- package/src/core/diagnostics/suggested-fixes.ts +0 -155
- package/src/core/exporter/case-study/index.ts +0 -270
- package/src/core/exporter/curl.ts +0 -40
- package/src/core/exporter/exporter.ts +0 -48
- package/src/core/exporter/html-report/escape.ts +0 -24
- package/src/core/exporter/html-report/index.ts +0 -479
- package/src/core/exporter/html-report/script.ts +0 -100
- package/src/core/exporter/html-report/styles.ts +0 -408
- package/src/core/generator/catalog-builder.ts +0 -179
- package/src/core/generator/chunker.ts +0 -78
- package/src/core/generator/coverage-phase.ts +0 -0
- package/src/core/generator/coverage-scanner.ts +0 -87
- package/src/core/generator/data-factory.ts +0 -759
- package/src/core/generator/describe.ts +0 -302
- package/src/core/generator/endpoint-warnings.ts +0 -52
- package/src/core/generator/fixtures-builder.ts +0 -332
- package/src/core/generator/index.ts +0 -14
- package/src/core/generator/openapi-reader.ts +0 -297
- package/src/core/generator/path-param-disambig.ts +0 -140
- package/src/core/generator/resources-builder.ts +0 -898
- package/src/core/generator/schema-utils.ts +0 -112
- package/src/core/generator/serializer.ts +0 -343
- package/src/core/generator/suite-generator.ts +0 -1301
- package/src/core/generator/types.ts +0 -63
- package/src/core/identity/identity-file.ts +0 -0
- package/src/core/lint/affects.ts +0 -28
- package/src/core/lint/config.ts +0 -96
- package/src/core/lint/format.ts +0 -42
- package/src/core/lint/index.ts +0 -94
- package/src/core/lint/reporter.ts +0 -128
- package/src/core/lint/rules/consistency.ts +0 -158
- package/src/core/lint/rules/heuristics.ts +0 -97
- package/src/core/lint/rules/strictness.ts +0 -109
- package/src/core/lint/types.ts +0 -96
- package/src/core/lint/walker.ts +0 -248
- package/src/core/meta/meta-store.ts +0 -11
- package/src/core/output/README.md +0 -73
- package/src/core/output/index.ts +0 -13
- package/src/core/output/run.ts +0 -91
- package/src/core/output/types.ts +0 -122
- package/src/core/parser/dynamic-values.ts +0 -160
- package/src/core/parser/env-interpolation.ts +0 -104
- package/src/core/parser/filter.ts +0 -97
- package/src/core/parser/schema.ts +0 -359
- package/src/core/parser/types.ts +0 -117
- package/src/core/parser/variables.ts +0 -0
- package/src/core/parser/yaml-parser.ts +0 -181
- package/src/core/probe/bootstrap.ts +0 -34
- package/src/core/probe/dry-run-envelope.ts +0 -61
- package/src/core/probe/mass-assignment/classify.ts +0 -175
- package/src/core/probe/mass-assignment/cleanup.ts +0 -52
- package/src/core/probe/mass-assignment/digest.ts +0 -114
- package/src/core/probe/mass-assignment/orchestrator.ts +0 -459
- package/src/core/probe/mass-assignment/regression.ts +0 -141
- package/src/core/probe/mass-assignment/suspects.ts +0 -92
- package/src/core/probe/mass-assignment/types.ts +0 -135
- package/src/core/probe/mass-assignment-probe-class.ts +0 -198
- package/src/core/probe/mass-assignment-probe.ts +0 -27
- package/src/core/probe/mass-assignment-template.ts +0 -240
- package/src/core/probe/method-probe.ts +0 -164
- package/src/core/probe/method-shared.ts +0 -69
- package/src/core/probe/negative-probe.ts +0 -691
- package/src/core/probe/orphan-tracker.ts +0 -188
- package/src/core/probe/path-discovery.ts +0 -439
- package/src/core/probe/probe-harness.ts +0 -119
- package/src/core/probe/registry.ts +0 -89
- package/src/core/probe/runner.ts +0 -136
- package/src/core/probe/security/baseline.ts +0 -174
- package/src/core/probe/security/classify.ts +0 -341
- package/src/core/probe/security/cleanup.ts +0 -125
- package/src/core/probe/security/detectors.ts +0 -71
- package/src/core/probe/security/digest.ts +0 -104
- package/src/core/probe/security/orchestrator.ts +0 -398
- package/src/core/probe/security/regression.ts +0 -103
- package/src/core/probe/security/types.ts +0 -151
- package/src/core/probe/security-probe-class.ts +0 -207
- package/src/core/probe/security-probe.ts +0 -32
- package/src/core/probe/shared.ts +0 -531
- package/src/core/probe/static-probe-class.ts +0 -125
- package/src/core/probe/types.ts +0 -165
- package/src/core/probe/verdict-aggregator.ts +0 -33
- package/src/core/probe/webhooks-probe.ts +0 -282
- package/src/core/reporter/console.ts +0 -240
- package/src/core/reporter/index.ts +0 -22
- package/src/core/reporter/json.ts +0 -22
- package/src/core/reporter/junit.ts +0 -93
- package/src/core/reporter/ndjson.ts +0 -37
- package/src/core/reporter/types.ts +0 -15
- package/src/core/runner/assertions.ts +0 -383
- package/src/core/runner/async-pool.ts +0 -108
- package/src/core/runner/auth-path.ts +0 -8
- package/src/core/runner/ci-context.ts +0 -72
- package/src/core/runner/executor.ts +0 -652
- package/src/core/runner/expr-eval.ts +0 -41
- package/src/core/runner/form-encode.ts +0 -41
- package/src/core/runner/http-client.ts +0 -224
- package/src/core/runner/learn-drift.ts +0 -293
- package/src/core/runner/preflight-vars.ts +0 -153
- package/src/core/runner/progress-tracker.ts +0 -73
- package/src/core/runner/rate-limiter.ts +0 -185
- package/src/core/runner/run-kind.ts +0 -45
- package/src/core/runner/schema-validator.ts +0 -308
- package/src/core/runner/send-request.ts +0 -232
- package/src/core/runner/transforms.ts +0 -65
- package/src/core/runner/types.ts +0 -94
- package/src/core/secrets/registry.ts +0 -164
- package/src/core/secrets/secrets-file.ts +0 -115
- package/src/core/selectors/operation-filter.ts +0 -144
- package/src/core/setup-api.ts +0 -590
- package/src/core/severity/category.ts +0 -94
- package/src/core/severity/index.ts +0 -58
- package/src/core/spec/infer-schema.ts +0 -102
- package/src/core/spec/layers.ts +0 -154
- package/src/core/spec/merge-specs.ts +0 -156
- package/src/core/spec/schema-from-runs.ts +0 -117
- package/src/core/spec/schema-overlay.ts +0 -130
- package/src/core/util/ajv.ts +0 -13
- package/src/core/util/format-eta.ts +0 -21
- package/src/core/util/headers.ts +0 -9
- package/src/core/util/url.ts +0 -24
- package/src/core/utils.ts +0 -13
- package/src/core/workspace/config.ts +0 -129
- package/src/core/workspace/fixture-gap-report.ts +0 -84
- package/src/core/workspace/fixture-gaps.ts +0 -71
- package/src/core/workspace/manifest.ts +0 -283
- package/src/core/workspace/output-rotation.ts +0 -62
- package/src/core/workspace/root.ts +0 -96
- package/src/core/workspace/triage-path.ts +0 -87
- package/src/db/lint-runs.ts +0 -47
- package/src/db/migrate.ts +0 -128
- package/src/db/migrations/0001_run_kind.sql +0 -25
- package/src/db/migrations/0002_run_kind_request.sql +0 -59
- package/src/db/migrations/sql.d.ts +0 -4
- package/src/db/queries/collections.ts +0 -133
- package/src/db/queries/coverage.ts +0 -9
- package/src/db/queries/dashboard.ts +0 -59
- package/src/db/queries/results.ts +0 -216
- package/src/db/queries/runs.ts +0 -289
- package/src/db/queries/sessions.ts +0 -42
- package/src/db/queries/settings.ts +0 -28
- package/src/db/queries/types.ts +0 -172
- package/src/db/queries.ts +0 -75
- package/src/db/schema.ts +0 -298
|
@@ -1,308 +0,0 @@
|
|
|
1
|
-
import Ajv from "ajv";
|
|
2
|
-
import type { ErrorObject, ValidateFunction, AnySchema } from "ajv";
|
|
3
|
-
import type { OpenAPIV3 } from "openapi-types";
|
|
4
|
-
import { makeAjv } from "../util/ajv.ts";
|
|
5
|
-
import { specPathToRegex } from "../generator/coverage-scanner.ts";
|
|
6
|
-
import type { AssertionResult } from "./types.ts";
|
|
7
|
-
|
|
8
|
-
export interface SchemaValidator {
|
|
9
|
-
validate(method: string, path: string, status: number, body: unknown): AssertionResult[];
|
|
10
|
-
/** TASK-142: surface whether an endpoint and a response branch matched.
|
|
11
|
-
* Lets ad-hoc callers (`zond request --validate-schema`) distinguish
|
|
12
|
-
* "no spec entry for this URL" from "spec entry exists, body is valid". */
|
|
13
|
-
inspect(method: string, path: string, status: number): {
|
|
14
|
-
matchedEndpoint: { method: string; path: string } | null;
|
|
15
|
-
matchedResponseStatus: string | null;
|
|
16
|
-
hasJsonSchema: boolean;
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
interface EndpointEntry {
|
|
21
|
-
method: string;
|
|
22
|
-
path: string;
|
|
23
|
-
regex: RegExp;
|
|
24
|
-
responses: OpenAPIV3.ResponsesObject;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const HTTP_METHODS = ["get", "post", "put", "patch", "delete", "head", "options"] as const;
|
|
28
|
-
|
|
29
|
-
export function createSchemaValidator(doc: OpenAPIV3.Document): SchemaValidator {
|
|
30
|
-
const isV31 = typeof doc.openapi === "string" && doc.openapi.startsWith("3.1");
|
|
31
|
-
// OpenAPI 3.1 → JSON Schema Draft 2020-12; 3.0 → Draft 4/7-ish.
|
|
32
|
-
// verbose:true exposes parentSchema on each error so humanize() can render
|
|
33
|
-
// the full required-set alongside the missing field name (TASK-277).
|
|
34
|
-
const ajv = makeAjv(isV31, { strict: false, allErrors: true, verbose: true });
|
|
35
|
-
applyStrictFormats(ajv);
|
|
36
|
-
|
|
37
|
-
const endpoints: EndpointEntry[] = [];
|
|
38
|
-
if (doc.paths) {
|
|
39
|
-
for (const [pathTpl, pathItem] of Object.entries(doc.paths)) {
|
|
40
|
-
if (!pathItem) continue;
|
|
41
|
-
const regex = specPathToRegex(pathTpl);
|
|
42
|
-
for (const m of HTTP_METHODS) {
|
|
43
|
-
const op = (pathItem as Record<string, unknown>)[m] as OpenAPIV3.OperationObject | undefined;
|
|
44
|
-
if (!op || !op.responses) continue;
|
|
45
|
-
endpoints.push({ method: m.toUpperCase(), path: pathTpl, regex, responses: op.responses });
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
// Sort by specificity so concrete paths (e.g. /users/me) win over templated
|
|
49
|
-
// ones (/users/{id}) regardless of spec declaration order. Tie-breaker is
|
|
50
|
-
// stable insertion order (Array.sort is stable in modern engines).
|
|
51
|
-
endpoints.sort((a, b) => paramCount(a.path) - paramCount(b.path));
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// ARV-214: per-schema compile is the dominant cost of --validate-schema
|
|
55
|
-
// on big dereferenced specs (github 14 MiB → minutes per response).
|
|
56
|
-
// The cache below is keyed by schema *object reference*, so endpoints
|
|
57
|
-
// that share a $ref source after dereference hit it on the second
|
|
58
|
-
// access. The slow_compile budget warns the user the first time a
|
|
59
|
-
// schema crosses 1s — the same compile only happens once per schema
|
|
60
|
-
// anyway thanks to the cache, so the warning is per-source, not
|
|
61
|
-
// per-call.
|
|
62
|
-
const SLOW_COMPILE_MS = Number(process.env.ZOND_VALIDATE_SCHEMA_SLOW_COMPILE_MS ?? "1000");
|
|
63
|
-
// Hard byte cap stops the run from hanging minutes on a pathological
|
|
64
|
-
// single response schema (post-deref github Repository / kubernetes
|
|
65
|
-
// pod-spec). Returning a no-op validator + a single warning is much
|
|
66
|
-
// friendlier than blocking on `ajv.compile` for an unbounded time.
|
|
67
|
-
// Default chosen from the bench in /tmp/bench-convert.ts: a 572 KiB
|
|
68
|
-
// schema compiles in ~800 ms; 1 MiB ≈ 1.4 s; beyond that we'd rather
|
|
69
|
-
// skip and tell the user.
|
|
70
|
-
const MAX_SCHEMA_BYTES = Number(process.env.ZOND_VALIDATE_SCHEMA_MAX_BYTES ?? String(1_048_576));
|
|
71
|
-
const compiled = new Map<unknown, ValidateFunction | null>();
|
|
72
|
-
const warnedSlow = new WeakSet<object>();
|
|
73
|
-
const warnedTooLarge = new WeakSet<object>();
|
|
74
|
-
|
|
75
|
-
function tooLargeSentinel(): null {
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function compile(schema: AnySchema): ValidateFunction | null {
|
|
80
|
-
if (compiled.has(schema)) return compiled.get(schema) ?? null;
|
|
81
|
-
// Cheap pre-check: a schema whose JSON serialisation exceeds
|
|
82
|
-
// MAX_SCHEMA_BYTES is almost certainly going to take seconds-to-
|
|
83
|
-
// minutes to compile and produce noisy mid-body errors that aren't
|
|
84
|
-
// worth the wait. Skip it with a warning. JSON.stringify itself is
|
|
85
|
-
// O(n) but at MiB-scale completes in tens of ms — orders of
|
|
86
|
-
// magnitude cheaper than ajv.compile on the same payload.
|
|
87
|
-
if (MAX_SCHEMA_BYTES > 0) {
|
|
88
|
-
try {
|
|
89
|
-
const sz = JSON.stringify(schema)?.length ?? 0;
|
|
90
|
-
if (sz > MAX_SCHEMA_BYTES) {
|
|
91
|
-
if (typeof schema === "object" && schema && !warnedTooLarge.has(schema as object)) {
|
|
92
|
-
warnedTooLarge.add(schema as object);
|
|
93
|
-
const kib = Math.round(sz / 1024);
|
|
94
|
-
process.stderr.write(
|
|
95
|
-
`[zond] schema too large for --validate-schema (${kib} KiB > ${Math.round(MAX_SCHEMA_BYTES / 1024)} KiB) — skipping; raise via ZOND_VALIDATE_SCHEMA_MAX_BYTES.\n`,
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
compiled.set(schema, tooLargeSentinel());
|
|
99
|
-
return null;
|
|
100
|
-
}
|
|
101
|
-
} catch { /* circular or non-serialisable — let ajv try */ }
|
|
102
|
-
}
|
|
103
|
-
const prepared = isV31 ? schema : convertOpenApi30(schema);
|
|
104
|
-
const t0 = performance.now();
|
|
105
|
-
const fn = ajv.compile(prepared as AnySchema);
|
|
106
|
-
const elapsed = performance.now() - t0;
|
|
107
|
-
if (elapsed >= SLOW_COMPILE_MS && typeof schema === "object" && schema && !warnedSlow.has(schema as object)) {
|
|
108
|
-
warnedSlow.add(schema as object);
|
|
109
|
-
process.stderr.write(
|
|
110
|
-
`[zond] schema validator compile took ${elapsed.toFixed(0)} ms (warn ≥ ${SLOW_COMPILE_MS} ms) — see ZOND_VALIDATE_SCHEMA_MAX_BYTES if --validate-schema runs feel slow.\n`,
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
|
-
compiled.set(schema, fn);
|
|
114
|
-
return fn;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function findResponseSchema(method: string, path: string, status: number): OpenAPIV3.SchemaObject | undefined {
|
|
118
|
-
const upper = method.toUpperCase();
|
|
119
|
-
// Endpoints are pre-sorted by specificity (concrete paths first), so the
|
|
120
|
-
// first regex match is the most specific — /users/me beats /users/{id}.
|
|
121
|
-
const match = endpoints.find(e => e.method === upper && e.regex.test(path));
|
|
122
|
-
if (!match) return undefined;
|
|
123
|
-
const responses = match.responses;
|
|
124
|
-
const exact = responses[String(status)] as OpenAPIV3.ResponseObject | undefined;
|
|
125
|
-
const wildcard = responses[`${Math.floor(status / 100)}XX`] as OpenAPIV3.ResponseObject | undefined;
|
|
126
|
-
const fallback = responses.default as OpenAPIV3.ResponseObject | undefined;
|
|
127
|
-
const response = exact ?? wildcard ?? fallback;
|
|
128
|
-
if (!response || !response.content) return undefined;
|
|
129
|
-
const json = response.content["application/json"];
|
|
130
|
-
return (json?.schema as OpenAPIV3.SchemaObject | undefined) ?? undefined;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function inspectMatch(method: string, path: string, status: number) {
|
|
134
|
-
const upper = method.toUpperCase();
|
|
135
|
-
const ep = endpoints.find(e => e.method === upper && e.regex.test(path));
|
|
136
|
-
if (!ep) {
|
|
137
|
-
return { matchedEndpoint: null, matchedResponseStatus: null, hasJsonSchema: false };
|
|
138
|
-
}
|
|
139
|
-
const exact = ep.responses[String(status)] as OpenAPIV3.ResponseObject | undefined;
|
|
140
|
-
const wildcard = ep.responses[`${Math.floor(status / 100)}XX`] as OpenAPIV3.ResponseObject | undefined;
|
|
141
|
-
const fallback = ep.responses.default as OpenAPIV3.ResponseObject | undefined;
|
|
142
|
-
const matchedKey = exact ? String(status) : wildcard ? `${Math.floor(status / 100)}XX` : fallback ? "default" : null;
|
|
143
|
-
const response = exact ?? wildcard ?? fallback;
|
|
144
|
-
const hasJsonSchema = !!(response?.content?.["application/json"]?.schema);
|
|
145
|
-
return {
|
|
146
|
-
matchedEndpoint: { method: ep.method, path: ep.path },
|
|
147
|
-
matchedResponseStatus: matchedKey,
|
|
148
|
-
hasJsonSchema,
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return {
|
|
153
|
-
validate(method, path, status, body) {
|
|
154
|
-
const schema = findResponseSchema(method, path, status);
|
|
155
|
-
if (!schema) return [];
|
|
156
|
-
let validator: ValidateFunction | null;
|
|
157
|
-
try {
|
|
158
|
-
validator = compile(schema);
|
|
159
|
-
} catch (err) {
|
|
160
|
-
return [{
|
|
161
|
-
field: "body",
|
|
162
|
-
rule: "schema.compile_error",
|
|
163
|
-
passed: false,
|
|
164
|
-
actual: undefined,
|
|
165
|
-
expected: err instanceof Error ? err.message : String(err),
|
|
166
|
-
}];
|
|
167
|
-
}
|
|
168
|
-
// ARV-214: schema crossed ZOND_VALIDATE_SCHEMA_MAX_BYTES — surface
|
|
169
|
-
// the skip as a passing assertion so the run stays green and the
|
|
170
|
-
// user knows validation was bypassed for this body.
|
|
171
|
-
if (validator === null) {
|
|
172
|
-
return [{
|
|
173
|
-
field: "body",
|
|
174
|
-
rule: "schema.skipped_too_large",
|
|
175
|
-
passed: true,
|
|
176
|
-
actual: undefined,
|
|
177
|
-
expected: "schema exceeded ZOND_VALIDATE_SCHEMA_MAX_BYTES — see stderr warning",
|
|
178
|
-
kind: "schema",
|
|
179
|
-
}];
|
|
180
|
-
}
|
|
181
|
-
const ok = validator(body);
|
|
182
|
-
if (ok) return [];
|
|
183
|
-
const errors = validator.errors ?? [];
|
|
184
|
-
return errors.map(e => ajvErrorToAssertion(e, body));
|
|
185
|
-
},
|
|
186
|
-
inspect: inspectMatch,
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function paramCount(path: string): number {
|
|
191
|
-
let n = 0;
|
|
192
|
-
for (const seg of path.split("/")) if (/^\{[^}]+\}$/.test(seg)) n++;
|
|
193
|
-
return n;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function ajvErrorToAssertion(err: ErrorObject, body: unknown): AssertionResult {
|
|
197
|
-
const ptr = err.instancePath || "";
|
|
198
|
-
// Field key like "body" or "body.user.email" for parity with checkAssertions.
|
|
199
|
-
const field = ptr ? `body${ptr.replace(/\//g, ".")}` : "body";
|
|
200
|
-
const actual = ptr ? getByJsonPointer(body, ptr) : body;
|
|
201
|
-
return {
|
|
202
|
-
field,
|
|
203
|
-
rule: `schema.${err.keyword}`,
|
|
204
|
-
passed: false,
|
|
205
|
-
actual,
|
|
206
|
-
expected: humanize(err),
|
|
207
|
-
kind: "schema",
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function humanize(err: ErrorObject): string {
|
|
212
|
-
switch (err.keyword) {
|
|
213
|
-
case "required": {
|
|
214
|
-
const missing = (err.params as { missingProperty: string }).missingProperty;
|
|
215
|
-
// verbose:true gives us the parent schema; surface the full required-set
|
|
216
|
-
// so the user can see drift at a glance instead of decoding the message
|
|
217
|
-
// one missing-field-error at a time (TASK-277).
|
|
218
|
-
const parent = (err as ErrorObject & { parentSchema?: unknown }).parentSchema;
|
|
219
|
-
const required =
|
|
220
|
-
parent && typeof parent === "object" && Array.isArray((parent as { required?: unknown }).required)
|
|
221
|
-
? ((parent as { required: string[] }).required)
|
|
222
|
-
: undefined;
|
|
223
|
-
const tail = required ? `; expected required: [${required.join(", ")}]` : "";
|
|
224
|
-
return `missing required field "${missing}"${tail}`;
|
|
225
|
-
}
|
|
226
|
-
case "type":
|
|
227
|
-
return `type ${(err.params as { type: string | string[] }).type}`;
|
|
228
|
-
case "enum":
|
|
229
|
-
return `one of ${JSON.stringify((err.params as { allowedValues: unknown[] }).allowedValues)}`;
|
|
230
|
-
case "format":
|
|
231
|
-
return `format "${(err.params as { format: string }).format}"`;
|
|
232
|
-
case "additionalProperties":
|
|
233
|
-
return `no additional property "${(err.params as { additionalProperty: string }).additionalProperty}"`;
|
|
234
|
-
case "const":
|
|
235
|
-
return `const ${JSON.stringify((err.params as { allowedValue: unknown }).allowedValue)}`;
|
|
236
|
-
case "minLength":
|
|
237
|
-
case "maxLength":
|
|
238
|
-
case "minimum":
|
|
239
|
-
case "maximum":
|
|
240
|
-
case "exclusiveMinimum":
|
|
241
|
-
case "exclusiveMaximum":
|
|
242
|
-
case "multipleOf":
|
|
243
|
-
case "pattern":
|
|
244
|
-
return `${err.keyword} ${JSON.stringify((err.params as Record<string, unknown>)[err.keyword] ?? "")}`.trim();
|
|
245
|
-
default:
|
|
246
|
-
return err.message ?? err.keyword;
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function getByJsonPointer(obj: unknown, pointer: string): unknown {
|
|
251
|
-
if (!pointer) return obj;
|
|
252
|
-
const segments = pointer.split("/").slice(1).map(s => s.replace(/~1/g, "/").replace(/~0/g, "~"));
|
|
253
|
-
let cur: unknown = obj;
|
|
254
|
-
for (const seg of segments) {
|
|
255
|
-
if (cur && typeof cur === "object") {
|
|
256
|
-
cur = (cur as Record<string, unknown>)[seg];
|
|
257
|
-
} else {
|
|
258
|
-
return undefined;
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
return cur;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// RFC3339 §5.6: date-time = full-date "T" full-time. T (or t) is required as
|
|
265
|
-
// separator; offset is "Z" or "[+-]HH:MM" with explicit colon. ajv-formats
|
|
266
|
-
// accepts " " as separator and "+HH" without colon, which lets PostgreSQL-style
|
|
267
|
-
// timestamps ("2026-04-29 07:10:44.674675+00") slip through.
|
|
268
|
-
export const STRICT_RFC3339_DATE_TIME =
|
|
269
|
-
/^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])[Tt](?:[01]\d|2[0-3]):[0-5]\d:(?:[0-5]\d|60)(?:\.\d+)?(?:[Zz]|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/;
|
|
270
|
-
|
|
271
|
-
function applyStrictFormats(ajv: Ajv): void {
|
|
272
|
-
// Override ajv-formats' lax date-time with strict RFC3339.
|
|
273
|
-
ajv.addFormat("date-time", { type: "string", validate: STRICT_RFC3339_DATE_TIME });
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* Convert OpenAPI 3.0 schema to JSON Schema Draft 7-compatible:
|
|
278
|
-
* - `nullable: true` → add "null" to `type`.
|
|
279
|
-
* - Drop unsupported `example`, `xml`, `discriminator` keywords (ajv tolerates with strict:false).
|
|
280
|
-
*/
|
|
281
|
-
function convertOpenApi30(schema: AnySchema): AnySchema {
|
|
282
|
-
if (!schema || typeof schema !== "object") return schema;
|
|
283
|
-
if (Array.isArray(schema)) {
|
|
284
|
-
return schema.map(s => convertOpenApi30(s as AnySchema)) as unknown as AnySchema;
|
|
285
|
-
}
|
|
286
|
-
const src = schema as Record<string, unknown>;
|
|
287
|
-
const out: Record<string, unknown> = {};
|
|
288
|
-
for (const [k, v] of Object.entries(src)) {
|
|
289
|
-
if (k === "nullable") continue;
|
|
290
|
-
if (k === "type" && src.nullable === true) {
|
|
291
|
-
if (Array.isArray(v)) {
|
|
292
|
-
out.type = [...v as unknown[], "null"];
|
|
293
|
-
} else if (typeof v === "string") {
|
|
294
|
-
out.type = [v, "null"];
|
|
295
|
-
} else {
|
|
296
|
-
out.type = v;
|
|
297
|
-
}
|
|
298
|
-
continue;
|
|
299
|
-
}
|
|
300
|
-
if (v && typeof v === "object") {
|
|
301
|
-
out[k] = convertOpenApi30(v as AnySchema);
|
|
302
|
-
} else {
|
|
303
|
-
out[k] = v;
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
// Standalone nullable: true with no explicit type → leave as-is (ajv accepts any).
|
|
307
|
-
return out as AnySchema;
|
|
308
|
-
}
|
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
import { executeRequest } from "./http-client.ts";
|
|
2
|
-
import { loadEnvironment, substituteString, substituteDeep } from "../parser/variables.ts";
|
|
3
|
-
import { getDb } from "../../db/schema.ts";
|
|
4
|
-
import { findCollectionByNameOrId } from "../../db/queries.ts";
|
|
5
|
-
import { encodeFormBody } from "./form-encode.ts";
|
|
6
|
-
import { hasHeaderCI } from "../util/headers.ts";
|
|
7
|
-
|
|
8
|
-
export function extractByPath(obj: unknown, path: string): unknown {
|
|
9
|
-
return extractByPathWithDiagnostic(obj, path).value;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/** ARV-70 (feedback round-01 / F11): same extractor as above but also
|
|
13
|
-
* reports *why* the path failed. The CLI surfaces this on stderr when
|
|
14
|
-
* `--json-path` returns undefined so users don't lose minutes debugging
|
|
15
|
-
* "empty stdout despite data[0].id is right there in the JSON" — the
|
|
16
|
-
* hint pinpoints the segment that didn't resolve (e.g. "body is a string
|
|
17
|
-
* — content-type was not application/json"). */
|
|
18
|
-
export function extractByPathWithDiagnostic(
|
|
19
|
-
obj: unknown,
|
|
20
|
-
path: string,
|
|
21
|
-
): { value: unknown; resolved: string[]; failedAt?: string; reason?: string } {
|
|
22
|
-
const segments = path.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
|
|
23
|
-
const resolved: string[] = [];
|
|
24
|
-
let current: unknown = obj;
|
|
25
|
-
for (const seg of segments) {
|
|
26
|
-
if (current === null || current === undefined) {
|
|
27
|
-
return { value: undefined, resolved, failedAt: seg, reason: "previous segment resolved to null/undefined" };
|
|
28
|
-
}
|
|
29
|
-
if (Array.isArray(current)) {
|
|
30
|
-
// ARV-207: `length` resolves to the array length (jq-style). This
|
|
31
|
-
// lets `--json-path data.items.length` answer "is the list non-empty?"
|
|
32
|
-
// in one call — the harvest flow needs it ('length>0 → has data;
|
|
33
|
-
// length==0 → seed') and previously had to shell out to jq.
|
|
34
|
-
if (seg === "length") {
|
|
35
|
-
current = current.length;
|
|
36
|
-
resolved.push(seg);
|
|
37
|
-
continue;
|
|
38
|
-
}
|
|
39
|
-
const idx = parseInt(seg, 10);
|
|
40
|
-
if (isNaN(idx)) {
|
|
41
|
-
return { value: undefined, resolved, failedAt: seg, reason: `expected an array index, got non-numeric segment "${seg}" (hint: use 'length' for array size)` };
|
|
42
|
-
}
|
|
43
|
-
if (idx < 0 || idx >= current.length) {
|
|
44
|
-
return { value: undefined, resolved, failedAt: seg, reason: `array index ${idx} out of bounds (length ${current.length})` };
|
|
45
|
-
}
|
|
46
|
-
current = current[idx];
|
|
47
|
-
} else if (typeof current === "object") {
|
|
48
|
-
const obj = current as Record<string, unknown>;
|
|
49
|
-
if (!(seg in obj)) {
|
|
50
|
-
const keys = Object.keys(obj).slice(0, 8).join(", ");
|
|
51
|
-
return { value: undefined, resolved, failedAt: seg, reason: `key "${seg}" not in object (keys: ${keys || "<empty>"})` };
|
|
52
|
-
}
|
|
53
|
-
current = obj[seg];
|
|
54
|
-
} else {
|
|
55
|
-
return {
|
|
56
|
-
value: undefined,
|
|
57
|
-
resolved,
|
|
58
|
-
failedAt: seg,
|
|
59
|
-
reason: `cannot traverse "${seg}" — body is a ${typeof current === "string" ? "string (content-type may not be application/json)" : typeof current}`,
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
resolved.push(seg);
|
|
63
|
-
}
|
|
64
|
-
return { value: current, resolved };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export interface SendAdHocRequestOptions {
|
|
68
|
-
method: string;
|
|
69
|
-
url: string;
|
|
70
|
-
headers?: Record<string, string>;
|
|
71
|
-
body?: string;
|
|
72
|
-
timeout?: number;
|
|
73
|
-
envName?: string;
|
|
74
|
-
collectionName?: string;
|
|
75
|
-
jsonPath?: string;
|
|
76
|
-
maxResponseChars?: number;
|
|
77
|
-
dbPath?: string;
|
|
78
|
-
searchDir?: string;
|
|
79
|
-
/** Extra vars merged on top of env (e.g. captured values from a stored run). */
|
|
80
|
-
extraVars?: Record<string, unknown>;
|
|
81
|
-
/** When true, resolve interpolation but do not actually send the request. */
|
|
82
|
-
dryRun?: boolean;
|
|
83
|
-
/** ARV-149: when true, send the body as `application/x-www-form-urlencoded`.
|
|
84
|
-
* Parses `body` as JSON to lift fields, then re-encodes with bracket notation
|
|
85
|
-
* (Stripe-style nested keys). If `body` isn't JSON-parseable it's passed
|
|
86
|
-
* through verbatim, and only the Content-Type header is set. */
|
|
87
|
-
form?: boolean;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export interface SendAdHocRequestResult {
|
|
91
|
-
status: number;
|
|
92
|
-
headers: Record<string, string>;
|
|
93
|
-
body: unknown;
|
|
94
|
-
duration_ms: number;
|
|
95
|
-
/** ARV-70: when --json-path failed to resolve, this carries which
|
|
96
|
-
* segment broke and why so the CLI can surface a hint on stderr. */
|
|
97
|
-
jsonPathDiagnostic?: { resolved: string[]; failedAt?: string; reason?: string };
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export interface ResolvedRequest {
|
|
101
|
-
method: string;
|
|
102
|
-
url: string;
|
|
103
|
-
headers: Record<string, string>;
|
|
104
|
-
body?: string;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export async function resolveAdHocRequest(options: SendAdHocRequestOptions): Promise<ResolvedRequest> {
|
|
108
|
-
let searchDir = options.searchDir ?? process.cwd();
|
|
109
|
-
if (options.collectionName) {
|
|
110
|
-
getDb(options.dbPath);
|
|
111
|
-
const col = findCollectionByNameOrId(options.collectionName);
|
|
112
|
-
if (!col) {
|
|
113
|
-
throw new Error(`API '${options.collectionName}' is not registered. Run \`zond add api <name> --base-url <url>\` first, or check the name with \`zond db collections\`.`);
|
|
114
|
-
}
|
|
115
|
-
if (col.base_dir) searchDir = col.base_dir;
|
|
116
|
-
}
|
|
117
|
-
const envVars = await loadEnvironment(options.envName, searchDir);
|
|
118
|
-
const vars = options.extraVars ? { ...envVars, ...options.extraVars } : envVars;
|
|
119
|
-
|
|
120
|
-
// Auto-prefix base_url for relative paths when --api is in play.
|
|
121
|
-
// Mirror the YAML-runner ergonomics: `zond request --api jp GET /users/1`
|
|
122
|
-
// should work the same as `... GET '{{base_url}}/users/1'`. We touch the URL
|
|
123
|
-
// only when it's clearly relative (leading "/") and has no scheme/template
|
|
124
|
-
// already, so absolute URLs and explicit {{var}} templates pass through.
|
|
125
|
-
let urlToResolve = options.url;
|
|
126
|
-
if (
|
|
127
|
-
options.collectionName
|
|
128
|
-
&& typeof vars.base_url === "string"
|
|
129
|
-
&& vars.base_url.length > 0
|
|
130
|
-
&& urlToResolve.startsWith("/")
|
|
131
|
-
&& !urlToResolve.startsWith("//")
|
|
132
|
-
) {
|
|
133
|
-
const base = vars.base_url.replace(/\/+$/, "");
|
|
134
|
-
urlToResolve = `${base}${urlToResolve}`;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const resolvedUrl = substituteString(urlToResolve, vars) as string;
|
|
138
|
-
const parsedHeaders = options.headers ?? {};
|
|
139
|
-
const resolvedHeaders = Object.keys(parsedHeaders).length > 0 ? substituteDeep(parsedHeaders, vars) : {};
|
|
140
|
-
let resolvedBody = options.body ? substituteString(options.body, vars) as string : undefined;
|
|
141
|
-
|
|
142
|
-
const finalHeaders: Record<string, string> = { ...resolvedHeaders };
|
|
143
|
-
const hasContentType =
|
|
144
|
-
finalHeaders["Content-Type"] !== undefined || finalHeaders["content-type"] !== undefined;
|
|
145
|
-
|
|
146
|
-
// ARV-149: `--form` (or auto-detection from spec content type) re-encodes
|
|
147
|
-
// the JSON body as `application/x-www-form-urlencoded` with bracket
|
|
148
|
-
// notation. Stripe v1 and other Rails/PHP-style APIs declare ONLY form
|
|
149
|
-
// bodies on their mutating endpoints — sending JSON yields a 400
|
|
150
|
-
// "check that your POST content type is application/x-www-form-urlencoded".
|
|
151
|
-
if (resolvedBody && options.form) {
|
|
152
|
-
try {
|
|
153
|
-
const parsed = JSON.parse(resolvedBody);
|
|
154
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
155
|
-
resolvedBody = encodeFormBody(parsed as Record<string, unknown>);
|
|
156
|
-
}
|
|
157
|
-
} catch {
|
|
158
|
-
// Body isn't JSON — assume it's already urlencoded; pass through verbatim.
|
|
159
|
-
}
|
|
160
|
-
if (!hasContentType) {
|
|
161
|
-
finalHeaders["Content-Type"] = "application/x-www-form-urlencoded";
|
|
162
|
-
}
|
|
163
|
-
} else if (resolvedBody && !hasContentType) {
|
|
164
|
-
try {
|
|
165
|
-
JSON.parse(resolvedBody);
|
|
166
|
-
finalHeaders["Content-Type"] = "application/json";
|
|
167
|
-
} catch {
|
|
168
|
-
// Not JSON — don't set content-type, let server decide
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// TASK-231: when --api resolved an env with `auth_token`, auto-inject the
|
|
173
|
-
// standard `Authorization: Bearer …` header. This mirrors the YAML runner's
|
|
174
|
-
// behaviour (probes/suites template the same header from the security
|
|
175
|
-
// scheme) so `zond request --api X GET /…` doesn't 401 just because the
|
|
176
|
-
// user didn't repeat `--header "Authorization: Bearer {{auth_token}}"`.
|
|
177
|
-
// User-supplied Authorization always wins.
|
|
178
|
-
if (
|
|
179
|
-
options.collectionName
|
|
180
|
-
&& typeof vars.auth_token === "string"
|
|
181
|
-
&& vars.auth_token.length > 0
|
|
182
|
-
&& !hasHeaderCI(finalHeaders, "Authorization")
|
|
183
|
-
) {
|
|
184
|
-
finalHeaders["Authorization"] = `Bearer ${vars.auth_token}`;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
return {
|
|
188
|
-
method: options.method,
|
|
189
|
-
url: resolvedUrl,
|
|
190
|
-
headers: finalHeaders,
|
|
191
|
-
...(resolvedBody !== undefined ? { body: resolvedBody } : {}),
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Note: `options.form` is consumed inside `resolveAdHocRequest` itself —
|
|
196
|
-
// the encoded `body` string and Content-Type are baked into `finalHeaders`.
|
|
197
|
-
// `sendAdHocRequest` doesn't need to forward the flag separately.
|
|
198
|
-
|
|
199
|
-
export async function sendAdHocRequest(options: SendAdHocRequestOptions): Promise<SendAdHocRequestResult> {
|
|
200
|
-
const resolved = await resolveAdHocRequest(options);
|
|
201
|
-
|
|
202
|
-
const response = await executeRequest(
|
|
203
|
-
{
|
|
204
|
-
method: resolved.method,
|
|
205
|
-
url: resolved.url,
|
|
206
|
-
headers: resolved.headers,
|
|
207
|
-
body: resolved.body,
|
|
208
|
-
},
|
|
209
|
-
options.timeout ? { timeout: options.timeout } : undefined,
|
|
210
|
-
);
|
|
211
|
-
|
|
212
|
-
let responseBody: unknown = response.body_parsed ?? response.body;
|
|
213
|
-
let jsonPathDiagnostic: SendAdHocRequestResult["jsonPathDiagnostic"];
|
|
214
|
-
|
|
215
|
-
if (options.jsonPath && responseBody !== undefined) {
|
|
216
|
-
const diag = extractByPathWithDiagnostic(responseBody, options.jsonPath);
|
|
217
|
-
responseBody = diag.value;
|
|
218
|
-
if (diag.value === undefined && diag.failedAt) {
|
|
219
|
-
jsonPathDiagnostic = { resolved: diag.resolved, failedAt: diag.failedAt, reason: diag.reason };
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const result: SendAdHocRequestResult = {
|
|
224
|
-
status: response.status,
|
|
225
|
-
headers: response.headers,
|
|
226
|
-
body: responseBody,
|
|
227
|
-
duration_ms: response.duration_ms,
|
|
228
|
-
...(jsonPathDiagnostic ? { jsonPathDiagnostic } : {}),
|
|
229
|
-
};
|
|
230
|
-
|
|
231
|
-
return result;
|
|
232
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
const DIRECTIVES = new Set(["concat", "append", "length", "get", "first", "map_field"]);
|
|
2
|
-
|
|
3
|
-
export function applyTransform(directive: unknown): unknown {
|
|
4
|
-
if (typeof directive !== "object" || directive === null || Array.isArray(directive)) {
|
|
5
|
-
return directive;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
const obj = directive as Record<string, unknown>;
|
|
9
|
-
const keys = Object.keys(obj);
|
|
10
|
-
|
|
11
|
-
if (keys.length !== 1 || !DIRECTIVES.has(keys[0]!)) {
|
|
12
|
-
return directive;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const op = keys[0]!;
|
|
16
|
-
const arg = obj[op];
|
|
17
|
-
|
|
18
|
-
switch (op) {
|
|
19
|
-
case "concat": {
|
|
20
|
-
if (!Array.isArray(arg)) return directive;
|
|
21
|
-
const result: unknown[] = [];
|
|
22
|
-
for (const item of arg) {
|
|
23
|
-
if (Array.isArray(item)) result.push(...item);
|
|
24
|
-
else result.push(item);
|
|
25
|
-
}
|
|
26
|
-
return result;
|
|
27
|
-
}
|
|
28
|
-
case "append": {
|
|
29
|
-
if (!Array.isArray(arg) || arg.length < 2) return directive;
|
|
30
|
-
const arr = Array.isArray(arg[0]) ? [...arg[0]] : [];
|
|
31
|
-
return [...arr, ...arg.slice(1)];
|
|
32
|
-
}
|
|
33
|
-
case "length": {
|
|
34
|
-
if (Array.isArray(arg)) return arg.length;
|
|
35
|
-
if (typeof arg === "string") return arg.length;
|
|
36
|
-
return 0;
|
|
37
|
-
}
|
|
38
|
-
case "get": {
|
|
39
|
-
if (!Array.isArray(arg) || arg.length < 2) return directive;
|
|
40
|
-
const [source, index] = arg;
|
|
41
|
-
if (Array.isArray(source) && typeof index === "number") return source[index];
|
|
42
|
-
if (typeof source === "object" && source !== null && typeof index === "string") {
|
|
43
|
-
return (source as Record<string, unknown>)[index];
|
|
44
|
-
}
|
|
45
|
-
return undefined;
|
|
46
|
-
}
|
|
47
|
-
case "first": {
|
|
48
|
-
if (Array.isArray(arg)) return arg[0];
|
|
49
|
-
return undefined;
|
|
50
|
-
}
|
|
51
|
-
case "map_field": {
|
|
52
|
-
if (!Array.isArray(arg) || arg.length < 2) return directive;
|
|
53
|
-
const [items, field] = arg;
|
|
54
|
-
if (!Array.isArray(items) || typeof field !== "string") return directive;
|
|
55
|
-
return items.map((item) => {
|
|
56
|
-
if (typeof item === "object" && item !== null) {
|
|
57
|
-
return (item as Record<string, unknown>)[field];
|
|
58
|
-
}
|
|
59
|
-
return undefined;
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
default:
|
|
63
|
-
return directive;
|
|
64
|
-
}
|
|
65
|
-
}
|