@lovrozagar/oat 0.9.1 → 0.11.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/README.md +26 -1936
- package/dist/cli.js +326 -60
- package/dist/config/define-config.d.ts +15 -1
- package/dist/config/define-config.d.ts.map +1 -1
- package/dist/config/define-config.js.map +1 -1
- package/dist/config/validate.d.ts +12 -0
- package/dist/config/validate.d.ts.map +1 -0
- package/dist/config/validate.js +86 -0
- package/dist/config/validate.js.map +1 -0
- package/dist/conformance/effects.d.ts +9 -0
- package/dist/conformance/effects.d.ts.map +1 -0
- package/dist/conformance/effects.js +276 -0
- package/dist/conformance/effects.js.map +1 -0
- package/dist/conformance/expected-failures.d.ts +18 -0
- package/dist/conformance/expected-failures.d.ts.map +1 -0
- package/dist/conformance/expected-failures.js +48 -0
- package/dist/conformance/expected-failures.js.map +1 -0
- package/dist/conformance/feature-gate.d.ts +7 -0
- package/dist/conformance/feature-gate.d.ts.map +1 -0
- package/dist/conformance/feature-gate.js +428 -0
- package/dist/conformance/feature-gate.js.map +1 -0
- package/dist/conformance/fuzz.d.ts +108 -0
- package/dist/conformance/fuzz.d.ts.map +1 -0
- package/dist/conformance/fuzz.js +246 -0
- package/dist/conformance/fuzz.js.map +1 -0
- package/dist/conformance/leaks.d.ts +10 -0
- package/dist/conformance/leaks.d.ts.map +1 -0
- package/dist/conformance/leaks.js +23 -0
- package/dist/conformance/leaks.js.map +1 -0
- package/dist/conformance/pool.d.ts +49 -0
- package/dist/conformance/pool.d.ts.map +1 -0
- package/dist/conformance/pool.js +75 -0
- package/dist/conformance/pool.js.map +1 -0
- package/dist/conformance/rate-limit.d.ts +7 -0
- package/dist/conformance/rate-limit.d.ts.map +1 -0
- package/dist/conformance/rate-limit.js +372 -0
- package/dist/conformance/rate-limit.js.map +1 -0
- package/dist/conformance/scope.d.ts +15 -0
- package/dist/conformance/scope.d.ts.map +1 -0
- package/dist/conformance/scope.js +78 -0
- package/dist/conformance/scope.js.map +1 -0
- package/dist/conformance/seed-contract.d.ts +11 -0
- package/dist/conformance/seed-contract.d.ts.map +1 -0
- package/dist/conformance/seed-contract.js +680 -0
- package/dist/conformance/seed-contract.js.map +1 -0
- package/dist/conformance/shapes.d.ts +35 -0
- package/dist/conformance/shapes.d.ts.map +1 -0
- package/dist/conformance/shapes.js +162 -0
- package/dist/conformance/shapes.js.map +1 -0
- package/dist/conformance/specs.d.ts +21 -0
- package/dist/conformance/specs.d.ts.map +1 -0
- package/dist/conformance/specs.js +306 -0
- package/dist/conformance/specs.js.map +1 -0
- package/dist/conformance/suite.d.ts +209 -0
- package/dist/conformance/suite.d.ts.map +1 -0
- package/dist/conformance/suite.js +1538 -0
- package/dist/conformance/suite.js.map +1 -0
- package/dist/conformance/unique.d.ts +7 -0
- package/dist/conformance/unique.d.ts.map +1 -0
- package/dist/conformance/unique.js +586 -0
- package/dist/conformance/unique.js.map +1 -0
- package/dist/conformance/worker.d.ts +3 -0
- package/dist/conformance/worker.d.ts.map +1 -0
- package/dist/conformance/worker.js +27 -0
- package/dist/conformance/worker.js.map +1 -0
- package/dist/index.d.ts +10 -3
- package/dist/index.js +6 -2
- package/dist/reference/defects.d.ts +71 -1
- package/dist/reference/defects.d.ts.map +1 -1
- package/dist/reference/defects.js +74 -8
- package/dist/reference/defects.js.map +1 -1
- package/dist/reference/dialect.d.ts +6 -4
- package/dist/reference/dialect.d.ts.map +1 -1
- package/dist/reference/dialect.js +1 -0
- package/dist/reference/dialect.js.map +1 -1
- package/dist/reference/http.d.ts +17 -25
- package/dist/reference/http.d.ts.map +1 -1
- package/dist/reference/http.js +574 -224
- package/dist/reference/http.js.map +1 -1
- package/dist/reference/listing.d.ts +51 -0
- package/dist/reference/listing.d.ts.map +1 -0
- package/dist/reference/listing.js +256 -0
- package/dist/reference/listing.js.map +1 -0
- package/dist/reference/model.d.ts +35 -1
- package/dist/reference/model.d.ts.map +1 -1
- package/dist/reference/model.js +42 -0
- package/dist/reference/model.js.map +1 -1
- package/dist/reference/query.d.ts +132 -43
- package/dist/reference/query.d.ts.map +1 -1
- package/dist/reference/query.js +397 -315
- package/dist/reference/query.js.map +1 -1
- package/dist/reference/shapes.d.ts +60 -0
- package/dist/reference/shapes.d.ts.map +1 -0
- package/dist/reference/shapes.js +142 -0
- package/dist/reference/shapes.js.map +1 -0
- package/dist/reference/spec.d.ts +9 -2
- package/dist/reference/spec.d.ts.map +1 -1
- package/dist/reference/spec.js +291 -71
- package/dist/reference/spec.js.map +1 -1
- package/dist/reference/store-api.d.ts +14 -43
- package/dist/reference/store-api.d.ts.map +1 -1
- package/dist/reference/store-api.js +8 -58
- package/dist/reference/store-api.js.map +1 -1
- package/dist/reference/stores/memory.d.ts +12 -15
- package/dist/reference/stores/memory.d.ts.map +1 -1
- package/dist/reference/stores/memory.js +22 -70
- package/dist/reference/stores/memory.js.map +1 -1
- package/dist/reference/stores/postgres.d.ts +11 -23
- package/dist/reference/stores/postgres.d.ts.map +1 -1
- package/dist/reference/stores/postgres.js +53 -334
- package/dist/reference/stores/postgres.js.map +1 -1
- package/dist/reference/stores/sql-compile.d.ts +36 -0
- package/dist/reference/stores/sql-compile.d.ts.map +1 -0
- package/dist/reference/stores/sql-compile.js +95 -0
- package/dist/reference/stores/sql-compile.js.map +1 -0
- package/dist/reference/stores/sqlite.d.ts +12 -27
- package/dist/reference/stores/sqlite.d.ts.map +1 -1
- package/dist/reference/stores/sqlite.js +54 -355
- package/dist/reference/stores/sqlite.js.map +1 -1
- package/dist/report/console.d.ts +2 -1
- package/dist/report/console.d.ts.map +1 -1
- package/dist/report/console.js +56 -10
- package/dist/report/console.js.map +1 -1
- package/dist/report/matrix.d.ts +10 -13
- package/dist/report/matrix.d.ts.map +1 -1
- package/dist/report/matrix.js +82 -18
- package/dist/report/matrix.js.map +1 -1
- package/dist/report/render.d.ts +18 -1
- package/dist/report/render.d.ts.map +1 -1
- package/dist/report/render.js +169 -14
- package/dist/report/render.js.map +1 -1
- package/dist/runtime/async.d.ts +16 -3
- package/dist/runtime/async.d.ts.map +1 -1
- package/dist/runtime/async.js +24 -19
- package/dist/runtime/async.js.map +1 -1
- package/dist/runtime/auth.d.ts +2 -9
- package/dist/runtime/auth.d.ts.map +1 -1
- package/dist/runtime/auth.js +38 -62
- package/dist/runtime/auth.js.map +1 -1
- package/dist/runtime/body.d.ts +15 -3
- package/dist/runtime/body.d.ts.map +1 -1
- package/dist/runtime/body.js +61 -12
- package/dist/runtime/body.js.map +1 -1
- package/dist/runtime/check-costs.d.ts +15 -0
- package/dist/runtime/check-costs.d.ts.map +1 -0
- package/dist/runtime/check-costs.js +114 -0
- package/dist/runtime/check-costs.js.map +1 -0
- package/dist/runtime/check-ids.d.ts +15 -0
- package/dist/runtime/check-ids.d.ts.map +1 -0
- package/dist/runtime/check-ids.js +140 -0
- package/dist/runtime/check-ids.js.map +1 -0
- package/dist/runtime/checks.d.ts +122 -18
- package/dist/runtime/checks.d.ts.map +1 -1
- package/dist/runtime/checks.js +3437 -1933
- package/dist/runtime/checks.js.map +1 -1
- package/dist/runtime/client.d.ts +112 -0
- package/dist/runtime/client.d.ts.map +1 -1
- package/dist/runtime/client.js +271 -39
- package/dist/runtime/client.js.map +1 -1
- package/dist/runtime/cookies.d.ts +16 -2
- package/dist/runtime/cookies.d.ts.map +1 -1
- package/dist/runtime/cookies.js +83 -22
- package/dist/runtime/cookies.js.map +1 -1
- package/dist/runtime/exchanges.d.ts +4 -5
- package/dist/runtime/exchanges.d.ts.map +1 -1
- package/dist/runtime/exchanges.js +33 -55
- package/dist/runtime/exchanges.js.map +1 -1
- package/dist/runtime/exit.d.ts +40 -0
- package/dist/runtime/exit.d.ts.map +1 -0
- package/dist/runtime/exit.js +48 -0
- package/dist/runtime/exit.js.map +1 -0
- package/dist/runtime/feature-gate.d.ts +0 -9
- package/dist/runtime/feature-gate.d.ts.map +1 -1
- package/dist/runtime/feature-gate.js +0 -21
- package/dist/runtime/feature-gate.js.map +1 -1
- package/dist/runtime/finding.d.ts +61 -7
- package/dist/runtime/finding.d.ts.map +1 -1
- package/dist/runtime/finding.js +74 -12
- package/dist/runtime/finding.js.map +1 -1
- package/dist/runtime/fixture.d.ts +35 -28
- package/dist/runtime/fixture.d.ts.map +1 -1
- package/dist/runtime/fixture.js +92 -731
- package/dist/runtime/fixture.js.map +1 -1
- package/dist/runtime/generate.d.ts +63 -0
- package/dist/runtime/generate.d.ts.map +1 -0
- package/dist/runtime/generate.js +635 -0
- package/dist/runtime/generate.js.map +1 -0
- package/dist/runtime/headers.d.ts +6 -0
- package/dist/runtime/headers.d.ts.map +1 -0
- package/dist/runtime/headers.js +16 -0
- package/dist/runtime/headers.js.map +1 -0
- package/dist/runtime/path.d.ts +7 -0
- package/dist/runtime/path.d.ts.map +1 -0
- package/dist/runtime/path.js +44 -0
- package/dist/runtime/path.js.map +1 -0
- package/dist/runtime/payloads.d.ts +2 -0
- package/dist/runtime/payloads.d.ts.map +1 -1
- package/dist/runtime/payloads.js +2 -1
- package/dist/runtime/payloads.js.map +1 -1
- package/dist/runtime/rate-limit.d.ts +6 -4
- package/dist/runtime/rate-limit.d.ts.map +1 -1
- package/dist/runtime/rate-limit.js +22 -33
- package/dist/runtime/rate-limit.js.map +1 -1
- package/dist/runtime/redact.d.ts +24 -0
- package/dist/runtime/redact.d.ts.map +1 -0
- package/dist/runtime/redact.js +98 -0
- package/dist/runtime/redact.js.map +1 -0
- package/dist/runtime/run.d.ts +51 -2
- package/dist/runtime/run.d.ts.map +1 -1
- package/dist/runtime/run.js +463 -195
- package/dist/runtime/run.js.map +1 -1
- package/dist/runtime/runs.d.ts +5 -0
- package/dist/runtime/runs.d.ts.map +1 -1
- package/dist/runtime/runs.js +7 -1
- package/dist/runtime/runs.js.map +1 -1
- package/dist/runtime/scope.d.ts +163 -0
- package/dist/runtime/scope.d.ts.map +1 -0
- package/dist/runtime/scope.js +423 -0
- package/dist/runtime/scope.js.map +1 -0
- package/dist/runtime/teardown.d.ts +17 -4
- package/dist/runtime/teardown.d.ts.map +1 -1
- package/dist/runtime/teardown.js +49 -16
- package/dist/runtime/teardown.js.map +1 -1
- package/dist/runtime/transcript.d.ts +34 -2
- package/dist/runtime/transcript.d.ts.map +1 -1
- package/dist/runtime/transcript.js +119 -24
- package/dist/runtime/transcript.js.map +1 -1
- package/dist/runtime/unique.d.ts +2 -11
- package/dist/runtime/unique.d.ts.map +1 -1
- package/dist/runtime/unique.js +27 -53
- package/dist/runtime/unique.js.map +1 -1
- package/dist/runtime/validate.d.ts +13 -2
- package/dist/runtime/validate.d.ts.map +1 -1
- package/dist/runtime/validate.js +51 -31
- package/dist/runtime/validate.js.map +1 -1
- package/dist/runtime/wait.d.ts +2 -1
- package/dist/runtime/wait.d.ts.map +1 -1
- package/dist/runtime/wait.js +2 -36
- package/dist/runtime/wait.js.map +1 -1
- package/dist/runtime/world.d.ts +31 -3
- package/dist/runtime/world.d.ts.map +1 -1
- package/dist/runtime/world.js +113 -32
- package/dist/runtime/world.js.map +1 -1
- package/dist/spec/collection.d.ts +8 -2
- package/dist/spec/collection.d.ts.map +1 -1
- package/dist/spec/collection.js +47 -7
- package/dist/spec/collection.js.map +1 -1
- package/dist/spec/extensions.d.ts +5 -1
- package/dist/spec/extensions.d.ts.map +1 -1
- package/dist/spec/extensions.js +9 -2
- package/dist/spec/extensions.js.map +1 -1
- package/dist/spec/graph.d.ts +46 -0
- package/dist/spec/graph.d.ts.map +1 -1
- package/dist/spec/graph.js +175 -11
- package/dist/spec/graph.js.map +1 -1
- package/dist/spec/load.d.ts +25 -5
- package/dist/spec/load.d.ts.map +1 -1
- package/dist/spec/load.js +160 -59
- package/dist/spec/load.js.map +1 -1
- package/dist/spec/query-capabilities.d.ts +4 -0
- package/dist/spec/query-capabilities.d.ts.map +1 -1
- package/dist/spec/query-capabilities.js +6 -0
- package/dist/spec/query-capabilities.js.map +1 -1
- package/dist/spec/schema.d.ts +20 -0
- package/dist/spec/schema.d.ts.map +1 -0
- package/dist/spec/schema.js +118 -0
- package/dist/spec/schema.js.map +1 -0
- package/dist/spec/tag-schema.d.ts +12 -0
- package/dist/spec/tag-schema.d.ts.map +1 -0
- package/dist/spec/tag-schema.js +143 -0
- package/dist/spec/tag-schema.js.map +1 -0
- package/dist/spec/types.js +17 -1
- package/dist/spec/types.js.map +1 -1
- package/docs/api.md +59 -0
- package/docs/assumptions.md +99 -0
- package/docs/checks.md +183 -0
- package/docs/commands.md +214 -0
- package/docs/configuration.md +650 -0
- package/docs/model.md +184 -0
- package/docs/reference-defects.md +94 -0
- package/docs/reports.md +211 -0
- package/docs/tags.md +400 -0
- package/package.json +3 -1
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One reading of an OpenAPI schema, shared by everything that interprets one.
|
|
3
|
+
*
|
|
4
|
+
* The validator, the value generator and the model each used to interpret JSON Schema on their
|
|
5
|
+
* own, and they disagreed: one stripped `example` anywhere it appeared — including a property
|
|
6
|
+
* *named* example — another ignored `writeOnly`, a third read OpenAPI 3.0's boolean
|
|
7
|
+
* `exclusiveMinimum` as a number. A schema is now translated once into JSON Schema 2020-12, for a
|
|
8
|
+
* stated direction, and every consumer reads the result.
|
|
9
|
+
*
|
|
10
|
+
* Direction matters because OpenAPI schemas are shared between requests and responses:
|
|
11
|
+
* `readOnly` properties never appear in a request and `writeOnly` ones never in a response.
|
|
12
|
+
*/
|
|
13
|
+
/** Keywords whose value is one subschema. */
|
|
14
|
+
const SINGLE = [
|
|
15
|
+
"items",
|
|
16
|
+
"additionalItems",
|
|
17
|
+
"additionalProperties",
|
|
18
|
+
"unevaluatedItems",
|
|
19
|
+
"unevaluatedProperties",
|
|
20
|
+
"not",
|
|
21
|
+
"if",
|
|
22
|
+
"then",
|
|
23
|
+
"else",
|
|
24
|
+
"contains",
|
|
25
|
+
"propertyNames",
|
|
26
|
+
];
|
|
27
|
+
/** Keywords whose value is a list of subschemas. */
|
|
28
|
+
const LIST = ["allOf", "anyOf", "oneOf", "prefixItems"];
|
|
29
|
+
/** Keywords whose value maps names to subschemas. */
|
|
30
|
+
const MAP = ["properties", "patternProperties", "$defs", "definitions", "dependentSchemas"];
|
|
31
|
+
/** OpenAPI annotations that are not JSON Schema keywords — removed from schema nodes only. */
|
|
32
|
+
const OPENAPI_ONLY = new Set(["nullable", "example", "discriminator", "xml", "externalDocs"]);
|
|
33
|
+
function isSchema(value) {
|
|
34
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
35
|
+
}
|
|
36
|
+
/** Translates `schema` into JSON Schema 2020-12 as seen in `direction`. Never mutates its input. */
|
|
37
|
+
export function normalizeSchema(schema, options) {
|
|
38
|
+
const seen = new Map();
|
|
39
|
+
const visit = (node) => {
|
|
40
|
+
const cached = seen.get(node);
|
|
41
|
+
if (cached !== undefined)
|
|
42
|
+
return cached;
|
|
43
|
+
const out = {};
|
|
44
|
+
seen.set(node, out);
|
|
45
|
+
for (const [key, value] of Object.entries(node)) {
|
|
46
|
+
if (OPENAPI_ONLY.has(key))
|
|
47
|
+
continue;
|
|
48
|
+
if (SINGLE.includes(key) && isSchema(value))
|
|
49
|
+
out[key] = visit(value);
|
|
50
|
+
else if (LIST.includes(key) && Array.isArray(value)) {
|
|
51
|
+
out[key] = value.map((item) => (isSchema(item) ? visit(item) : item));
|
|
52
|
+
}
|
|
53
|
+
else if (key === "items" && Array.isArray(value)) {
|
|
54
|
+
/* Draft-04 tuple form: positional item schemas. */
|
|
55
|
+
out.prefixItems = value.map((item) => (isSchema(item) ? visit(item) : item));
|
|
56
|
+
}
|
|
57
|
+
else if (MAP.includes(key) && isSchema(value)) {
|
|
58
|
+
const map = {};
|
|
59
|
+
for (const [name, sub] of Object.entries(value))
|
|
60
|
+
map[name] = isSchema(sub) ? visit(sub) : sub;
|
|
61
|
+
out[key] = map;
|
|
62
|
+
}
|
|
63
|
+
else
|
|
64
|
+
out[key] = value;
|
|
65
|
+
}
|
|
66
|
+
/* OpenAPI 3.0 exclusive bounds are booleans qualifying `minimum` / `maximum`. */
|
|
67
|
+
for (const [flag, bound] of [
|
|
68
|
+
["exclusiveMinimum", "minimum"],
|
|
69
|
+
["exclusiveMaximum", "maximum"],
|
|
70
|
+
]) {
|
|
71
|
+
if (typeof out[flag] !== "boolean")
|
|
72
|
+
continue;
|
|
73
|
+
if (out[flag] === true && typeof out[bound] === "number") {
|
|
74
|
+
out[flag] = out[bound];
|
|
75
|
+
delete out[bound];
|
|
76
|
+
}
|
|
77
|
+
else
|
|
78
|
+
delete out[flag];
|
|
79
|
+
}
|
|
80
|
+
/* Properties that do not travel in this direction are neither allowed nor required. */
|
|
81
|
+
const hidden = options.direction === "request" ? "readOnly" : "writeOnly";
|
|
82
|
+
if (isSchema(out.properties)) {
|
|
83
|
+
const properties = { ...out.properties };
|
|
84
|
+
const dropped = Object.entries(properties)
|
|
85
|
+
.filter(([, sub]) => isSchema(sub) && sub[hidden] === true)
|
|
86
|
+
.map(([name]) => name);
|
|
87
|
+
for (const name of dropped)
|
|
88
|
+
delete properties[name];
|
|
89
|
+
out.properties = properties;
|
|
90
|
+
if (Array.isArray(out.required))
|
|
91
|
+
out.required = out.required.filter((name) => !dropped.includes(String(name)));
|
|
92
|
+
}
|
|
93
|
+
/* `nullable` widens whatever the schema otherwise allows. 3.1 has no such keyword, but
|
|
94
|
+
* generators still emit it, and reading it there costs nothing. */
|
|
95
|
+
if (node.nullable === true)
|
|
96
|
+
return nullable(out);
|
|
97
|
+
return out;
|
|
98
|
+
};
|
|
99
|
+
return visit(schema);
|
|
100
|
+
}
|
|
101
|
+
/** `schema` or null — by widening `type` and `enum` where that is exact, by a union where not. */
|
|
102
|
+
function nullable(schema) {
|
|
103
|
+
const type = schema.type;
|
|
104
|
+
const typed = typeof type === "string" || Array.isArray(type);
|
|
105
|
+
const composite = ["allOf", "anyOf", "oneOf", "$ref", "not", "const"].some((key) => key in schema);
|
|
106
|
+
if (typed && !composite) {
|
|
107
|
+
const out = { ...schema };
|
|
108
|
+
const types = Array.isArray(type) ? [...type] : [type];
|
|
109
|
+
if (!types.includes("null"))
|
|
110
|
+
types.push("null");
|
|
111
|
+
out.type = types;
|
|
112
|
+
if (Array.isArray(out.enum) && !out.enum.includes(null))
|
|
113
|
+
out.enum = [...out.enum, null];
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
return { anyOf: [schema, { type: "null" }] };
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/spec/schema.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every oat tag, checked against its shape.
|
|
3
|
+
*
|
|
4
|
+
* Readers stay defensive — a tag they cannot use is ignored — but ignoring it silently meant a
|
|
5
|
+
* typo read as an absence: `x-destructive: yes` was simply not destructive. A tag that is present
|
|
6
|
+
* and malformed is a gap naming the tag and what is wrong with it, so `doctor` shows it.
|
|
7
|
+
*/
|
|
8
|
+
import type { GapCollector } from "./extensions.ts";
|
|
9
|
+
import type { OperationObject } from "./types.ts";
|
|
10
|
+
/** Records a gap for every oat tag on `op` (and its parameters) that is present but malformed. */
|
|
11
|
+
export declare function validateTags(op: OperationObject, operationId: string, gaps: GapCollector): void;
|
|
12
|
+
//# sourceMappingURL=tag-schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tag-schema.d.ts","sourceRoot":"","sources":["../../src/spec/tag-schema.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AACnD,OAAO,KAAK,EAAE,eAAe,EAAmB,MAAM,YAAY,CAAA;AA6IlE,kGAAkG;AAClG,wBAAgB,YAAY,CAAC,EAAE,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,GAAG,IAAI,CAa/F"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every oat tag, checked against its shape.
|
|
3
|
+
*
|
|
4
|
+
* Readers stay defensive — a tag they cannot use is ignored — but ignoring it silently meant a
|
|
5
|
+
* typo read as an absence: `x-destructive: yes` was simply not destructive. A tag that is present
|
|
6
|
+
* and malformed is a gap naming the tag and what is wrong with it, so `doctor` shows it.
|
|
7
|
+
*/
|
|
8
|
+
const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
9
|
+
const ROUTE = /^[A-Za-z]+\s+\/\S*$/;
|
|
10
|
+
const string = (value) => (typeof value === "string" && value !== "" ? null : "must be a non-empty string");
|
|
11
|
+
const boolean = (value) => (typeof value === "boolean" ? null : "must be true or false");
|
|
12
|
+
const route = (value) => typeof value === "string" && ROUTE.test(value.trim()) ? null : 'must be a route like "GET /path"';
|
|
13
|
+
const strings = (value) => Array.isArray(value) && value.every((item) => typeof item === "string") ? null : "must be an array of strings";
|
|
14
|
+
const oneOf = (...allowed) => (value) => allowed.includes(value) ? null : `must be one of ${allowed.map((a) => `"${a}"`).join(", ")}`;
|
|
15
|
+
const positive = (value) => (typeof value === "number" && value > 0 ? null : "must be a positive number");
|
|
16
|
+
const integer = (value) => typeof value === "number" && Number.isInteger(value) && value > 0 ? null : "must be a positive integer";
|
|
17
|
+
const stringMap = (value) => isObject(value) && Object.values(value).every((item) => typeof item === "string") ? null : "must map names to strings";
|
|
18
|
+
/** An object with these fields; `required` must be present, the rest are checked when present. */
|
|
19
|
+
const shape = (fields, required = []) => (value) => {
|
|
20
|
+
if (!isObject(value))
|
|
21
|
+
return "must be an object";
|
|
22
|
+
for (const key of required)
|
|
23
|
+
if (value[key] === undefined)
|
|
24
|
+
return `needs "${key}"`;
|
|
25
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
26
|
+
const check = fields[key];
|
|
27
|
+
if (check === undefined || entry === undefined)
|
|
28
|
+
continue;
|
|
29
|
+
const problem = check(entry);
|
|
30
|
+
if (problem !== null)
|
|
31
|
+
return `"${key}" ${problem}`;
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
};
|
|
35
|
+
/** One query axis: field names, structured rows, or `null` for an explicit "none". */
|
|
36
|
+
const axis = (row) => {
|
|
37
|
+
const fieldRow = shape(row, ["field"]);
|
|
38
|
+
return (value) => {
|
|
39
|
+
if (value === null)
|
|
40
|
+
return null;
|
|
41
|
+
if (!Array.isArray(value))
|
|
42
|
+
return "must be an array of field names or { field } rows";
|
|
43
|
+
for (const item of value) {
|
|
44
|
+
if (typeof item === "string")
|
|
45
|
+
continue;
|
|
46
|
+
const problem = fieldRow(item);
|
|
47
|
+
if (problem !== null)
|
|
48
|
+
return `has a row that ${problem}`;
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
const harvest = shape({ operationId: string, path: string, typeMap: stringMap, typePath: string }, [
|
|
54
|
+
"operationId",
|
|
55
|
+
"path",
|
|
56
|
+
]);
|
|
57
|
+
/** `x-query`, as `docs/tags.md` documents it. */
|
|
58
|
+
const xQuery = shape({
|
|
59
|
+
aliases: stringMap,
|
|
60
|
+
defaultOrder: string,
|
|
61
|
+
emptyIn: oneOf("reject", "match-none"),
|
|
62
|
+
filterable: axis({ field: string, ops: strings, type: string }),
|
|
63
|
+
filterableFrom: harvest,
|
|
64
|
+
grammar: oneOf("postgrest", "colon", "equality"),
|
|
65
|
+
identityFilter: string,
|
|
66
|
+
maxFilterConditions: integer,
|
|
67
|
+
maxInValues: integer,
|
|
68
|
+
maxLimit: integer,
|
|
69
|
+
maxSortKeys: integer,
|
|
70
|
+
operators: strings,
|
|
71
|
+
operatorsByType: (value) => isObject(value) && Object.values(value).every((ops) => strings(ops) === null)
|
|
72
|
+
? null
|
|
73
|
+
: "must map type names to arrays of operators",
|
|
74
|
+
searchable: axis({ field: string }),
|
|
75
|
+
searchableFrom: harvest,
|
|
76
|
+
searchEmpty: oneOf("ignore", "match-all", "reject"),
|
|
77
|
+
searchModes: strings,
|
|
78
|
+
selectable: axis({ field: string }),
|
|
79
|
+
selectableFrom: harvest,
|
|
80
|
+
selectNested: boolean,
|
|
81
|
+
selectUnknown: oneOf("reject", "ignore"),
|
|
82
|
+
sortable: axis({ field: string, nulls: strings, type: string }),
|
|
83
|
+
sortableFrom: harvest,
|
|
84
|
+
sortCollation: oneOf("binary", "case-insensitive", "locale"),
|
|
85
|
+
sortDefaultNulls: oneOf("first", "last"),
|
|
86
|
+
sortNulls: strings,
|
|
87
|
+
stableTiebreak: string,
|
|
88
|
+
});
|
|
89
|
+
const OPERATION_TAGS = {
|
|
90
|
+
"x-async": shape({ idFrom: string, poll: string, pollIntervalMs: positive, successWhen: string, timeoutMs: positive, until: string }, ["poll"]),
|
|
91
|
+
"x-cleanup": route,
|
|
92
|
+
"x-cost": oneOf("low", "medium", "high"),
|
|
93
|
+
"x-destructive": boolean,
|
|
94
|
+
"x-entity": shape({
|
|
95
|
+
action: oneOf("create", "list", "read", "update", "delete", "action"),
|
|
96
|
+
identity: string,
|
|
97
|
+
name: string,
|
|
98
|
+
}, ["name", "action"]),
|
|
99
|
+
"x-feature-gate": string,
|
|
100
|
+
"x-fresh-principal": boolean,
|
|
101
|
+
"x-generated": strings,
|
|
102
|
+
"x-idempotent": boolean,
|
|
103
|
+
"x-immutable": strings,
|
|
104
|
+
"x-invalidate": (value) => Array.isArray(value) && value.every((item) => route(item) === null)
|
|
105
|
+
? null
|
|
106
|
+
: 'must be an array of routes like "GET /path"',
|
|
107
|
+
"x-invite": shape({
|
|
108
|
+
accept: string,
|
|
109
|
+
acceptFrom: oneOf("link"),
|
|
110
|
+
grantPointer: string,
|
|
111
|
+
granteeField: string,
|
|
112
|
+
invite: string,
|
|
113
|
+
revoke: string,
|
|
114
|
+
tokenFrom: oneOf("response", "outOfBand"),
|
|
115
|
+
tokenKind: string,
|
|
116
|
+
tokenPointer: string,
|
|
117
|
+
}, ["invite", "accept", "revoke"]),
|
|
118
|
+
"x-query": xQuery,
|
|
119
|
+
"x-rate-limit": shape({ category: string, rps: positive }, ["category"]),
|
|
120
|
+
"x-soft-delete": string,
|
|
121
|
+
"x-tenant": string,
|
|
122
|
+
"x-wait": shape({ operationId: string, pollIntervalMs: positive, timeoutMs: positive, until: string }, [
|
|
123
|
+
"operationId",
|
|
124
|
+
]),
|
|
125
|
+
};
|
|
126
|
+
/** Records a gap for every oat tag on `op` (and its parameters) that is present but malformed. */
|
|
127
|
+
export function validateTags(op, operationId, gaps) {
|
|
128
|
+
const record = op;
|
|
129
|
+
for (const [tag, check] of Object.entries(OPERATION_TAGS)) {
|
|
130
|
+
if (record[tag] === undefined)
|
|
131
|
+
continue;
|
|
132
|
+
const problem = check(record[tag]);
|
|
133
|
+
if (problem !== null)
|
|
134
|
+
gaps.record(operationId, tag, `${tag} ${problem}; oat cannot use it`);
|
|
135
|
+
}
|
|
136
|
+
for (const parameter of (op.parameters ?? [])) {
|
|
137
|
+
const root = parameter["x-root"];
|
|
138
|
+
if (root !== undefined && typeof root !== "boolean") {
|
|
139
|
+
gaps.record(operationId, "x-root", `x-root on "${parameter.name}" must be true or false; oat cannot use it`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=tag-schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tag-schema.js","sourceRoot":"","sources":["../../src/spec/tag-schema.ts"],"names":[],"mappings":"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"}
|
package/dist/spec/types.js
CHANGED
|
@@ -10,7 +10,7 @@ export function listEndpoints(doc) {
|
|
|
10
10
|
const op = item[method];
|
|
11
11
|
if (op === null || typeof op !== "object")
|
|
12
12
|
continue;
|
|
13
|
-
const operation = op;
|
|
13
|
+
const operation = withPathParameters(op, item.parameters);
|
|
14
14
|
out.push({
|
|
15
15
|
method,
|
|
16
16
|
op: operation,
|
|
@@ -21,4 +21,20 @@ export function listEndpoints(doc) {
|
|
|
21
21
|
}
|
|
22
22
|
return out;
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* An operation with its path item's `parameters` folded in.
|
|
26
|
+
*
|
|
27
|
+
* A path item may declare parameters once for every operation under it, and an operation may
|
|
28
|
+
* override one by name and location. Reading only the operation's own list lost every shared
|
|
29
|
+
* path parameter, and with it the declared schema of each identifier.
|
|
30
|
+
*/
|
|
31
|
+
function withPathParameters(op, shared) {
|
|
32
|
+
if (!Array.isArray(shared) || shared.length === 0)
|
|
33
|
+
return op;
|
|
34
|
+
const own = op.parameters ?? [];
|
|
35
|
+
const key = (p) => `${String(p.in)}:${String(p.name)}`;
|
|
36
|
+
const overridden = new Set(own.map(key));
|
|
37
|
+
const inherited = shared.filter((parameter) => parameter !== null && typeof parameter === "object" && !overridden.has(key(parameter)));
|
|
38
|
+
return inherited.length === 0 ? op : { ...op, parameters: [...inherited, ...own] };
|
|
39
|
+
}
|
|
24
40
|
//# sourceMappingURL=types.js.map
|
package/dist/spec/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/spec/types.ts"],"names":[],"mappings":"AAAA,gFAAgF;AA8ChF,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,CAAU,CAAA;AAGjG,MAAM,CAAC,MAAM,gBAAgB,GAAwB,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAA;AAUhG,MAAM,UAAU,aAAa,CAAC,GAAoB;IACjD,MAAM,GAAG,GAAe,EAAE,CAAA;IAC1B,KAAK,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC,EAAE,CAAC;QAC5D,IAAI,IAAI,KAAK,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ;YAAE,SAAQ;QACvD,KAAK,MAAM,MAAM,IAAI,YAAY,EAAE,CAAC;YACnC,MAAM,EAAE,GAAI,IAAgC,CAAC,MAAM,CAAC,CAAA;YACpD,IAAI,EAAE,KAAK,IAAI,IAAI,OAAO,EAAE,KAAK,QAAQ;gBAAE,SAAQ;YACnD,MAAM,SAAS,GAAG,EAAqB,CAAA;
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/spec/types.ts"],"names":[],"mappings":"AAAA,gFAAgF;AA8ChF,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,CAAU,CAAA;AAGjG,MAAM,CAAC,MAAM,gBAAgB,GAAwB,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAA;AAUhG,MAAM,UAAU,aAAa,CAAC,GAAoB;IACjD,MAAM,GAAG,GAAe,EAAE,CAAA;IAC1B,KAAK,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC,EAAE,CAAC;QAC5D,IAAI,IAAI,KAAK,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ;YAAE,SAAQ;QACvD,KAAK,MAAM,MAAM,IAAI,YAAY,EAAE,CAAC;YACnC,MAAM,EAAE,GAAI,IAAgC,CAAC,MAAM,CAAC,CAAA;YACpD,IAAI,EAAE,KAAK,IAAI,IAAI,OAAO,EAAE,KAAK,QAAQ;gBAAE,SAAQ;YACnD,MAAM,SAAS,GAAG,kBAAkB,CAAC,EAAqB,EAAG,IAAiC,CAAC,UAAU,CAAC,CAAA;YAC1G,GAAG,CAAC,IAAI,CAAC;gBACR,MAAM;gBACN,EAAE,EAAE,SAAS;gBACb,WAAW,EAAE,SAAS,CAAC,WAAW,IAAI,GAAG,MAAM,CAAC,WAAW,EAAE,IAAI,IAAI,EAAE;gBACvE,IAAI;aACJ,CAAC,CAAA;QACH,CAAC;IACF,CAAC;IACD,OAAO,GAAG,CAAA;AACX,CAAC;AAED;;;;;;GAMG;AACH,SAAS,kBAAkB,CAAC,EAAmB,EAAE,MAAe;IAC/D,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAA;IAC5D,MAAM,GAAG,GAAG,EAAE,CAAC,UAAU,IAAI,EAAE,CAAA;IAC/B,MAAM,GAAG,GAAG,CAAC,CAAmC,EAAU,EAAE,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAA;IAChG,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAA;IACxC,MAAM,SAAS,GAAI,MAA4B,CAAC,MAAM,CACrD,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,KAAK,IAAI,IAAI,OAAO,SAAS,KAAK,QAAQ,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CACrG,CAAA;IACD,OAAO,SAAS,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,EAAE,UAAU,EAAE,CAAC,GAAG,SAAS,EAAE,GAAG,GAAG,CAAC,EAAE,CAAA;AACnF,CAAC"}
|
package/docs/api.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# oat — Programmatic API
|
|
2
|
+
|
|
3
|
+
[← back to the overview](../README.md)
|
|
4
|
+
|
|
5
|
+
## Programmatic API
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import {
|
|
9
|
+
defineConfig,
|
|
10
|
+
loadConfig,
|
|
11
|
+
run,
|
|
12
|
+
loadSpec,
|
|
13
|
+
dereference,
|
|
14
|
+
buildModel,
|
|
15
|
+
renderJson,
|
|
16
|
+
renderMarkdown,
|
|
17
|
+
} from "@lovrozagar/oat"
|
|
18
|
+
|
|
19
|
+
const config = defineConfig({
|
|
20
|
+
spec: "./openapi.yaml",
|
|
21
|
+
baseUrl: "https://api.example.com",
|
|
22
|
+
principals: [{ id: "alpha", headers: { authorization: `Bearer ${process.env.API_TOKEN}` } }],
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const result = await run({
|
|
26
|
+
spec: config.spec,
|
|
27
|
+
baseUrl: config.baseUrl,
|
|
28
|
+
principals: config.principals,
|
|
29
|
+
seed: 1,
|
|
30
|
+
maxInFlight: 4,
|
|
31
|
+
onProgress: (snap) => {
|
|
32
|
+
// snap.phase, snap.entity, snap.check, snap.inflight, snap.last, snap.message, …
|
|
33
|
+
},
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
// result.findings, result.checksSkipped, result.checksSuppressed,
|
|
37
|
+
// result.inconclusive, result.entitiesTested, result.teardown, result.created,
|
|
38
|
+
// result.scope (per-operation status), result.checksOutOfScope
|
|
39
|
+
// pass `ops: ["product.update"]` to run() for a targeted run; ScopeError on an unknown name
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`loadConfig(path)` loads `.ts` / `.js` / `.mjs` / `.json` the same way the CLI does. The CLI then expands `${NAME}` in every string. `defineConfig` is an identity function for typing; it does not interpolate. If you call `run()` with an in-process object, resolve secrets yourself (template literals, `process.env`) before passing it.
|
|
43
|
+
|
|
44
|
+
`runVerdict(result)` is the verdict the CLI and every report state: `{ outcome, reason }`, where
|
|
45
|
+
`outcome` is `clean`, `defects` or `failed` (a run that graded nothing, a network that went away, or
|
|
46
|
+
a `--ops` target never judged). `exitCode(result)` is that outcome as the CLI's exit code, from
|
|
47
|
+
`EXIT`: 0, 1 or 3.
|
|
48
|
+
|
|
49
|
+
`run(options)` does not write files. The CLI writes reports after `run` returns. To produce the same artifacts, call `renderMarkdown` / `renderJson` / `renderMatrixHtml` / `renderMatrixGraph` / `renderRepros` with a `ReportInput` (`findings`, `model`, `client`, `baseUrl`, `entitiesTested`, `checksRun`, `startedAt`, `durationMs`, plus optional skip/suppress/inconclusive lists).
|
|
50
|
+
|
|
51
|
+
Offline:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
const doc = await loadSpec("./openapi.yaml")
|
|
55
|
+
const { doc: resolved, externalRefs } = dereference(doc)
|
|
56
|
+
const model = buildModel(resolved)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Types exported: `OatConfig`, `Principal`, `AuthFlow`, `AuthRefresh`, `AuthStep`, `HookAuth`, `Hooks`, `Uploads`, `UploadRequest`, `UploadFile`, `HeaderRequest`, `InputRequest`, `OriginSpec`, `OutOfBandConfig`, `OutOfBandRequest`, `TeardownPrincipalContext`, `RunOptions`, `RunResult`, `Finding`, `Verdict`, `Actor`, `SpecModel`, `EntityModel`, `OperationModel`, `OpenApiDocument`, `AuthRefreshRequiredError`, `loadPersistedPrincipals`, `worstCaseWaitMs`, `allocateRunDir`, `DEFAULT_RUNS_ROOT`, matrix types.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# oat — Assumptions
|
|
2
|
+
|
|
3
|
+
[← back to the overview](../README.md)
|
|
4
|
+
|
|
5
|
+
oat tests a backend against its own document, and where the document is silent it has to assume
|
|
6
|
+
something. This page lists every such assumption, what oat does when an API does not fit it, and
|
|
7
|
+
how to declare the exception. An assumption that does not hold should never become a false
|
|
8
|
+
finding: it should become a skip, an inconclusive verdict or a coverage gap that names the tag that
|
|
9
|
+
would settle it. If one of these produces a finding against a correct backend, that is an oat bug.
|
|
10
|
+
|
|
11
|
+
## Shape of the API
|
|
12
|
+
|
|
13
|
+
| oat assumes | when it does not hold | declare |
|
|
14
|
+
| ----------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
|
|
15
|
+
| A resource is a noun in the path: `/tables`, `/tables/{id}` | the operation is untracked; `doctor` names the gap | [`x-entity`](tags.md#x-entity) `{ name, action }` |
|
|
16
|
+
| An instance is identified by `id`, `uuid`, `slug`, `key` or `name`, or by the trailing path parameter | the entity is not trackable, and checks that need a record stand down | `x-entity.identity` |
|
|
17
|
+
| Creating is `POST` to the collection, and the response carries the created record | the cohort cannot be seeded; dependent checks are blocked | [`x-async`](tags.md#x-async) for a receipt, `x-entity` for a create elsewhere |
|
|
18
|
+
| A record oat created can be deleted by its item route | teardown reports what it left behind | [`x-cleanup`](tags.md#x-cleanup) |
|
|
19
|
+
| Path parameters oat cannot create are supplied | the entity cannot be reached | [`x-root`](tags.md#x-root), or `roots` in the config |
|
|
20
|
+
| A list answers with an array, or an object holding one | list checks stand down | the collection schema |
|
|
21
|
+
|
|
22
|
+
## Lists and paging
|
|
23
|
+
|
|
24
|
+
| oat assumes | when it does not hold | declare |
|
|
25
|
+
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------ | --------------------------------------------------------------------------- |
|
|
26
|
+
| Paging parameters carry recognisable names (`limit`, `page`, `offset`, `cursor`, …) | the role is unresolved and the paging checks do not apply | the parameter's name or default, or [`x-query`](tags.md#x-query) `maxLimit` |
|
|
27
|
+
| A set has ended when a page is empty, shorter than a full page already seen, or the envelope says so — and, when the envelope carries both a total and a more-pages flag, they agree | oat keeps reading; it never takes the requested page size as proof | nothing: a server may serve fewer rows than asked for |
|
|
28
|
+
| A server pages by one mechanism consistently | a read that sees pages repeat stops and is unresolved | — |
|
|
29
|
+
| A set larger than 500 records is too large to compare whole | checks that need the whole set are unresolved, not failed | smaller cohorts, or a filtered scope |
|
|
30
|
+
|
|
31
|
+
## Queries
|
|
32
|
+
|
|
33
|
+
| oat assumes | when it does not hold | declare |
|
|
34
|
+
| ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | -------------------------------------------------- |
|
|
35
|
+
| Filtering is written in a grammar the document demonstrates (PostgREST, colon, equality) | filter checks do not apply | `x-query.grammar` |
|
|
36
|
+
| Without `x-query`, every scalar property can be filtered, sorted and selected | probes reach fields the backend never indexed; findings name the guess | `x-query` `filterable` / `sortable` / `selectable` |
|
|
37
|
+
| A declared operator is accepted for a value of the field's own type | — | per-field `ops` and `type` in `x-query` |
|
|
38
|
+
| Only 400 and 422 mean "this capability is not supported" | 401, 403, 404 and 429 leave the check unresolved; a 5xx is a finding | — |
|
|
39
|
+
| An alias of an allowed operator is allowed | — | `x-query.aliases` |
|
|
40
|
+
|
|
41
|
+
## Ordering
|
|
42
|
+
|
|
43
|
+
| oat assumes | when it does not hold | declare |
|
|
44
|
+
| --------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | -------------------------- |
|
|
45
|
+
| Numbers sort numerically | `sort.numeric-order-is-numeric` reports text order | — |
|
|
46
|
+
| Text sorts by one consistent collation — binary, case-insensitive or locale — whichever the backend shows first | a sequence sorted under none of them is a finding | `x-query.sortCollation` |
|
|
47
|
+
| Nulls sit together at one end, the same end every time | nulls in the middle, or switching ends, is a finding | `x-query.sortDefaultNulls` |
|
|
48
|
+
| Descending is ascending reversed, up to ties | — | — |
|
|
49
|
+
| Tied records may come back in any order unless a tiebreak is declared | — | `x-query.stableTiebreak` |
|
|
50
|
+
|
|
51
|
+
## Writes
|
|
52
|
+
|
|
53
|
+
| oat assumes | when it does not hold | declare |
|
|
54
|
+
| --------------------------------------------------------------------- | -------------------------------------------------- | ---------------------------------------------------------------- |
|
|
55
|
+
| `PATCH` changes only the fields it names | the minimality and lost-update checks report it | — |
|
|
56
|
+
| `PUT` replaces the whole record | partial-update checks stand down on a PUT-only API | the update operation's method |
|
|
57
|
+
| A field the server owns is marked `readOnly` or generated | oat would try to write it | `readOnly`, or [`x-generated`](tags.md#x-immutable--x-generated) |
|
|
58
|
+
| A field that must never change after create is declared | not tested | [`x-immutable`](tags.md#x-immutable--x-generated) |
|
|
59
|
+
| A `writeOnly` field is accepted and not returned | — | `writeOnly` |
|
|
60
|
+
| A header named like `Idempotency-Key` promises replay | the replay check does not apply | the header's name |
|
|
61
|
+
| A unique column set is declared, and a duplicate is refused with 409 | — | [`x-unique`](tags.md#x-unique) |
|
|
62
|
+
| A deleted record leaves the default listing, unless it is a tombstone | — | [`x-soft-delete`](tags.md#x-soft-delete) |
|
|
63
|
+
|
|
64
|
+
## Errors and statuses
|
|
65
|
+
|
|
66
|
+
| oat assumes | when it does not hold | declare |
|
|
67
|
+
| ------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ------------------------------------------ |
|
|
68
|
+
| A refusal of invalid input is 400 or 422; of a wrong content type, 415 (when documented); of a conflict, 409 | any other refusal leaves the check unresolved | — |
|
|
69
|
+
| A 5xx is never the right answer to bad input | it is a finding | — |
|
|
70
|
+
| Every status an operation returns is documented, ranges (`2XX`) and `default` included | the status check reports the gap, apart from what deliberately invalid probes drew | the response |
|
|
71
|
+
| A documented feature gate answers 403 with `vars.type: feature_gate` | the 403 is a backend failure | [`x-feature-gate`](tags.md#x-feature-gate) |
|
|
72
|
+
|
|
73
|
+
## Isolation and authorization
|
|
74
|
+
|
|
75
|
+
| oat assumes | when it does not hold | declare |
|
|
76
|
+
| ----------------------------------------------------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------ |
|
|
77
|
+
| A second principal with different `roots` is another tenant | isolation checks do not apply | `principals` |
|
|
78
|
+
| The tenant is a path parameter, or declared | an inferred tenant makes a cross-tenant read an ambiguity, not a security finding | [`x-tenant`](tags.md#x-tenant) |
|
|
79
|
+
| A denial does not depend on whether the record exists | — | — |
|
|
80
|
+
| Higher `rank` can do everything a lower rank can | a lower rank seeing more is a finding | `rank` on principals |
|
|
81
|
+
| An invite grants nothing until accepted, and a revoke takes access away | — | [`x-invite`](tags.md#x-invite) and a peer's `inviteAs` |
|
|
82
|
+
|
|
83
|
+
## Async work, effects and pacing
|
|
84
|
+
|
|
85
|
+
| oat assumes | when it does not hold | declare |
|
|
86
|
+
| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------ | ------------------------------------------------------------------------ |
|
|
87
|
+
| A write is complete when it answers | — | [`x-async`](tags.md#x-async), [`x-wait`](tags.md#x-wait) |
|
|
88
|
+
| A write affects only its own entity and the routes it declares | other entities' changes are not tested | [`x-invalidate`](tags.md#x-invalidate), [`x-effects`](tags.md#x-effects) |
|
|
89
|
+
| Entities that share no parent, invalidated route or effect cannot observe each other | they are tested side by side | the shared relation, declared |
|
|
90
|
+
| A rate the document declares is the rate the backend enforces | a 429 under it is a finding; a config-supplied rate is only paced around | [`x-rate-limit`](tags.md#x-rate-limit--pacing-oats-own-traffic) |
|
|
91
|
+
|
|
92
|
+
## What oat never assumes
|
|
93
|
+
|
|
94
|
+
- That your data has a ground truth. Every oracle compares the API with itself.
|
|
95
|
+
- That the requested page size is the size served.
|
|
96
|
+
- That a field absent from the cohort cannot exist: a declared field is probed with a value of its
|
|
97
|
+
own type.
|
|
98
|
+
- That an unanswered question is a pass: a check that cannot conclude says why, and a check that
|
|
99
|
+
does not apply says what it needs.
|