@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
package/docs/tags.md
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
# oat — OpenAPI meta tags
|
|
2
|
+
|
|
3
|
+
[← back to the overview](../README.md)
|
|
4
|
+
|
|
5
|
+
## OpenAPI meta tags
|
|
6
|
+
|
|
7
|
+
Vendor-neutral `x-*` extensions. Every one is optional. Precedence: **explicit tag → heuristic → skip with a coverage gap**.
|
|
8
|
+
|
|
9
|
+
A complete document with every tag in place is shipped as `labs/annotated-openapi.yaml` (also in the npm package). `oat conformance` asserts the derived model matches that file.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
oat plan --spec node_modules/@lovrozagar/oat/labs/annotated-openapi.yaml
|
|
13
|
+
oat doctor --spec node_modules/@lovrozagar/oat/labs/annotated-openapi.yaml
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
What each tag **unlocks** (otherwise the check cannot run):
|
|
17
|
+
|
|
18
|
+
| tag | checks unlocked |
|
|
19
|
+
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
20
|
+
| `x-async` | `async.reaches-terminal-state`, `async.receipt-identifies-the-job` |
|
|
21
|
+
| `x-effects` | `effects.declared-effect-occurs` |
|
|
22
|
+
| `x-immutable` | `patch.immutable-field-rejected` |
|
|
23
|
+
| `x-invalidate` | `invalidation.declared-route-changes` (when the list names another entity) |
|
|
24
|
+
| `x-query` | `spec.declared-filterable-is-filterable`, `spec.declared-sortable-is-sortable`, `spec.declared-selectable-is-selectable`, plus the declared-or-skip catalog checks (`in` / `ilike` / `is` / nulls / caps / …) |
|
|
25
|
+
| `x-soft-delete` | `softdelete.absent-from-default-list` |
|
|
26
|
+
| `x-invite` | `auth.invite-grants-then-revokes` |
|
|
27
|
+
| `x-wait` | `effects.side-effect-arrives` |
|
|
28
|
+
| `x-unique` | `create.unique-conflict-rejected`, `update.unique-conflict-rejected` |
|
|
29
|
+
|
|
30
|
+
What each tag **sharpens** (the check already runs, but the verdict changes):
|
|
31
|
+
|
|
32
|
+
| tag | without it |
|
|
33
|
+
| ---------- | --------------------------------------------------------------------------------------------------------------------------- |
|
|
34
|
+
| `x-query` | every scalar is probed, including columns you never indexed — expect findings you will dismiss |
|
|
35
|
+
| `x-tenant` | inferred tenant: a cross-tenant read is `AMBIGUITY`, not `SECURITY`. No tenant tagged or inferred: the check does not apply |
|
|
36
|
+
|
|
37
|
+
### `x-invalidate`
|
|
38
|
+
|
|
39
|
+
```yaml
|
|
40
|
+
x-invalidate:
|
|
41
|
+
- GET /v1/projects/{project_id}/tables
|
|
42
|
+
- GET /v1/projects/{project_id}/tables/{table_id}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`string[]` of `"METHOD /path"`. Colon or brace path params; oat normalises to brace. Method is uppercased.
|
|
46
|
+
|
|
47
|
+
This is the entity graph. Inverted, it is each entity's read surface. Highest-value tag.
|
|
48
|
+
|
|
49
|
+
**Fallback:** pair a mutator with sibling collection/item routes on the same path prefix. Misses cross-entity effects.
|
|
50
|
+
|
|
51
|
+
**Unlocks:** `invalidation.declared-route-changes` (when the list names _another_ entity's route).
|
|
52
|
+
|
|
53
|
+
### `x-entity`
|
|
54
|
+
|
|
55
|
+
```yaml
|
|
56
|
+
x-entity:
|
|
57
|
+
name: table
|
|
58
|
+
action: create # create | list | read | update | delete | action
|
|
59
|
+
identity: id
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Overrides path-segment inference. `identity` is required when the item schema has no `id` (or `uuid` / `slug` / `key` / `name`).
|
|
63
|
+
|
|
64
|
+
**Fallback:** deepest plural segment + HTTP verb. See [How the model is derived](model.md#how-the-model-is-derived).
|
|
65
|
+
|
|
66
|
+
### `x-invite`
|
|
67
|
+
|
|
68
|
+
```yaml
|
|
69
|
+
x-invite:
|
|
70
|
+
invite: table.invite
|
|
71
|
+
accept: invite.accept
|
|
72
|
+
revoke: table.revoke
|
|
73
|
+
granteeField: key
|
|
74
|
+
tokenPointer: $.token
|
|
75
|
+
grantPointer: $.grant_id
|
|
76
|
+
tokenFrom: response # or outOfBand
|
|
77
|
+
tokenKind: org-invite # only when tokenFrom is outOfBand; default `${entity}-invite`
|
|
78
|
+
acceptFrom: token # default. `link` = GET the OOB URL instead of POSTing accept JSON
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Put this on the invite operation. Config must give the invitee `inviteAs`. Defaults if omitted: `granteeField: key`, `tokenPointer: $.token`, `grantPointer: $.grant_id`, `tokenFrom: response`, `acceptFrom: token`. All three of `invite` / `accept` / `revoke` (operationIds) are required or the tag is ignored. `oat doctor` / `oat plan` print the accept mode.
|
|
82
|
+
|
|
83
|
+
`tokenFrom: response` (default) reads the accept token from the invite HTTP body at `tokenPointer`. Keep this for backends that still put the token in JSON.
|
|
84
|
+
|
|
85
|
+
`tokenFrom: outOfBand` ignores the response token and calls `resolveOutOfBand({ address: inviteAs, kind, scope, headers })` after the invite POST. `kind` is `tokenKind` or `${entity}-invite` (`org` → `org-invite`, `project` → `project-invite`). Use this when the live profile must accept only the mailed token.
|
|
86
|
+
|
|
87
|
+
`acceptFrom: token` (default) stuffs that string into the documented accept JSON / path as today. `acceptFrom: link` requires the string to be an absolute `http(s)` URL: oat GETs it (recorded, cookie-jar follow). The `accept` operationId is unused for that hop; `revoke` still uses the documented revoke operation. 2xx/3xx that leaves the grant readable continues the timeline; 4xx, 404, or an unreadable grant is a finding. A config that still POSTs `{ token }` with `tokenFrom: outOfBand` and no `acceptFrom` does not change meaning.
|
|
88
|
+
|
|
89
|
+
An invite operation is **not** the entity's fixture create, even when it is `POST` on the collection. oat will not seed it with a generated email. The invite check (and only that check) creates the grant, using `inviteAs` as `granteeField`. The check still runs when there is no non-invite create, as long as an item or list route exists.
|
|
90
|
+
|
|
91
|
+
Timeline asserted: cannot read → invite → still cannot → accept → can → revoke → cannot.
|
|
92
|
+
|
|
93
|
+
Accept and revoke send the documented JSON request body when the operation declares one, filled from the invite token / grant id (and other known scope values). A path-only accept (`POST /invites/{token}` with no body) stays path-only — oat does not invent a body.
|
|
94
|
+
|
|
95
|
+
**Fallback:** the check does not run.
|
|
96
|
+
|
|
97
|
+
### `x-query`
|
|
98
|
+
|
|
99
|
+
String arrays still work. Structured rows are optional. Either, not both required.
|
|
100
|
+
|
|
101
|
+
```yaml
|
|
102
|
+
x-query:
|
|
103
|
+
grammar: postgrest # postgrest | colon | equality
|
|
104
|
+
filterable: [id, name, created_at]
|
|
105
|
+
sortable: [name, created_at]
|
|
106
|
+
searchable: [name, slug]
|
|
107
|
+
selectable: [id, name, created_at]
|
|
108
|
+
maxLimit: 100
|
|
109
|
+
defaultOrder: created_at.desc
|
|
110
|
+
stableTiebreak: id
|
|
111
|
+
|
|
112
|
+
# structured (optional) — unlocks per-field ops / types / nulls
|
|
113
|
+
filterable:
|
|
114
|
+
- { field: id, type: string, ops: [eq, neq, in, nin] }
|
|
115
|
+
- { field: name, type: string, ops: [eq, neq, like, ilike, in] }
|
|
116
|
+
- { field: created_at, type: date, ops: [eq, gt, gte, lt, lte, is] }
|
|
117
|
+
- { field: tags, type: array, ops: [contains, eq, is] }
|
|
118
|
+
sortable:
|
|
119
|
+
- { field: name, type: string }
|
|
120
|
+
- { field: created_at, type: date, nulls: [first, last] }
|
|
121
|
+
|
|
122
|
+
operators: [eq, ne, neq, gt, gte, lt, lte, in, nin, like, ilike, is, contains]
|
|
123
|
+
operatorsByType:
|
|
124
|
+
string: [eq, ne, neq, like, ilike, in, nin, is]
|
|
125
|
+
number: [eq, ne, neq, gt, gte, lt, lte, in, nin, is]
|
|
126
|
+
date: [eq, ne, neq, gt, gte, lt, lte, is]
|
|
127
|
+
enum: [eq, ne, neq, in, nin, is]
|
|
128
|
+
boolean: [eq, ne, neq, is]
|
|
129
|
+
array: [contains, eq, is]
|
|
130
|
+
aliases: { ne: neq }
|
|
131
|
+
identityFilter: _id # filter field when it differs from the JSON identity
|
|
132
|
+
emptyIn: match-none # reject | match-none; undeclared → empty-in check skips
|
|
133
|
+
maxInValues: 100
|
|
134
|
+
maxFilterConditions: 20
|
|
135
|
+
searchModes: [keyword] # free-form; undeclared → mode checks skip
|
|
136
|
+
searchEmpty: match-all # ignore | match-all | reject
|
|
137
|
+
sortNulls: [first, last]
|
|
138
|
+
maxSortKeys: 3
|
|
139
|
+
sortCollation: case-insensitive # binary | case-insensitive | locale; undeclared → any, consistently
|
|
140
|
+
sortDefaultNulls: last # first | last, with no modifier; undeclared → either end, consistently
|
|
141
|
+
selectNested: true # `rel(col)` grammar; undeclared → nested check skips
|
|
142
|
+
selectUnknown: reject # reject | ignore; undeclared → unknown-select check skips
|
|
143
|
+
|
|
144
|
+
# harvest any axis from another GET — no baked path
|
|
145
|
+
filterableFrom: { operationId: table.get, path: $.columns[*].name, typePath: $.columns[*].type, typeMap: { text: string, int: number } }
|
|
146
|
+
sortableFrom: { operationId: table.get, path: $.columns[*].name }
|
|
147
|
+
searchableFrom: { operationId: table.get, path: $.columns[?(@.searchable==true)].name }
|
|
148
|
+
selectableFrom: { operationId: table.get, path: $.columns[*].name }
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
`defaultOrder` / `stableTiebreak` / `maxLimit` stay. Harvest keys are generic JSON paths (`$`, dots, `[n]`, `[*]`, `[?(@.key==value)]`). Absent → no harvest.
|
|
152
|
+
|
|
153
|
+
String arrays + `operatorsByType` is enough for most APIs. Per-field `ops` / `nulls` close the list that field actually accepts.
|
|
154
|
+
|
|
155
|
+
**Fallback:** if those roles resolve and the axis was not tagged and not set in config, treat every scalar as capable and warn. Empty tagged `filterable: []` / `searchable: null` / `selectable: []` is an explicit claim of none.
|
|
156
|
+
|
|
157
|
+
**Unlocks:** `spec.declared-filterable-is-filterable`, `spec.declared-sortable-is-sortable`, `spec.declared-selectable-is-selectable`. Structured extras unlock the matching catalog checks (`in` = union of `eq`, `ilike` vs `like`, `is.null`, illegal op, empty `in`, caps, nulls, search empty/modes, unknown select, nested `rel(col)`). Sharpens every other query check (without the tag they probe columns you may not have indexed).
|
|
158
|
+
|
|
159
|
+
`oat doctor` prints, per entity, the effective map (all four axes, ops, modes, caps, harvest/hook) and which new checks will apply. Unknown `config.entities` names are ignored and warned.
|
|
160
|
+
|
|
161
|
+
### `x-async`
|
|
162
|
+
|
|
163
|
+
```yaml
|
|
164
|
+
x-async:
|
|
165
|
+
poll: "GET /v1/projects/{project_id}/batches/{batch_id}"
|
|
166
|
+
idFrom: batch_id # or $.id
|
|
167
|
+
until: "status.in.complete,partial,failed"
|
|
168
|
+
successWhen: "status.eq.complete"
|
|
169
|
+
timeoutMs: 120000
|
|
170
|
+
pollIntervalMs: 2000
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
`x-async` means **POST is a receipt, then poll**. It is not how you mark a stream. A stream is a success response that lists `text/event-stream`.
|
|
174
|
+
|
|
175
|
+
When the start response is JSON, oat polls `poll` until `until` matches, then treats that payload as the result. `poll` may be an operationId or `"GET /path/{id}"`. Defaults: `timeoutMs: 120000`, `pollIntervalMs: 2000`.
|
|
176
|
+
|
|
177
|
+
When the start response is `text/event-stream`, oat parses `event:` / `data:` frames. `data` is JSON when it starts with `{` / `[`. `idFrom` is resolved against **each event's JSON `data`**, first hit wins — `$.batch_id` on `event: batch` data `{ "batch_id": "…" }` works. A frame named `complete` / `error`, or one whose `data` matches `until`, is the terminal record; `successWhen` is applied to it and oat does **not** GET the poll route. Poll only if the stream ended without a terminal frame **and** `idFrom` resolved.
|
|
178
|
+
|
|
179
|
+
A spec may leave both `x-async` and `text/event-stream` on the same operation. The stream is still the result; `x-async` only supplies `idFrom` / `until` / the poll fallback.
|
|
180
|
+
|
|
181
|
+
`until` / `successWhen` use the same `field.op.value` predicates as filters (`eq`, `in`, …).
|
|
182
|
+
|
|
183
|
+
**Fallback:** treated as synchronous; async checks are `COVERAGE_GAP`. A 2xx stream without `x-async` is consumed and recorded; async checks do not run.
|
|
184
|
+
|
|
185
|
+
### `x-effects`
|
|
186
|
+
|
|
187
|
+
```yaml
|
|
188
|
+
x-effects:
|
|
189
|
+
- { entity: table, op: create }
|
|
190
|
+
- { entity: row, op: append, min: 1 }
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
`op`: `create` | `append` | `update` | `delete` | `replace`. Each item is `{ entity, op, count?, min? }`.
|
|
194
|
+
|
|
195
|
+
- `count` is an **exact** cardinality delta on that entity's list.
|
|
196
|
+
- `min` is **at-least** (`delta >= min` and `added.length >= min`). Use this when the child count is data-dependent (an extract that appends 1 / 2 / 5 rows).
|
|
197
|
+
- Omit both → `count: 1` (invite / create).
|
|
198
|
+
- Set both on one item → rejected at load / `oat doctor` (`x-effects` gap). The item is not checked.
|
|
199
|
+
|
|
200
|
+
After a write that declares `create` on A, oat binds the new A id from the write response (`table_id`, or the entity identity) or from A's list delta — the same adopt idea as a 402 plan-limit reuse. Later items in **that same** `x-effects` array whose child list is under A (`GET .../tables/{table_id}/rows`) fill the path with that id. `x-wait` after the same write uses the same bound id. No tester hook.
|
|
201
|
+
|
|
202
|
+
An extract-shaped write (create table + append ≥1 row) fails when the new table's row list is empty, and passes when it has 5.
|
|
203
|
+
|
|
204
|
+
**Fallback:** derived from `x-entity.action` for this entity only.
|
|
205
|
+
|
|
206
|
+
### `x-wait`
|
|
207
|
+
|
|
208
|
+
```yaml
|
|
209
|
+
x-wait:
|
|
210
|
+
operationId: inbox.list
|
|
211
|
+
until: $.items.0
|
|
212
|
+
timeoutMs: 30000
|
|
213
|
+
pollIntervalMs: 1000
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Put this on the **write**. After that write succeeds, oat polls `operationId` until `until` (JSON path `$.items.0` or JSON pointer `/items/0`) is non-empty, or `hooks.awaitSideEffect` returns `true`. Default `timeoutMs` is **30s**. Timeout is a **finding** (`effects.side-effect-arrives`), not a coverage gap.
|
|
217
|
+
|
|
218
|
+
Use this for queue consumers and webhook inboxes (1–30 s), not for the same request. `x-effects` still asserts cardinality; `x-wait` asserts “this other GET eventually has a body”. If the poll path needs a parent id the write created, oat binds it the same way `x-effects` does.
|
|
219
|
+
|
|
220
|
+
**Fallback:** the check does not run.
|
|
221
|
+
|
|
222
|
+
### `x-soft-delete`
|
|
223
|
+
|
|
224
|
+
```yaml
|
|
225
|
+
x-soft-delete: deleted_at
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
On any operation of the entity (commonly DELETE). Tombstone, not remove. Without it, a correct soft-delete looks like a bug (the row is still GET-able).
|
|
229
|
+
|
|
230
|
+
### `x-immutable` / `x-generated`
|
|
231
|
+
|
|
232
|
+
```yaml
|
|
233
|
+
x-immutable: [id, project_id, created_at]
|
|
234
|
+
x-generated: [id, created_at, updated_at]
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Generated fields are omitted from create bodies and expected in responses. Immutable fields must reject or ignore PATCH.
|
|
238
|
+
|
|
239
|
+
**Fallback:** `readOnly: true` counts as generated. No immutability testing without the tag.
|
|
240
|
+
|
|
241
|
+
### `x-tenant`
|
|
242
|
+
|
|
243
|
+
```yaml
|
|
244
|
+
x-tenant: project_id
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Path parameter that scopes the operation.
|
|
248
|
+
|
|
249
|
+
**Fallback:** regex over `{organization_id}`, `{project_id}`, `{tenant_id}`, `{workspace_id}`, `{app_slug}`, `{org_id}`, `{account_id}`, … (`org|organization|tenant|workspace|account|project|app` + optional `_id`/`_slug`). Without the tag, a matching path parameter still infers a tenant and a cross-tenant read is `AMBIGUITY`, not `SECURITY`. Omitting `x-tenant` and not naming a tenant path parameter means the check does not apply.
|
|
250
|
+
|
|
251
|
+
### `x-root`
|
|
252
|
+
|
|
253
|
+
```yaml
|
|
254
|
+
# on a path parameter, not an operation
|
|
255
|
+
x-root: true
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
This resource has no create endpoint; supply it in config `roots` / principal `roots`.
|
|
259
|
+
|
|
260
|
+
**Fallback:** inferred when a path param has no create op; everything beneath is `UNSEEDABLE`.
|
|
261
|
+
|
|
262
|
+
### `x-cleanup`
|
|
263
|
+
|
|
264
|
+
```yaml
|
|
265
|
+
x-cleanup: "DELETE /v1/projects/{project_id}/tables/{table_id}"
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Teardown route when the entity has no discoverable delete. Without it, leftover records are reported at end of run.
|
|
269
|
+
|
|
270
|
+
### `x-cost` / `x-destructive` / `x-idempotent` / `x-fresh-principal`
|
|
271
|
+
|
|
272
|
+
```yaml
|
|
273
|
+
x-cost: high # low | medium | high
|
|
274
|
+
x-destructive: true
|
|
275
|
+
x-idempotent: true
|
|
276
|
+
x-fresh-principal: true
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
`x-cost` and `x-destructive` are consulted by `--profile` (below). `x-idempotent` and `x-fresh-principal` are parsed onto the operation model for the `plan`/`doctor` output but not yet consulted anywhere else. Replay safety is tested from a documented `Idempotency-Key` header (`idempotency.replay-does-not-duplicate`), not from `x-idempotent`.
|
|
280
|
+
|
|
281
|
+
### `x-feature-gate`
|
|
282
|
+
|
|
283
|
+
```yaml
|
|
284
|
+
x-feature-gate: webhooks # or custom_domain, audit_log, …
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Plan key this operation is sold behind. Honey/apps emit it as route meta → OpenAPI. `doctor` / `plan` show it. A non-string or missing tag is ignored (`null`).
|
|
288
|
+
|
|
289
|
+
This is not cost. `--profile cheap` skips expensive operations; a free principal hitting `webhook.create` is the real product. Skipping the op in a profile would hide the gate.
|
|
290
|
+
|
|
291
|
+
A 403 is a **documented feature-gate denial** only when all of:
|
|
292
|
+
|
|
293
|
+
1. The operation has `x-feature-gate: <string>`.
|
|
294
|
+
2. HTTP status is 403.
|
|
295
|
+
3. JSON body `vars.type === "feature_gate"`.
|
|
296
|
+
4. `vars.feature` equals the tag **when `vars.feature` is a string**. Absent `vars.feature` is enough together with (3). A string that disagrees with the tag is backend/tag drift — still a seed failure.
|
|
297
|
+
|
|
298
|
+
`error_key` / `feature_name` / `required_plan` are i18n/product and are not required. A typical body:
|
|
299
|
+
|
|
300
|
+
```json
|
|
301
|
+
{
|
|
302
|
+
"success": false,
|
|
303
|
+
"status": 403,
|
|
304
|
+
"status_key": "forbidden",
|
|
305
|
+
"error_key": "forbidden",
|
|
306
|
+
"vars": {
|
|
307
|
+
"type": "feature_gate",
|
|
308
|
+
"feature": "webhooks",
|
|
309
|
+
"feature_name": "Webhooks",
|
|
310
|
+
"current_plan": "free",
|
|
311
|
+
"required_plan": "pro"
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
A gated create is a **coverage gap, not a fail**. oat degrades the entity the same way `--profile` excluding `create` does: `COVERAGE_GAP` on `world.seed` naming `x-feature-gate: <key>` (and the plan vars if present); remaining checks that needed a seeded row `did not apply` / `BLOCKED` because of that gap, not a page of "returned 403" copies. If the list route already has rows, read-only checks still run.
|
|
317
|
+
|
|
318
|
+
A later write (`create` after seed, `update`, action) that returns a documented gate 403 is the same: the check that needed a 2xx is `COVERAGE_GAP` / did not apply, citing the tag — not `SECURITY`, not a schema defect. `validation.*` and `schema.error-response-matches-document` still apply: the 403 body must match the documented 403 schema. A gate 403 with an undeclared shape is still drift.
|
|
319
|
+
|
|
320
|
+
Every 403 is not a feature gate. No tag, or a tag that disagrees with `vars.feature`, stays a `SeedError`.
|
|
321
|
+
|
|
322
|
+
**Fallback:** none. Without the tag, a 403 is a failed create.
|
|
323
|
+
|
|
324
|
+
### `x-unique`
|
|
325
|
+
|
|
326
|
+
```yaml
|
|
327
|
+
x-unique:
|
|
328
|
+
- [email]
|
|
329
|
+
- [workspace_id, slug]
|
|
330
|
+
# or: { columns: [email] }
|
|
331
|
+
# or a single string[] meaning one set: [email]
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
Column sets that must stay unique. Honey/apps emit this as route meta → OpenAPI. `doctor` / `plan` show the effective sets per entity the way they show `x-feature-gate`.
|
|
335
|
+
|
|
336
|
+
Accepted shapes: a list of column sets, `{ columns: [...] }` objects, and a single `string[]` meaning one set. Each set is a non-empty list of column names; empty sets are dropped. Malformed / non-array → treat as absent (`null`), unique checks do not run, `doctor` records an `x-unique` gap. `[]` after filtering → explicit none, checks do not run. No tag → unique checks do not run, `doctor` says so.
|
|
337
|
+
|
|
338
|
+
Do not infer uniqueness from 409s, unique-looking names, or JSON Schema `uniqueItems`. Index names and product `error_key` / `vars.type` are not required; HTTP **409** is the unique-conflict class.
|
|
339
|
+
|
|
340
|
+
`create.unique-conflict-rejected` — after a known row (seeded or adopted), a second create with the same unique-set values is 409, list cardinality does not grow, and a documented 409 body still matches the 409 schema. **2xx is `BACKEND_BUG`** (evidence: both exchanges, unique columns, list before/after when list resolved; tear down a returned id). Probe each probeable set separately (one 2xx fails the check; do not collide every set in one request). Probeable = at least one column on the create JSON or form body; path/tenant/`x-generated` columns may fill set identity from scope, but a set with no body columns is skipped, not failed. Do not reuse `Idempotency-Key` / `Idempotence-Key` / `X-Idempotency-Key` (omit, or send a fresh key if the document requires it). 402 / documented feature-gate 403 on the probe is `COVERAGE_GAP`, not a unique pass. Other 4xx / 5xx is not a unique pass. Verdict is never `SECURITY`. Leftover/extra rows do not skip the check.
|
|
341
|
+
|
|
342
|
+
`update.unique-conflict-rejected` — PATCH a **different** row onto another row's unique-set values is 409 (same scoring). PATCH of a row's own unique values unchanged is not this check. Skip update sets whose columns are all `x-immutable`.
|
|
343
|
+
|
|
344
|
+
**Fallback:** none. Without the tag, unique checks do not run, and a seed 409 is a failed create.
|
|
345
|
+
|
|
346
|
+
### `--profile` — cost gating
|
|
347
|
+
|
|
348
|
+
```bash
|
|
349
|
+
oat run --config oat.config.ts --profile cheap
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
A profile restricts which operations a run is allowed to touch, filtering on `x-cost` and `x-destructive`. Two exist without being declared anywhere: `full` (no gating — the default, today's behaviour if you never mention a profile) and `cheap` (`{ maxCost: "low" }`). Reach for a profile when some operations are expensive to call every run — an extraction endpoint billed per request, a bulk job, anything you don't want fired on every `oat run`.
|
|
353
|
+
|
|
354
|
+
Anything more specific than a cost band is a named entry in config:
|
|
355
|
+
|
|
356
|
+
```ts
|
|
357
|
+
export default defineConfig({
|
|
358
|
+
// ...
|
|
359
|
+
profiles: {
|
|
360
|
+
// skip everything above "low" cost, same as the built-in "cheap"
|
|
361
|
+
cheap: { maxCost: "low" },
|
|
362
|
+
// skip destructive operations and two specific extraction endpoints
|
|
363
|
+
safe: { excludeDestructive: true, exclude: ["report.extract", "report.summarize"] },
|
|
364
|
+
},
|
|
365
|
+
profile: "safe", // --profile on the CLI overrides this
|
|
366
|
+
})
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
An excluded operation never silently narrows what gets reported. Excluding an entity's `create` degrades that entity the same way a real seeding failure does — a `COVERAGE_GAP` naming the reason, then read-only checks run against whatever the list route already returns; `BLOCKED` if nothing exists to fall back on. Excluding `read`/`update`/`delete` individually stands down just the checks that need that one operation.
|
|
370
|
+
|
|
371
|
+
Exclusion applies to every invocation, not only create/read/update/delete. `effects.declared-effect-occurs` and `async.reaches-terminal-state` drop ops the profile forbids and record `profile.skip` — they do not POST `extract.once` under `--profile cheap`. A check whose only targets were excluded did not apply / names the profile; it never looks like the backend returned 500.
|
|
372
|
+
|
|
373
|
+
The run summary states what a profile skipped: `skipped 12 operation(s) under --profile cheap (12 high-cost)` — the same principle as `did not apply` for a coverage gap: a report that only shows what ran invites the reader to assume the rest was verified.
|
|
374
|
+
|
|
375
|
+
`--profile cheap` also defaults the exchange journal **off**. `--save-exchanges` turns it back on; `--quiet` does not change it.
|
|
376
|
+
|
|
377
|
+
### `x-rate-limit` — pacing oat's own traffic
|
|
378
|
+
|
|
379
|
+
```yaml
|
|
380
|
+
x-rate-limit: { category: ai, rps: 3 }
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
Groups operations sharing one throughput budget (`category`) and optionally the rate itself (`rps`). Without this, oat's matrix can hammer a `login` or paid-inference route past its real limit, collect 429s, and report them as backend defects — exactly the false-positive class that erodes trust fastest. With it, requests to that category are paced through a token bucket before they fire.
|
|
384
|
+
|
|
385
|
+
Tags are the **proactive** path. HTTP 429 is the **reactive** path and is honoured even when the operation has no `x-rate-limit` at all (untagged JWT writes, teardown DELETEs). oat reads `Retry-After` (delta-seconds or HTTP-date), otherwise backs off 1s, 2s, 4s… capped at 30s, and retries the same request up to five times. The wait is fed into the matching bucket, or into an implicit `untagged-write` bucket so the next call does not immediately re-trip.
|
|
386
|
+
|
|
387
|
+
A 429 is only ever reported when the request that drew it was demonstrably under the _declared_ rate — oat's own bucket had a free token, so it did not have to wait for one. A 429 that arrived only after oat's bucket made the request wait means oat's own rate model was too generous, which is paced around, never reported. A 429 against a config-supplied or implicit rate is never a finding.
|
|
388
|
+
|
|
389
|
+
`config.rateLimits` is checked first and needs no tag at all — it is what keeps oat usable against a backend that has not adopted `x-rate-limit` yet, or against an environment (staging, usually) whose real limit differs from what the document claims for production. A 429 against a config-supplied rate is never a finding: it is the operator's own belief about the environment, not a claim the API made.
|
|
390
|
+
|
|
391
|
+
```ts
|
|
392
|
+
export default defineConfig({
|
|
393
|
+
// ...
|
|
394
|
+
rateLimits: [
|
|
395
|
+
{ match: "POST /v1/auth/login", rps: 2 },
|
|
396
|
+
{ match: "auth.login", rps: 2 }, // operationId works too
|
|
397
|
+
{ match: "category:ai", rps: 1 }, // overrides every x-rate-limit-tagged "ai" operation at once
|
|
398
|
+
],
|
|
399
|
+
})
|
|
400
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lovrozagar/oat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "OpenAPI Tester — matrix-test a live API against its OpenAPI spec",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"api",
|
|
@@ -28,11 +28,13 @@
|
|
|
28
28
|
"dist/index.js",
|
|
29
29
|
"dist/index.d.ts",
|
|
30
30
|
"dist/config",
|
|
31
|
+
"dist/conformance",
|
|
31
32
|
"dist/reference",
|
|
32
33
|
"dist/report",
|
|
33
34
|
"dist/runtime",
|
|
34
35
|
"dist/spec",
|
|
35
36
|
"README.md",
|
|
37
|
+
"docs",
|
|
36
38
|
"LICENSE",
|
|
37
39
|
"labs/annotated-openapi.yaml",
|
|
38
40
|
"labs/local.config.ts",
|