@lovrozagar/oat 0.10.0 → 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 +25 -2006
- package/dist/cli.js +283 -59
- package/dist/config/define-config.d.ts +8 -0
- 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 +8 -3
- package/dist/index.js +5 -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.map +1 -1
- package/dist/report/console.js +23 -9
- package/dist/report/console.js.map +1 -1
- package/dist/report/matrix.d.ts +2 -0
- package/dist/report/matrix.d.ts.map +1 -1
- package/dist/report/matrix.js +63 -10
- package/dist/report/matrix.js.map +1 -1
- package/dist/report/render.d.ts +10 -1
- package/dist/report/render.d.ts.map +1 -1
- package/dist/report/render.js +75 -13
- 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 +83 -20
- package/dist/runtime/checks.d.ts.map +1 -1
- package/dist/runtime/checks.js +3285 -1947
- 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 +49 -8
- package/dist/runtime/finding.d.ts.map +1 -1
- package/dist/runtime/finding.js +51 -8
- 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 +34 -2
- package/dist/runtime/run.d.ts.map +1 -1
- package/dist/runtime/run.js +346 -138
- 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/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 +24 -1
- package/dist/runtime/world.d.ts.map +1 -1
- package/dist/runtime/world.js +101 -26
- 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/dist/reference/spec.js
CHANGED
|
@@ -5,19 +5,53 @@
|
|
|
5
5
|
* heuristic fallbacks. The conformance suite also serves stripped variants to test the fallbacks.
|
|
6
6
|
*/
|
|
7
7
|
import { POSTGREST } from "./dialect.js";
|
|
8
|
-
import { ENTITIES,
|
|
8
|
+
import { ENTITIES, fieldsWhere, writableFields } from "./model.js";
|
|
9
|
+
const DEFAULT_CONTEXT = { entities: ENTITIES, shape: {} };
|
|
10
|
+
function entityNamed(ctx, name) {
|
|
11
|
+
return ctx.entities.find((entity) => entity.name === name);
|
|
12
|
+
}
|
|
9
13
|
function fieldSchema(field) {
|
|
10
|
-
const base = { type: field.type };
|
|
14
|
+
const base = field.ref === undefined ? { type: field.type } : { $ref: `#/components/schemas/${field.ref}` };
|
|
15
|
+
if (field.type === "array") {
|
|
16
|
+
base.items = { enum: [...(field.items ?? [])], type: "string" };
|
|
17
|
+
base.maxItems = 5;
|
|
18
|
+
}
|
|
19
|
+
if (field.format !== undefined)
|
|
20
|
+
base.format = field.format;
|
|
21
|
+
if (field.minimum !== undefined)
|
|
22
|
+
base.minimum = field.minimum;
|
|
23
|
+
if (field.maximum !== undefined)
|
|
24
|
+
base.maximum = field.maximum;
|
|
25
|
+
if (field.multipleOf !== undefined)
|
|
26
|
+
base.multipleOf = field.multipleOf;
|
|
27
|
+
if (field.const !== undefined)
|
|
28
|
+
base.const = field.const;
|
|
11
29
|
if (field.enum !== undefined)
|
|
12
30
|
base.enum = [...field.enum];
|
|
13
31
|
if (field.maxLength !== undefined)
|
|
14
32
|
base.maxLength = field.maxLength;
|
|
15
33
|
if (field.generated === true)
|
|
16
34
|
base.readOnly = true;
|
|
35
|
+
if (field.writeOnly === true)
|
|
36
|
+
base.writeOnly = true;
|
|
17
37
|
if (field.nullable === true)
|
|
18
38
|
return { oneOf: [base, { type: "null" }] };
|
|
19
39
|
return base;
|
|
20
40
|
}
|
|
41
|
+
/** The schema of an identifier in a path, matching the identity it names. */
|
|
42
|
+
function idSchema(ctx, param) {
|
|
43
|
+
const owner = ctx.entities.find((entity) => entity.itemParam === param);
|
|
44
|
+
const identity = owner?.fields.find((field) => field.name === owner.identity);
|
|
45
|
+
if (identity?.type === "integer")
|
|
46
|
+
return { minimum: 1, type: "integer" };
|
|
47
|
+
if (identity?.format === "uuid")
|
|
48
|
+
return { format: "uuid", type: "string" };
|
|
49
|
+
return { type: "string" };
|
|
50
|
+
}
|
|
51
|
+
/** A success status as the document spells it — exactly, or as the `2XX` range. */
|
|
52
|
+
function ok(ctx, status) {
|
|
53
|
+
return ctx.shape.rangeStatuses === true ? "2XX" : String(status);
|
|
54
|
+
}
|
|
21
55
|
function itemSchema(entity) {
|
|
22
56
|
const properties = {};
|
|
23
57
|
for (const field of entity.fields)
|
|
@@ -29,15 +63,23 @@ function itemSchema(entity) {
|
|
|
29
63
|
type: "object",
|
|
30
64
|
};
|
|
31
65
|
}
|
|
32
|
-
|
|
66
|
+
/** An entity's record schema — inline, or a reference into `components` under the layout shape. */
|
|
67
|
+
function itemSchemaRef(ctx, entity) {
|
|
68
|
+
return ctx.shape.specLayout === true ? { $ref: `#/components/schemas/${schemaName(entity)}` } : itemSchema(entity);
|
|
69
|
+
}
|
|
70
|
+
function schemaName(entity) {
|
|
71
|
+
return `${entity.name[0]?.toUpperCase() ?? ""}${entity.name.slice(1)}`;
|
|
72
|
+
}
|
|
73
|
+
function bodySchema(entity, phase, replace = false) {
|
|
33
74
|
const properties = {};
|
|
34
75
|
for (const field of writableFields(entity, phase))
|
|
35
76
|
properties[field.name] = fieldSchema(field);
|
|
36
77
|
return {
|
|
37
78
|
additionalProperties: false,
|
|
38
79
|
properties,
|
|
39
|
-
required
|
|
40
|
-
|
|
80
|
+
/* A create, and a replacing update, must carry every required field; a PATCH need not. */
|
|
81
|
+
required: phase === "create" || replace
|
|
82
|
+
? writableFields(entity, phase)
|
|
41
83
|
.filter((f) => f.required === true)
|
|
42
84
|
.map((f) => f.name)
|
|
43
85
|
: [],
|
|
@@ -48,25 +90,26 @@ function bodySchema(entity, phase) {
|
|
|
48
90
|
function collectionKey(entity, dialect) {
|
|
49
91
|
return dialect.envelope?.collection ?? entity.plural;
|
|
50
92
|
}
|
|
51
|
-
function listSchema(entity, dialect) {
|
|
93
|
+
function listSchema(ctx, entity, dialect) {
|
|
52
94
|
const env = dialect.envelope;
|
|
53
95
|
/* No envelope: the response *is* the array. A document that says so is the only place oat can
|
|
54
96
|
* learn it, so the schema has to say it rather than describe a wrapper that does not exist. */
|
|
55
97
|
if (env === null)
|
|
56
|
-
return { items:
|
|
98
|
+
return { items: itemSchemaRef(ctx, entity), type: "array" };
|
|
57
99
|
const key = collectionKey(entity, dialect);
|
|
58
|
-
const properties = {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
[
|
|
100
|
+
const properties = { [key]: { items: itemSchemaRef(ctx, entity), type: "array" } };
|
|
101
|
+
const required = [key];
|
|
102
|
+
const declare = (name, schema) => {
|
|
103
|
+
if (name === undefined)
|
|
104
|
+
return;
|
|
105
|
+
properties[name] = schema;
|
|
106
|
+
required.push(name);
|
|
64
107
|
};
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
108
|
+
declare(env.hasMore, { type: "boolean" });
|
|
109
|
+
declare(env.limit, { minimum: 1, type: "integer" });
|
|
110
|
+
declare(env.page, { oneOf: [{ minimum: 1, type: "integer" }, { type: "null" }] });
|
|
111
|
+
declare(env.total, { minimum: 0, type: "integer" });
|
|
112
|
+
declare(env.nextCursor, { oneOf: [{ type: "string" }, { type: "null" }] });
|
|
70
113
|
return { additionalProperties: false, properties, required, type: "object" };
|
|
71
114
|
}
|
|
72
115
|
function errorSchema(status, key) {
|
|
@@ -90,7 +133,16 @@ const ERRORS = [
|
|
|
90
133
|
[409, "conflict"],
|
|
91
134
|
[415, "unsupported_media_type"],
|
|
92
135
|
];
|
|
93
|
-
function errorResponses(codes) {
|
|
136
|
+
function errorResponses(ctx, codes) {
|
|
137
|
+
/* A document may describe every failure at once: one `default` response and one schema. */
|
|
138
|
+
if (ctx.shape.rangeStatuses === true) {
|
|
139
|
+
return {
|
|
140
|
+
default: {
|
|
141
|
+
content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } },
|
|
142
|
+
description: "error",
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
94
146
|
const out = {};
|
|
95
147
|
for (const [status, key] of ERRORS) {
|
|
96
148
|
if (!codes.includes(status))
|
|
@@ -169,6 +221,13 @@ function listQueryParams(dialect, entity) {
|
|
|
169
221
|
{ type: "string" },
|
|
170
222
|
"Comma-separated sparse fieldset; * selects all",
|
|
171
223
|
], [p.search, { maxLength: 200, type: "string" }, "Free-text search across searchable fields"]);
|
|
224
|
+
if (p.searchMode !== undefined) {
|
|
225
|
+
params.push([
|
|
226
|
+
p.searchMode,
|
|
227
|
+
{ default: "contains", enum: ["contains", "prefix"], type: "string" },
|
|
228
|
+
"How the search term matches: anywhere in a field, or at its start",
|
|
229
|
+
]);
|
|
230
|
+
}
|
|
172
231
|
/* Exactly one paging model is published, because that is what a real document does — and a
|
|
173
232
|
* document advertising a parameter the backend ignores is itself a defect oat reports. */
|
|
174
233
|
if (p.page !== undefined) {
|
|
@@ -182,16 +241,30 @@ function listQueryParams(dialect, entity) {
|
|
|
182
241
|
}
|
|
183
242
|
return params;
|
|
184
243
|
}
|
|
185
|
-
|
|
244
|
+
/** A canonical `field.dir` order spelled in the dialect's own sort grammar. */
|
|
245
|
+
function renderOrder(dialect, canonical) {
|
|
246
|
+
const [field = "", direction = "asc"] = canonical.split(".");
|
|
247
|
+
switch (dialect.sortGrammar ?? "dotted") {
|
|
248
|
+
case "prefixed":
|
|
249
|
+
return direction === "desc" ? `-${field}` : field;
|
|
250
|
+
case "colon":
|
|
251
|
+
return `${field}:${direction}`;
|
|
252
|
+
case "spaced":
|
|
253
|
+
return `${field} ${direction}`;
|
|
254
|
+
case "dotted":
|
|
255
|
+
return `${field}.${direction}`;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function pathParams(ctx, entity, includeItem) {
|
|
186
259
|
const params = entity.parents.map((name) => ({
|
|
187
260
|
in: "path",
|
|
188
261
|
name,
|
|
189
262
|
required: true,
|
|
190
|
-
schema:
|
|
263
|
+
schema: idSchema(ctx, name),
|
|
191
264
|
...(name === "project_id" ? { "x-root": true } : {}),
|
|
192
265
|
}));
|
|
193
266
|
if (includeItem) {
|
|
194
|
-
params.push({ in: "path", name: entity.itemParam, required: true, schema:
|
|
267
|
+
params.push({ in: "path", name: entity.itemParam, required: true, schema: idSchema(ctx, entity.itemParam) });
|
|
195
268
|
}
|
|
196
269
|
return params;
|
|
197
270
|
}
|
|
@@ -218,15 +291,15 @@ function queryFieldType(field) {
|
|
|
218
291
|
* row write genuinely changes the table's representation. Declaring routes that a write does not
|
|
219
292
|
* actually affect would make the invalidation check assert something false.
|
|
220
293
|
*/
|
|
221
|
-
function parentReadRoutes(entity) {
|
|
294
|
+
function parentReadRoutes(ctx, entity) {
|
|
222
295
|
if (entity.name !== "row")
|
|
223
296
|
return [];
|
|
224
|
-
const table =
|
|
297
|
+
const table = ctx.entities.find((candidate) => candidate.name === "table");
|
|
225
298
|
if (table === undefined)
|
|
226
299
|
return [];
|
|
227
300
|
return [`GET ${table.collectionPath}`, `GET ${table.itemPath}`];
|
|
228
301
|
}
|
|
229
|
-
function buildEntityPaths(entity, dialect) {
|
|
302
|
+
function buildEntityPaths(ctx, entity, dialect) {
|
|
230
303
|
const listRoute = `GET ${entity.collectionPath}`;
|
|
231
304
|
const itemRoute = `GET ${entity.itemPath}`;
|
|
232
305
|
const surface = [listRoute, itemRoute];
|
|
@@ -260,6 +333,23 @@ function buildEntityPaths(entity, dialect) {
|
|
|
260
333
|
return row;
|
|
261
334
|
}),
|
|
262
335
|
stableTiebreak: entity.identity,
|
|
336
|
+
/* Search semantics stated rather than left to inference: case-insensitive, and a blank
|
|
337
|
+
* term matches everything. */
|
|
338
|
+
searchCase: "insensitive",
|
|
339
|
+
searchEmpty: "match-all",
|
|
340
|
+
sort: { defaultOrder: renderOrder(dialect, entity.defaultOrder) },
|
|
341
|
+
...(dialect.params.searchMode === undefined ? {} : { searchModes: ["contains", "prefix"] }),
|
|
342
|
+
...(entity.relations === undefined
|
|
343
|
+
? {}
|
|
344
|
+
: {
|
|
345
|
+
select: {
|
|
346
|
+
nested: true,
|
|
347
|
+
relations: Object.entries(entity.relations).map(([name, relation]) => ({
|
|
348
|
+
fields: [...relation.fields],
|
|
349
|
+
name,
|
|
350
|
+
})),
|
|
351
|
+
},
|
|
352
|
+
}),
|
|
263
353
|
...(catalog === undefined
|
|
264
354
|
? {}
|
|
265
355
|
: {
|
|
@@ -267,7 +357,6 @@ function buildEntityPaths(entity, dialect) {
|
|
|
267
357
|
emptyIn: catalog.emptyIn,
|
|
268
358
|
maxFilterConditions: catalog.maxFilterConditions,
|
|
269
359
|
maxInValues: catalog.maxInValues,
|
|
270
|
-
searchEmpty: "match-all",
|
|
271
360
|
selectUnknown: catalog.selectUnknown,
|
|
272
361
|
sortNulls: ["first", "last"],
|
|
273
362
|
maxSortKeys: 3,
|
|
@@ -277,7 +366,7 @@ function buildEntityPaths(entity, dialect) {
|
|
|
277
366
|
get: {
|
|
278
367
|
operationId: `${entity.name}.list`,
|
|
279
368
|
parameters: [
|
|
280
|
-
...pathParams(entity, false),
|
|
369
|
+
...pathParams(ctx, entity, false),
|
|
281
370
|
...listQueryParams(dialect, entity).map(([name, schema, description]) => ({
|
|
282
371
|
description,
|
|
283
372
|
in: "query",
|
|
@@ -293,20 +382,22 @@ function buildEntityPaths(entity, dialect) {
|
|
|
293
382
|
},
|
|
294
383
|
],
|
|
295
384
|
responses: {
|
|
296
|
-
|
|
297
|
-
...errorResponses([400, 401, 403, 404]),
|
|
385
|
+
[ok(ctx, 200)]: jsonResponse(`List ${entity.plural}`, listSchema(ctx, entity, dialect), listResponseHeaders(dialect)),
|
|
386
|
+
...errorResponses(ctx, [400, 401, 403, 404]),
|
|
298
387
|
},
|
|
299
388
|
summary: `List ${entity.plural}`,
|
|
300
389
|
tags: [title],
|
|
301
390
|
"x-entity": { action: "list", identity: entity.identity, name: entity.name },
|
|
302
391
|
"x-query": query,
|
|
392
|
+
/* Generous enough never to slow a run; declared so a stricter real limit is testable. */
|
|
393
|
+
"x-rate-limit": { category: `${entity.name}.list`, rps: 500 },
|
|
303
394
|
"x-tenant": tenantParam(entity),
|
|
304
395
|
...(entity.softDeleteField === undefined ? {} : { "x-soft-delete": entity.softDeleteField }),
|
|
305
396
|
},
|
|
306
397
|
post: {
|
|
307
398
|
operationId: `${entity.name}.create`,
|
|
308
399
|
parameters: [
|
|
309
|
-
...pathParams(entity, false),
|
|
400
|
+
...pathParams(ctx, entity, false),
|
|
310
401
|
/* Declared as an ordinary header parameter, because that is how real APIs publish
|
|
311
402
|
* it. oat needs no new meta tag to find this: a create operation naming a header
|
|
312
403
|
* whose name reads as an idempotency key is enough to know replay is promised. */
|
|
@@ -320,12 +411,16 @@ function buildEntityPaths(entity, dialect) {
|
|
|
320
411
|
},
|
|
321
412
|
],
|
|
322
413
|
requestBody: {
|
|
323
|
-
content: {
|
|
414
|
+
content: {
|
|
415
|
+
[ctx.shape.formCreate === true ? "application/x-www-form-urlencoded" : "application/json"]: {
|
|
416
|
+
schema: bodySchema(entity, "create"),
|
|
417
|
+
},
|
|
418
|
+
},
|
|
324
419
|
required: true,
|
|
325
420
|
},
|
|
326
421
|
responses: {
|
|
327
|
-
|
|
328
|
-
...errorResponses([400, 401, 403, 404, 409, 415]),
|
|
422
|
+
[ok(ctx, 201)]: jsonResponse(`Created ${entity.name}`, itemSchemaRef(ctx, entity)),
|
|
423
|
+
...errorResponses(ctx, [400, 401, 403, 404, 409, 415]),
|
|
329
424
|
},
|
|
330
425
|
summary: `Create ${entity.name}`,
|
|
331
426
|
tags: [title],
|
|
@@ -337,47 +432,50 @@ function buildEntityPaths(entity, dialect) {
|
|
|
337
432
|
* carries a derived value — the parent's routes as well. Declaring it is what makes
|
|
338
433
|
* the cross-entity consistency testable rather than assumed.
|
|
339
434
|
*/
|
|
340
|
-
"x-invalidate": [listRoute, ...parentReadRoutes(entity)],
|
|
435
|
+
"x-invalidate": [listRoute, ...parentReadRoutes(ctx, entity)],
|
|
341
436
|
"x-tenant": tenantParam(entity),
|
|
342
437
|
},
|
|
343
438
|
};
|
|
344
439
|
const item = {
|
|
345
440
|
delete: {
|
|
346
441
|
operationId: `${entity.name}.delete`,
|
|
347
|
-
parameters: pathParams(entity, true),
|
|
442
|
+
parameters: pathParams(ctx, entity, true),
|
|
348
443
|
responses: {
|
|
349
|
-
|
|
350
|
-
...errorResponses([400, 401, 403, 404]),
|
|
444
|
+
[ok(ctx, 200)]: jsonResponse(`Deleted ${entity.name}`, itemSchemaRef(ctx, entity)),
|
|
445
|
+
...errorResponses(ctx, [400, 401, 403, 404]),
|
|
351
446
|
},
|
|
352
447
|
summary: `Delete ${entity.name}`,
|
|
353
448
|
tags: [title],
|
|
354
449
|
"x-entity": { action: "delete", identity: entity.identity, name: entity.name },
|
|
355
|
-
|
|
450
|
+
/* A delete changes the parent's derived count just as a create does. */
|
|
451
|
+
"x-invalidate": [...surface, ...parentReadRoutes(ctx, entity)],
|
|
356
452
|
"x-tenant": tenantParam(entity),
|
|
357
453
|
...(entity.softDeleteField === undefined ? {} : { "x-soft-delete": entity.softDeleteField }),
|
|
358
454
|
},
|
|
359
455
|
get: {
|
|
360
456
|
operationId: `${entity.name}.get`,
|
|
361
|
-
parameters: pathParams(entity, true),
|
|
457
|
+
parameters: pathParams(ctx, entity, true),
|
|
362
458
|
responses: {
|
|
363
|
-
|
|
364
|
-
...errorResponses([400, 401, 403, 404]),
|
|
459
|
+
[ok(ctx, 200)]: jsonResponse(entity.name, itemSchemaRef(ctx, entity)),
|
|
460
|
+
...errorResponses(ctx, [400, 401, 403, 404]),
|
|
365
461
|
},
|
|
366
462
|
summary: `Get ${entity.name}`,
|
|
367
463
|
tags: [title],
|
|
368
464
|
"x-entity": { action: "read", identity: entity.identity, name: entity.name },
|
|
369
465
|
"x-tenant": tenantParam(entity),
|
|
370
466
|
},
|
|
371
|
-
patch: {
|
|
467
|
+
[ctx.shape.updateMethod === "PUT" ? "put" : "patch"]: {
|
|
372
468
|
operationId: `${entity.name}.update`,
|
|
373
|
-
parameters: pathParams(entity, true),
|
|
469
|
+
parameters: pathParams(ctx, entity, true),
|
|
374
470
|
requestBody: {
|
|
375
|
-
content: {
|
|
471
|
+
content: {
|
|
472
|
+
"application/json": { schema: bodySchema(entity, "update", ctx.shape.updateMethod === "PUT") },
|
|
473
|
+
},
|
|
376
474
|
required: true,
|
|
377
475
|
},
|
|
378
476
|
responses: {
|
|
379
|
-
|
|
380
|
-
...errorResponses([400, 401, 403, 404, 409, 415]),
|
|
477
|
+
[ok(ctx, 200)]: jsonResponse(`Updated ${entity.name}`, itemSchemaRef(ctx, entity)),
|
|
478
|
+
...errorResponses(ctx, [400, 401, 403, 404, 409, 415]),
|
|
381
479
|
},
|
|
382
480
|
summary: `Update ${entity.name}`,
|
|
383
481
|
tags: [title],
|
|
@@ -393,14 +491,16 @@ function buildEntityPaths(entity, dialect) {
|
|
|
393
491
|
};
|
|
394
492
|
return { [entity.collectionPath]: collection, [entity.itemPath]: item };
|
|
395
493
|
}
|
|
396
|
-
export function buildSpec(dialect = POSTGREST) {
|
|
494
|
+
export function buildSpec(dialect = POSTGREST, ctx = DEFAULT_CONTEXT) {
|
|
495
|
+
const TABLE = entityNamed(ctx, "table");
|
|
496
|
+
const JOB = entityNamed(ctx, "job");
|
|
397
497
|
const paths = {};
|
|
398
|
-
for (const entity of
|
|
399
|
-
Object.assign(paths, buildEntityPaths(entity, dialect));
|
|
498
|
+
for (const entity of ctx.entities)
|
|
499
|
+
Object.assign(paths, buildEntityPaths(ctx, entity, dialect));
|
|
400
500
|
paths[`${TABLE.itemPath}/invites`] = {
|
|
401
501
|
post: {
|
|
402
502
|
operationId: "table.invite",
|
|
403
|
-
parameters: pathParams(TABLE, true),
|
|
503
|
+
parameters: pathParams(ctx, TABLE, true),
|
|
404
504
|
requestBody: {
|
|
405
505
|
content: {
|
|
406
506
|
"application/json": {
|
|
@@ -415,13 +515,13 @@ export function buildSpec(dialect = POSTGREST) {
|
|
|
415
515
|
required: true,
|
|
416
516
|
},
|
|
417
517
|
responses: {
|
|
418
|
-
|
|
518
|
+
[ok(ctx, 201)]: jsonResponse("Invite created", {
|
|
419
519
|
additionalProperties: false,
|
|
420
520
|
properties: { grant_id: { type: "string" }, token: { type: "string" } },
|
|
421
521
|
required: ["grant_id", "token"],
|
|
422
522
|
type: "object",
|
|
423
523
|
}),
|
|
424
|
-
...errorResponses([400, 401, 403, 404, 415]),
|
|
524
|
+
...errorResponses(ctx, [400, 401, 403, 404, 415]),
|
|
425
525
|
},
|
|
426
526
|
summary: "Invite another principal to read this table",
|
|
427
527
|
tags: ["Table"],
|
|
@@ -442,13 +542,13 @@ export function buildSpec(dialect = POSTGREST) {
|
|
|
442
542
|
operationId: "invite.accept",
|
|
443
543
|
parameters: [{ in: "path", name: "token", required: true, schema: { type: "string" } }],
|
|
444
544
|
responses: {
|
|
445
|
-
|
|
545
|
+
[ok(ctx, 200)]: jsonResponse("Invite accepted", {
|
|
446
546
|
additionalProperties: false,
|
|
447
547
|
properties: { accepted: { type: "boolean" } },
|
|
448
548
|
required: ["accepted"],
|
|
449
549
|
type: "object",
|
|
450
550
|
}),
|
|
451
|
-
...errorResponses([400, 401, 404]),
|
|
551
|
+
...errorResponses(ctx, [400, 401, 404]),
|
|
452
552
|
},
|
|
453
553
|
summary: "Accept an invite",
|
|
454
554
|
tags: ["Table"],
|
|
@@ -459,17 +559,17 @@ export function buildSpec(dialect = POSTGREST) {
|
|
|
459
559
|
delete: {
|
|
460
560
|
operationId: "table.revoke",
|
|
461
561
|
parameters: [
|
|
462
|
-
...pathParams(TABLE, true),
|
|
562
|
+
...pathParams(ctx, TABLE, true),
|
|
463
563
|
{ in: "path", name: "grant_id", required: true, schema: { type: "string" } },
|
|
464
564
|
],
|
|
465
565
|
responses: {
|
|
466
|
-
|
|
566
|
+
[ok(ctx, 200)]: jsonResponse("Grant revoked", {
|
|
467
567
|
additionalProperties: false,
|
|
468
568
|
properties: { revoked: { type: "boolean" } },
|
|
469
569
|
required: ["revoked"],
|
|
470
570
|
type: "object",
|
|
471
571
|
}),
|
|
472
|
-
...errorResponses([400, 401, 403, 404]),
|
|
572
|
+
...errorResponses(ctx, [400, 401, 403, 404]),
|
|
473
573
|
},
|
|
474
574
|
summary: "Revoke a grant",
|
|
475
575
|
tags: ["Table"],
|
|
@@ -482,7 +582,7 @@ export function buildSpec(dialect = POSTGREST) {
|
|
|
482
582
|
paths[`${JOB.collectionPath}/start`] = {
|
|
483
583
|
post: {
|
|
484
584
|
operationId: "job.start",
|
|
485
|
-
parameters: pathParams(JOB, false),
|
|
585
|
+
parameters: pathParams(ctx, JOB, false),
|
|
486
586
|
requestBody: {
|
|
487
587
|
content: {
|
|
488
588
|
"application/json": {
|
|
@@ -497,13 +597,13 @@ export function buildSpec(dialect = POSTGREST) {
|
|
|
497
597
|
required: true,
|
|
498
598
|
},
|
|
499
599
|
responses: {
|
|
500
|
-
|
|
600
|
+
[ok(ctx, 202)]: jsonResponse("Job accepted", {
|
|
501
601
|
additionalProperties: false,
|
|
502
602
|
properties: { accepted: { type: "boolean" }, job_id: { type: "string" } },
|
|
503
603
|
required: ["job_id", "accepted"],
|
|
504
604
|
type: "object",
|
|
505
605
|
}),
|
|
506
|
-
...errorResponses([400, 401, 403, 404, 415]),
|
|
606
|
+
...errorResponses(ctx, [400, 401, 403, 404, 415]),
|
|
507
607
|
},
|
|
508
608
|
summary: "Start a job",
|
|
509
609
|
tags: ["Job"],
|
|
@@ -520,6 +620,69 @@ export function buildSpec(dialect = POSTGREST) {
|
|
|
520
620
|
"x-tenant": tenantParam(JOB),
|
|
521
621
|
},
|
|
522
622
|
};
|
|
623
|
+
/* A write whose effect arrives elsewhere, later. x-wait tells oat where to look and how long. */
|
|
624
|
+
paths[`${JOB.collectionPath.replace("/jobs", "")}/notifications`] = {
|
|
625
|
+
post: {
|
|
626
|
+
operationId: "job.notify",
|
|
627
|
+
parameters: pathParams(ctx, JOB, false),
|
|
628
|
+
requestBody: {
|
|
629
|
+
content: {
|
|
630
|
+
"application/json": {
|
|
631
|
+
schema: {
|
|
632
|
+
additionalProperties: false,
|
|
633
|
+
properties: { message: { maxLength: 200, minLength: 1, type: "string" } },
|
|
634
|
+
required: ["message"],
|
|
635
|
+
type: "object",
|
|
636
|
+
},
|
|
637
|
+
},
|
|
638
|
+
},
|
|
639
|
+
required: true,
|
|
640
|
+
},
|
|
641
|
+
responses: {
|
|
642
|
+
[ok(ctx, 202)]: jsonResponse("Notification queued", {
|
|
643
|
+
additionalProperties: false,
|
|
644
|
+
properties: { accepted: { type: "boolean" }, notification_id: { type: "string" } },
|
|
645
|
+
required: ["accepted", "notification_id"],
|
|
646
|
+
type: "object",
|
|
647
|
+
}),
|
|
648
|
+
...errorResponses(ctx, [400, 401, 403, 404, 415]),
|
|
649
|
+
},
|
|
650
|
+
summary: "Queue a notification for delivery to the project inbox",
|
|
651
|
+
tags: ["Job"],
|
|
652
|
+
"x-entity": { action: "action", identity: "id", name: "job" },
|
|
653
|
+
"x-tenant": tenantParam(JOB),
|
|
654
|
+
"x-wait": { operationId: "job.inbox", timeoutMs: 2000, until: "$.messages[0]" },
|
|
655
|
+
},
|
|
656
|
+
};
|
|
657
|
+
paths[`${JOB.collectionPath.replace("/jobs", "")}/inbox`] = {
|
|
658
|
+
get: {
|
|
659
|
+
operationId: "job.inbox",
|
|
660
|
+
parameters: pathParams(ctx, JOB, false),
|
|
661
|
+
responses: {
|
|
662
|
+
[ok(ctx, 200)]: jsonResponse("Delivered notifications", {
|
|
663
|
+
additionalProperties: false,
|
|
664
|
+
properties: {
|
|
665
|
+
messages: {
|
|
666
|
+
items: {
|
|
667
|
+
additionalProperties: false,
|
|
668
|
+
properties: { id: { type: "string" }, message: { type: "string" } },
|
|
669
|
+
required: ["id", "message"],
|
|
670
|
+
type: "object",
|
|
671
|
+
},
|
|
672
|
+
type: "array",
|
|
673
|
+
},
|
|
674
|
+
},
|
|
675
|
+
required: ["messages"],
|
|
676
|
+
type: "object",
|
|
677
|
+
}),
|
|
678
|
+
...errorResponses(ctx, [401, 403, 404]),
|
|
679
|
+
},
|
|
680
|
+
summary: "List delivered notifications",
|
|
681
|
+
tags: ["Job"],
|
|
682
|
+
"x-entity": { action: "action", identity: "id", name: "job" },
|
|
683
|
+
"x-tenant": tenantParam(JOB),
|
|
684
|
+
},
|
|
685
|
+
};
|
|
523
686
|
paths["/v1/auth/token"] = {
|
|
524
687
|
post: {
|
|
525
688
|
operationId: "auth.token",
|
|
@@ -537,7 +700,7 @@ export function buildSpec(dialect = POSTGREST) {
|
|
|
537
700
|
required: true,
|
|
538
701
|
},
|
|
539
702
|
responses: {
|
|
540
|
-
|
|
703
|
+
[ok(ctx, 200)]: jsonResponse("Access token", {
|
|
541
704
|
additionalProperties: false,
|
|
542
705
|
properties: {
|
|
543
706
|
access_token: { type: "string" },
|
|
@@ -547,7 +710,7 @@ export function buildSpec(dialect = POSTGREST) {
|
|
|
547
710
|
required: ["access_token", "expires_in", "project_id"],
|
|
548
711
|
type: "object",
|
|
549
712
|
}),
|
|
550
|
-
...errorResponses([400, 401, 415]),
|
|
713
|
+
...errorResponses(ctx, [400, 401, 415]),
|
|
551
714
|
},
|
|
552
715
|
security: [],
|
|
553
716
|
summary: "Exchange an API key for an access token",
|
|
@@ -557,14 +720,42 @@ export function buildSpec(dialect = POSTGREST) {
|
|
|
557
720
|
const schemas = {};
|
|
558
721
|
for (const [status, key] of ERRORS)
|
|
559
722
|
schemas[`Err${status}`] = errorSchema(status, key);
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
723
|
+
schemas.Error = {
|
|
724
|
+
additionalProperties: false,
|
|
725
|
+
properties: {
|
|
726
|
+
error_key: { type: "string" },
|
|
727
|
+
message: { type: "string" },
|
|
728
|
+
status: { type: "integer" },
|
|
729
|
+
success: { const: false },
|
|
730
|
+
},
|
|
731
|
+
required: ["error_key", "message", "status", "success"],
|
|
732
|
+
type: "object",
|
|
733
|
+
};
|
|
734
|
+
schemas.Point = {
|
|
735
|
+
additionalProperties: false,
|
|
736
|
+
properties: { x: { type: "integer" }, y: { type: "integer" } },
|
|
737
|
+
required: ["x", "y"],
|
|
738
|
+
type: "object",
|
|
739
|
+
};
|
|
740
|
+
/* Recursive: a node's children are nodes. Real documents describe trees, comment threads and
|
|
741
|
+
* org charts this way, and nothing about it is unusual — except to a tool that inlines refs. */
|
|
742
|
+
schemas.TreeNode = {
|
|
743
|
+
properties: {
|
|
744
|
+
children: { items: { $ref: "#/components/schemas/TreeNode" }, type: "array" },
|
|
745
|
+
name: { type: "string" },
|
|
564
746
|
},
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
747
|
+
required: ["name"],
|
|
748
|
+
type: "object",
|
|
749
|
+
};
|
|
750
|
+
if (ctx.shape.specLayout === true) {
|
|
751
|
+
for (const entity of ctx.entities)
|
|
752
|
+
schemas[schemaName(entity)] = itemSchema(entity);
|
|
753
|
+
}
|
|
754
|
+
const components = {
|
|
755
|
+
schemas,
|
|
756
|
+
securitySchemes: { bearer: { bearerFormat: "JWT", scheme: "bearer", type: "http" } },
|
|
757
|
+
};
|
|
758
|
+
const rest = {
|
|
568
759
|
security: [{ bearer: [] }],
|
|
569
760
|
"x-auth-flows": {
|
|
570
761
|
default: {
|
|
@@ -574,10 +765,39 @@ export function buildSpec(dialect = POSTGREST) {
|
|
|
574
765
|
},
|
|
575
766
|
},
|
|
576
767
|
};
|
|
768
|
+
const head = { info: { title: "oat reference backend", version: "1.0.0" }, openapi: "3.1.0" };
|
|
769
|
+
/* Key order is the layout: a document is free to define its components before its paths, and
|
|
770
|
+
* to declare path parameters once on the path item rather than on every operation. */
|
|
771
|
+
if (ctx.shape.specLayout === true) {
|
|
772
|
+
return { ...head, components, paths: hoistPathParameters(paths), ...rest };
|
|
773
|
+
}
|
|
774
|
+
return { ...head, paths, components, ...rest };
|
|
775
|
+
}
|
|
776
|
+
/** Moves each path item's `in: path` parameters from its operations onto the path item itself. */
|
|
777
|
+
function hoistPathParameters(paths) {
|
|
778
|
+
const out = {};
|
|
779
|
+
for (const [route, item] of Object.entries(paths)) {
|
|
780
|
+
const operations = Object.entries(item);
|
|
781
|
+
const shared = new Map();
|
|
782
|
+
const hoisted = {};
|
|
783
|
+
for (const [method, operation] of operations) {
|
|
784
|
+
const parameters = (operation.parameters ?? []);
|
|
785
|
+
for (const parameter of parameters) {
|
|
786
|
+
if (parameter.in === "path")
|
|
787
|
+
shared.set(String(parameter.name), parameter);
|
|
788
|
+
}
|
|
789
|
+
hoisted[method] = {
|
|
790
|
+
...operation,
|
|
791
|
+
parameters: parameters.filter((parameter) => parameter.in !== "path"),
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
out[route] = shared.size === 0 ? item : { parameters: [...shared.values()], ...hoisted };
|
|
795
|
+
}
|
|
796
|
+
return out;
|
|
577
797
|
}
|
|
578
798
|
/** Variant with every oat meta tag removed — exercises the heuristic fallbacks. */
|
|
579
|
-
export function buildUntaggedSpec(dialect = POSTGREST) {
|
|
580
|
-
const spec = buildSpec(dialect);
|
|
799
|
+
export function buildUntaggedSpec(dialect = POSTGREST, ctx = DEFAULT_CONTEXT) {
|
|
800
|
+
const spec = buildSpec(dialect, ctx);
|
|
581
801
|
const strip = (node) => {
|
|
582
802
|
if (Array.isArray(node))
|
|
583
803
|
return node.map(strip);
|