@lovrozagar/oat 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +401 -0
- package/bin/oat.js +2 -0
- package/dist/cli.js +386 -0
- package/dist/config/define-config.d.ts +165 -0
- package/dist/config/define-config.d.ts.map +1 -0
- package/dist/config/define-config.js +24 -0
- package/dist/config/define-config.js.map +1 -0
- package/dist/config/load.d.ts +10 -0
- package/dist/config/load.d.ts.map +1 -0
- package/dist/config/load.js +61 -0
- package/dist/config/load.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +9 -0
- package/dist/reference/defects.d.ts +254 -0
- package/dist/reference/defects.d.ts.map +1 -0
- package/dist/reference/defects.js +265 -0
- package/dist/reference/defects.js.map +1 -0
- package/dist/reference/dialect.d.ts +104 -0
- package/dist/reference/dialect.d.ts.map +1 -0
- package/dist/reference/dialect.js +174 -0
- package/dist/reference/dialect.js.map +1 -0
- package/dist/reference/http.d.ts +79 -0
- package/dist/reference/http.d.ts.map +1 -0
- package/dist/reference/http.js +889 -0
- package/dist/reference/http.js.map +1 -0
- package/dist/reference/model.d.ts +56 -0
- package/dist/reference/model.d.ts.map +1 -0
- package/dist/reference/model.js +156 -0
- package/dist/reference/model.js.map +1 -0
- package/dist/reference/query.d.ts +45 -0
- package/dist/reference/query.d.ts.map +1 -0
- package/dist/reference/query.js +353 -0
- package/dist/reference/query.js.map +1 -0
- package/dist/reference/spec.d.ts +13 -0
- package/dist/reference/spec.d.ts.map +1 -0
- package/dist/reference/spec.js +547 -0
- package/dist/reference/spec.js.map +1 -0
- package/dist/reference/store-api.d.ts +59 -0
- package/dist/reference/store-api.d.ts.map +1 -0
- package/dist/reference/store-api.js +76 -0
- package/dist/reference/store-api.js.map +1 -0
- package/dist/reference/stores/memory.d.ts +27 -0
- package/dist/reference/stores/memory.d.ts.map +1 -0
- package/dist/reference/stores/memory.js +97 -0
- package/dist/reference/stores/memory.js.map +1 -0
- package/dist/reference/stores/postgres.d.ts +61 -0
- package/dist/reference/stores/postgres.d.ts.map +1 -0
- package/dist/reference/stores/postgres.js +406 -0
- package/dist/reference/stores/postgres.js.map +1 -0
- package/dist/reference/stores/sqlite-driver.d.ts +54 -0
- package/dist/reference/stores/sqlite-driver.d.ts.map +1 -0
- package/dist/reference/stores/sqlite-driver.js +99 -0
- package/dist/reference/stores/sqlite-driver.js.map +1 -0
- package/dist/reference/stores/sqlite.d.ts +68 -0
- package/dist/reference/stores/sqlite.d.ts.map +1 -0
- package/dist/reference/stores/sqlite.js +464 -0
- package/dist/reference/stores/sqlite.js.map +1 -0
- package/dist/report/console.d.ts +33 -0
- package/dist/report/console.d.ts.map +1 -0
- package/dist/report/console.js +237 -0
- package/dist/report/console.js.map +1 -0
- package/dist/report/matrix.d.ts +131 -0
- package/dist/report/matrix.d.ts.map +1 -0
- package/dist/report/matrix.js +865 -0
- package/dist/report/matrix.js.map +1 -0
- package/dist/report/render.d.ts +66 -0
- package/dist/report/render.d.ts.map +1 -0
- package/dist/report/render.js +423 -0
- package/dist/report/render.js.map +1 -0
- package/dist/runtime/async.d.ts +34 -0
- package/dist/runtime/async.d.ts.map +1 -0
- package/dist/runtime/async.js +130 -0
- package/dist/runtime/async.js.map +1 -0
- package/dist/runtime/auth.d.ts +46 -0
- package/dist/runtime/auth.d.ts.map +1 -0
- package/dist/runtime/auth.js +236 -0
- package/dist/runtime/auth.js.map +1 -0
- package/dist/runtime/checks.d.ts +89 -0
- package/dist/runtime/checks.d.ts.map +1 -0
- package/dist/runtime/checks.js +3400 -0
- package/dist/runtime/checks.js.map +1 -0
- package/dist/runtime/client.d.ts +65 -0
- package/dist/runtime/client.d.ts.map +1 -0
- package/dist/runtime/client.js +122 -0
- package/dist/runtime/client.js.map +1 -0
- package/dist/runtime/finding.d.ts +50 -0
- package/dist/runtime/finding.d.ts.map +1 -0
- package/dist/runtime/finding.js +50 -0
- package/dist/runtime/finding.js.map +1 -0
- package/dist/runtime/fixture.d.ts +20 -0
- package/dist/runtime/fixture.d.ts.map +1 -0
- package/dist/runtime/fixture.js +175 -0
- package/dist/runtime/fixture.js.map +1 -0
- package/dist/runtime/progress.d.ts +41 -0
- package/dist/runtime/progress.d.ts.map +1 -0
- package/dist/runtime/progress.js +186 -0
- package/dist/runtime/progress.js.map +1 -0
- package/dist/runtime/run.d.ts +69 -0
- package/dist/runtime/run.d.ts.map +1 -0
- package/dist/runtime/run.js +575 -0
- package/dist/runtime/run.js.map +1 -0
- package/dist/runtime/teardown.d.ts +38 -0
- package/dist/runtime/teardown.d.ts.map +1 -0
- package/dist/runtime/teardown.js +129 -0
- package/dist/runtime/teardown.js.map +1 -0
- package/dist/runtime/validate.d.ts +31 -0
- package/dist/runtime/validate.d.ts.map +1 -0
- package/dist/runtime/validate.js +111 -0
- package/dist/runtime/validate.js.map +1 -0
- package/dist/runtime/world.d.ts +49 -0
- package/dist/runtime/world.d.ts.map +1 -0
- package/dist/runtime/world.js +119 -0
- package/dist/runtime/world.js.map +1 -0
- package/dist/spec/collection.d.ts +31 -0
- package/dist/spec/collection.d.ts.map +1 -0
- package/dist/spec/collection.js +121 -0
- package/dist/spec/collection.js.map +1 -0
- package/dist/spec/conventions.d.ts +103 -0
- package/dist/spec/conventions.d.ts.map +1 -0
- package/dist/spec/conventions.js +275 -0
- package/dist/spec/conventions.js.map +1 -0
- package/dist/spec/extensions.d.ts +74 -0
- package/dist/spec/extensions.d.ts.map +1 -0
- package/dist/spec/extensions.js +335 -0
- package/dist/spec/extensions.js.map +1 -0
- package/dist/spec/graph.d.ts +89 -0
- package/dist/spec/graph.d.ts.map +1 -0
- package/dist/spec/graph.js +318 -0
- package/dist/spec/graph.js.map +1 -0
- package/dist/spec/load.d.ts +36 -0
- package/dist/spec/load.d.ts.map +1 -0
- package/dist/spec/load.js +166 -0
- package/dist/spec/load.js.map +1 -0
- package/dist/spec/types.d.ts +57 -0
- package/dist/spec/types.d.ts.map +1 -0
- package/dist/spec/types.js +24 -0
- package/dist/spec/types.js.map +1 -0
- package/labs/README.md +130 -0
- package/labs/annotated-openapi.yaml +311 -0
- package/labs/anyrow.config.ts +107 -0
- package/labs/demo.mjs +124 -0
- package/labs/local.config.ts +41 -0
- package/labs/minimal.config.ts +32 -0
- package/package.json +80 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the entity graph — the model every scenario is planned against.
|
|
3
|
+
*
|
|
4
|
+
* The central move: `x-invalidate` is read as a graph edge, not a cache hint. Inverting it
|
|
5
|
+
* gives each entity its *read surface*, the set of projections through which an instance is
|
|
6
|
+
* observable. That set is the criss-cross matrix.
|
|
7
|
+
*/
|
|
8
|
+
import { deriveCollectionShape, deriveIdentity, requestSchema, successSchema } from "./collection.js";
|
|
9
|
+
import { GapCollector, pathParameterNames, readAsync, readCleanup, readCost, readEffects, readEntity, readFlag, readGenerated, readImmutable, readInvalidate, readInvite, readQueryCapability, readRootParams, readSoftDelete, readTenantParam, readTenantSource, singularise, } from "./extensions.js";
|
|
10
|
+
import { deriveQueryConventions } from "./conventions.js";
|
|
11
|
+
import { parseRouteRef } from "./load.js";
|
|
12
|
+
import { MUTATING_METHODS, listEndpoints, } from "./types.js";
|
|
13
|
+
/**
|
|
14
|
+
* Header names the operation's success response declares.
|
|
15
|
+
*
|
|
16
|
+
* Pagination facts do not always live in the body — `Link` is the obvious case — so a header the
|
|
17
|
+
* document publishes is part of the response contract and has to be read as such.
|
|
18
|
+
*/
|
|
19
|
+
function successResponseHeaders(op) {
|
|
20
|
+
const responses = (op.responses ?? {});
|
|
21
|
+
const names = new Set();
|
|
22
|
+
for (const [status, response] of Object.entries(responses)) {
|
|
23
|
+
if (!/^2\d\d$/.test(status))
|
|
24
|
+
continue;
|
|
25
|
+
for (const name of Object.keys(response?.headers ?? {}))
|
|
26
|
+
names.add(name);
|
|
27
|
+
}
|
|
28
|
+
return [...names];
|
|
29
|
+
}
|
|
30
|
+
export function buildModel(doc) {
|
|
31
|
+
const gaps = new GapCollector();
|
|
32
|
+
const endpoints = listEndpoints(doc);
|
|
33
|
+
const operations = endpoints.map((e) => modelOperation(e, doc, gaps));
|
|
34
|
+
demoteNonEntities(operations, gaps);
|
|
35
|
+
const byOperationId = new Map(operations.map((o) => [o.operationId, o]));
|
|
36
|
+
const byRoute = new Map(operations.map((o) => [o.route, o]));
|
|
37
|
+
const entities = buildEntities(operations, byRoute, gaps);
|
|
38
|
+
const roots = collectRoots(operations, entities);
|
|
39
|
+
return {
|
|
40
|
+
byOperationId,
|
|
41
|
+
byRoute,
|
|
42
|
+
entities,
|
|
43
|
+
gaps,
|
|
44
|
+
hasAuthOperations: operations.some((o) => /auth|login|token|session/i.test(o.path)),
|
|
45
|
+
operations,
|
|
46
|
+
rawOperations: new Map(endpoints.map((e) => [e.operationId, e.op])),
|
|
47
|
+
roots,
|
|
48
|
+
securitySchemes: Object.keys(doc.components?.securitySchemes ?? {}),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function modelOperation(endpoint, doc, gaps) {
|
|
52
|
+
const { op, method, path, operationId } = endpoint;
|
|
53
|
+
const responseSchema = successSchema(op);
|
|
54
|
+
const collection = deriveCollectionShape(responseSchema);
|
|
55
|
+
const bodySchema = requestSchema(op);
|
|
56
|
+
const entity = readEntity(endpoint, gaps);
|
|
57
|
+
const pathParams = pathParameterNames(path);
|
|
58
|
+
const lastParam = pathParams.at(-1);
|
|
59
|
+
const itemSchema = collection?.itemSchema ?? responseSchema;
|
|
60
|
+
const identity = entity?.identity ?? deriveIdentity(itemSchema, lastParam === undefined ? undefined : lastParam);
|
|
61
|
+
const security = op.security ?? doc.security ?? [];
|
|
62
|
+
const securitySchemes = [...new Set(security.flatMap((group) => Object.keys(group)))];
|
|
63
|
+
const documentedStatuses = Object.keys(op.responses ?? {})
|
|
64
|
+
.map((s) => Number.parseInt(s, 10))
|
|
65
|
+
.filter((n) => !Number.isNaN(n))
|
|
66
|
+
.sort((a, b) => a - b);
|
|
67
|
+
return {
|
|
68
|
+
action: entity?.action ?? null,
|
|
69
|
+
async: readAsync(op),
|
|
70
|
+
cleanup: readCleanup(op),
|
|
71
|
+
collection,
|
|
72
|
+
cost: readCost(op),
|
|
73
|
+
destructive: readFlag(op, "x-destructive"),
|
|
74
|
+
documentedStatuses,
|
|
75
|
+
effects: readEffects(op),
|
|
76
|
+
entity: entity?.name ?? null,
|
|
77
|
+
entitySource: entity?.source ?? null,
|
|
78
|
+
freshPrincipal: readFlag(op, "x-fresh-principal"),
|
|
79
|
+
generated: readGenerated(op, bodySchema),
|
|
80
|
+
hasRequestBody: bodySchema !== null,
|
|
81
|
+
identity,
|
|
82
|
+
idempotent: readFlag(op, "x-idempotent"),
|
|
83
|
+
immutable: readImmutable(op),
|
|
84
|
+
invalidates: readInvalidate(op),
|
|
85
|
+
isMutation: MUTATING_METHODS.has(method.toUpperCase()),
|
|
86
|
+
method: method.toUpperCase(),
|
|
87
|
+
operationId,
|
|
88
|
+
path,
|
|
89
|
+
pathParams,
|
|
90
|
+
query: collection === null ? null : readQueryCapability(endpoint, collection.itemSchema, gaps),
|
|
91
|
+
conventions: deriveQueryConventions(op.parameters ?? [], collection, ext(op, "x-query"), (message) => gaps.record(operationId, "x-query", message), successResponseHeaders(op)),
|
|
92
|
+
queryParamNames: (op.parameters ?? []).filter((p) => p.in === "query").map((p) => p.name),
|
|
93
|
+
idempotencyHeader: (op.parameters ?? []).find((p) => p.in === "header"
|
|
94
|
+
&& /^(x-)?idempotenc(y|e)([-_]?key)?$/i.test(p.name.replace(/\s/g, "")))?.name ?? null,
|
|
95
|
+
rootParams: readRootParams(op),
|
|
96
|
+
route: `${method.toUpperCase()} ${path}`,
|
|
97
|
+
securitySchemes,
|
|
98
|
+
softDelete: readSoftDelete(op),
|
|
99
|
+
invite: readInvite(op),
|
|
100
|
+
tenantParam: readTenantParam(endpoint, gaps),
|
|
101
|
+
tenantSource: readTenantSource(endpoint),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function ext(op, key) {
|
|
105
|
+
const value = op[key];
|
|
106
|
+
return value === undefined ? undefined : value;
|
|
107
|
+
}
|
|
108
|
+
const PARAM_SEGMENT = /^\{[^}]+\}$/;
|
|
109
|
+
/**
|
|
110
|
+
* The naming heuristic cannot tell a collection from a verb: `POST /tables/{id}/duplicate` and
|
|
111
|
+
* `POST /tables` are the same shape. The document itself settles it — a noun names an entity
|
|
112
|
+
* only if the API exposes an *item route* for it (`.../<noun>/{param}`), or if a GET on the
|
|
113
|
+
* collection returns a collection-shaped body.
|
|
114
|
+
*
|
|
115
|
+
* Everything else is an action on the nearest enclosing entity. Without this pass a spec with
|
|
116
|
+
* 5 entities models as 24, and every spurious one drags a full lifecycle plan behind it.
|
|
117
|
+
*/
|
|
118
|
+
function demoteNonEntities(operations, gaps) {
|
|
119
|
+
const nounsWithItemRoute = new Set();
|
|
120
|
+
for (const op of operations) {
|
|
121
|
+
const segments = op.path.split("/").filter(Boolean);
|
|
122
|
+
for (let i = 1; i < segments.length; i++) {
|
|
123
|
+
const seg = segments[i];
|
|
124
|
+
const prev = segments[i - 1];
|
|
125
|
+
if (seg === undefined || prev === undefined)
|
|
126
|
+
continue;
|
|
127
|
+
if (PARAM_SEGMENT.test(seg) && !PARAM_SEGMENT.test(prev)) {
|
|
128
|
+
nounsWithItemRoute.add(singularise(prev));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
for (const op of operations) {
|
|
133
|
+
if (op.entity === null || op.entitySource === "tag")
|
|
134
|
+
continue;
|
|
135
|
+
if (nounsWithItemRoute.has(op.entity))
|
|
136
|
+
continue;
|
|
137
|
+
const collectionShaped = op.method === "GET" && op.collection !== null;
|
|
138
|
+
if (collectionShaped && !isVerbPhrase(op.path)) {
|
|
139
|
+
/* A collection-shaped GET with no item route is usually a named view of another
|
|
140
|
+
* entity (`/table-templates/mine`). oat cannot know which, so it says so. */
|
|
141
|
+
gaps.record(op.operationId, "x-entity", `returns a collection but exposes no item route, so oat treats "${op.entity}" as its ` +
|
|
142
|
+
"own entity. If it is a filtered view of another entity, name that entity explicitly");
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const parent = enclosingEntity(op.path, nounsWithItemRoute);
|
|
146
|
+
op.entity = parent;
|
|
147
|
+
op.action = parent === null ? null : "action";
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Whether the final segment reads as a verb rather than a noun — `/pet/findByStatus`,
|
|
152
|
+
* `/reports/exportAll`, `/users/searchByEmail`.
|
|
153
|
+
*
|
|
154
|
+
* Such a segment names an operation, not a collection, even though it returns a collection. Left
|
|
155
|
+
* alone it becomes an entity called "findbystatus", which is both untestable and noise in the
|
|
156
|
+
* report.
|
|
157
|
+
*/
|
|
158
|
+
const VERB_PREFIX = /^(find|search|query|list|get|fetch|count|export|import|download|upload|bulk|batch|lookup|resolve|mine|me|all|current|latest|recent|active|archived|default|summary|stats?|statistics|history|preview)([A-Z_-]|$)/;
|
|
159
|
+
function isVerbPhrase(path) {
|
|
160
|
+
const segments = path.split("/").filter(Boolean);
|
|
161
|
+
const last = segments.at(-1);
|
|
162
|
+
if (last === undefined || PARAM_SEGMENT.test(last))
|
|
163
|
+
return false;
|
|
164
|
+
return VERB_PREFIX.test(last);
|
|
165
|
+
}
|
|
166
|
+
/** Nearest ancestor segment in the path that names a real entity. */
|
|
167
|
+
function enclosingEntity(path, entities) {
|
|
168
|
+
const segments = path.split("/").filter(Boolean);
|
|
169
|
+
for (let i = segments.length - 1; i >= 0; i--) {
|
|
170
|
+
const seg = segments[i];
|
|
171
|
+
if (seg === undefined || PARAM_SEGMENT.test(seg))
|
|
172
|
+
continue;
|
|
173
|
+
const name = singularise(seg);
|
|
174
|
+
if (entities.has(name))
|
|
175
|
+
return name;
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
function buildEntities(operations, byRoute, gaps) {
|
|
180
|
+
const entities = new Map();
|
|
181
|
+
const get = (name) => {
|
|
182
|
+
const existing = entities.get(name);
|
|
183
|
+
if (existing !== undefined)
|
|
184
|
+
return existing;
|
|
185
|
+
const created = {
|
|
186
|
+
actions: [],
|
|
187
|
+
declaredSurface: [],
|
|
188
|
+
identity: null,
|
|
189
|
+
invite: null,
|
|
190
|
+
name,
|
|
191
|
+
readSurface: [],
|
|
192
|
+
tenantParams: [],
|
|
193
|
+
trackable: false,
|
|
194
|
+
};
|
|
195
|
+
entities.set(name, created);
|
|
196
|
+
return created;
|
|
197
|
+
};
|
|
198
|
+
for (const op of operations) {
|
|
199
|
+
if (op.entity === null || op.action === null)
|
|
200
|
+
continue;
|
|
201
|
+
const entity = get(op.entity);
|
|
202
|
+
if (entity.identity === null && op.identity !== null)
|
|
203
|
+
entity.identity = op.identity;
|
|
204
|
+
if (op.tenantParam !== null && !entity.tenantParams.includes(op.tenantParam)) {
|
|
205
|
+
entity.tenantParams.push(op.tenantParam);
|
|
206
|
+
}
|
|
207
|
+
if (entity.invite === null && op.invite !== null)
|
|
208
|
+
entity.invite = op.invite;
|
|
209
|
+
switch (op.action) {
|
|
210
|
+
case "create":
|
|
211
|
+
entity.create = op.operationId;
|
|
212
|
+
break;
|
|
213
|
+
case "list":
|
|
214
|
+
entity.list = op.operationId;
|
|
215
|
+
break;
|
|
216
|
+
case "read":
|
|
217
|
+
entity.read = op.operationId;
|
|
218
|
+
break;
|
|
219
|
+
case "update":
|
|
220
|
+
entity.update = op.operationId;
|
|
221
|
+
break;
|
|
222
|
+
case "delete":
|
|
223
|
+
entity.delete = op.operationId;
|
|
224
|
+
break;
|
|
225
|
+
case "action":
|
|
226
|
+
entity.actions.push(op.operationId);
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/* Invert x-invalidate: a mutator's declared read routes belong to its entity's surface. */
|
|
231
|
+
for (const op of operations) {
|
|
232
|
+
if (op.entity === null || op.invalidates.length === 0)
|
|
233
|
+
continue;
|
|
234
|
+
const entity = get(op.entity);
|
|
235
|
+
for (const ref of op.invalidates) {
|
|
236
|
+
const parsed = parseRouteRef(ref);
|
|
237
|
+
if (parsed === null || parsed.method !== "GET")
|
|
238
|
+
continue;
|
|
239
|
+
if (!entity.readSurface.includes(ref)) {
|
|
240
|
+
entity.readSurface.push(ref);
|
|
241
|
+
entity.declaredSurface.push(ref);
|
|
242
|
+
}
|
|
243
|
+
if (!byRoute.has(ref)) {
|
|
244
|
+
gaps.record(op.operationId, "x-invalidate", `names "${ref}", which is not an operation in this document`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/* Sibling fallback for entities whose mutators declared nothing. */
|
|
249
|
+
for (const entity of entities.values()) {
|
|
250
|
+
if (entity.readSurface.length > 0)
|
|
251
|
+
continue;
|
|
252
|
+
for (const opId of [entity.list, entity.read]) {
|
|
253
|
+
if (opId === undefined)
|
|
254
|
+
continue;
|
|
255
|
+
const op = operations.find((o) => o.operationId === opId);
|
|
256
|
+
if (op !== undefined)
|
|
257
|
+
entity.readSurface.push(op.route);
|
|
258
|
+
}
|
|
259
|
+
if (entity.readSurface.length > 0 && (entity.create ?? entity.update ?? entity.delete)) {
|
|
260
|
+
gaps.record(entity.create ?? entity.update ?? entity.delete ?? entity.name, "x-invalidate", `entity "${entity.name}" has mutators but no declared read surface; ` +
|
|
261
|
+
`falling back to sibling routes ${entity.readSurface.join(", ")}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
for (const entity of entities.values()) {
|
|
265
|
+
entity.trackable = entity.identity !== null;
|
|
266
|
+
if (!entity.trackable && (entity.create !== undefined || entity.update !== undefined)) {
|
|
267
|
+
gaps.record(entity.create ?? entity.update ?? entity.name, "x-entity.identity", `entity "${entity.name}" has no discoverable identity property, so instances cannot ` +
|
|
268
|
+
"be tracked across projections; lifecycle and criss-cross coverage disabled");
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return entities;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* A path parameter is a root when nothing in the document can produce it: no entity whose name
|
|
275
|
+
* matches has a create operation. Roots must be supplied by config before a run starts —
|
|
276
|
+
* discovering them late is what turns one missing fixture into a cascade of failures.
|
|
277
|
+
*/
|
|
278
|
+
function collectRoots(operations, entities) {
|
|
279
|
+
const roots = new Set();
|
|
280
|
+
for (const op of operations) {
|
|
281
|
+
for (const param of op.rootParams)
|
|
282
|
+
roots.add(param);
|
|
283
|
+
}
|
|
284
|
+
for (const op of operations) {
|
|
285
|
+
for (const param of op.pathParams) {
|
|
286
|
+
/* Resolve the entity from the segment the parameter qualifies, not from its name:
|
|
287
|
+
* `/table-templates/{template_id}` identifies a `table-template`, and matching on
|
|
288
|
+
* the name alone would invent a phantom `template` root. */
|
|
289
|
+
const implied = owningEntityName(op.path, param) ?? impliedEntityName(param);
|
|
290
|
+
if (implied === null)
|
|
291
|
+
continue;
|
|
292
|
+
const entity = entities.get(implied);
|
|
293
|
+
if (entity === undefined || entity.create === undefined)
|
|
294
|
+
roots.add(param);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return [...roots].sort();
|
|
298
|
+
}
|
|
299
|
+
/** The collection segment immediately preceding `{param}` in the path. */
|
|
300
|
+
export function owningEntityName(path, param) {
|
|
301
|
+
const segments = path.split("/").filter(Boolean);
|
|
302
|
+
const index = segments.indexOf(`{${param}}`);
|
|
303
|
+
if (index <= 0)
|
|
304
|
+
return null;
|
|
305
|
+
const previous = segments[index - 1];
|
|
306
|
+
if (previous === undefined || PARAM_SEGMENT.test(previous))
|
|
307
|
+
return null;
|
|
308
|
+
return singularise(previous);
|
|
309
|
+
}
|
|
310
|
+
/** `{project_id}` → `project`, `{tableId}` → `table`. */
|
|
311
|
+
export function impliedEntityName(param) {
|
|
312
|
+
const stripped = param
|
|
313
|
+
.replace(/[_.]?(id|slug|uuid|key)$/i, "")
|
|
314
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
315
|
+
.toLowerCase();
|
|
316
|
+
return stripped.length === 0 ? null : stripped.replace(/_/g, "-");
|
|
317
|
+
}
|
|
318
|
+
//# sourceMappingURL=graph.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graph.js","sourceRoot":"","sources":["../../src/spec/graph.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { OpenApiDocument } from "./types.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Loads a specification from wherever it lives, in JSON or YAML.
|
|
4
|
+
*
|
|
5
|
+
* Three forms are accepted, resolved in a fixed order so the outcome never depends on a guess
|
|
6
|
+
* about what the string "looks like":
|
|
7
|
+
*
|
|
8
|
+
* 1. an absolute `http(s)://` or `file://` URL — used as given
|
|
9
|
+
* 2. a path that exists on disk — read as a file, relative to the working directory
|
|
10
|
+
* 3. anything else, when a base URL is known — resolved against it, so `/v1/openapi/spec`
|
|
11
|
+
* and `openapi.json` both work next to `baseUrl`
|
|
12
|
+
*
|
|
13
|
+
* When none apply the error names every location that was tried, rather than reporting the last
|
|
14
|
+
* failure as though it were the only attempt.
|
|
15
|
+
*/
|
|
16
|
+
export declare function loadSpec(source: string, baseUrl?: string): Promise<OpenApiDocument>;
|
|
17
|
+
/**
|
|
18
|
+
* Resolves internal `$ref`s in place, sharing object identity for repeated refs so the result
|
|
19
|
+
* stays compact. Cycles are preserved as shared references rather than expanded — AJV handles
|
|
20
|
+
* recursive schemas natively, and expanding them would not terminate.
|
|
21
|
+
*
|
|
22
|
+
* External refs are left untouched and reported by the caller as a coverage gap: resolving them
|
|
23
|
+
* would mean fetching arbitrary URLs, which a test tool should not do implicitly.
|
|
24
|
+
*/
|
|
25
|
+
export declare function dereference(doc: OpenApiDocument): {
|
|
26
|
+
doc: OpenApiDocument;
|
|
27
|
+
externalRefs: string[];
|
|
28
|
+
};
|
|
29
|
+
/** Normalises `/things/:id` to `/things/{id}` so both path syntaxes compare equal. */
|
|
30
|
+
export declare function normalisePath(path: string): string;
|
|
31
|
+
/** `"GET /things/:id"` → `{ method: "GET", path: "/things/{id}" }` */
|
|
32
|
+
export declare function parseRouteRef(ref: string): {
|
|
33
|
+
method: string;
|
|
34
|
+
path: string;
|
|
35
|
+
} | null;
|
|
36
|
+
//# sourceMappingURL=load.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load.d.ts","sourceRoot":"","sources":["../../src/spec/load.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAEjD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CA6BzF;AA8DD;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,eAAe,GAAG;IAClD,GAAG,EAAE,eAAe,CAAA;IACpB,YAAY,EAAE,MAAM,EAAE,CAAA;CACtB,CAkDA;AAMD,sFAAsF;AACtF,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAElD;AAED,sEAAsE;AACtE,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAIlF"}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
/**
|
|
5
|
+
* Loads a specification from wherever it lives, in JSON or YAML.
|
|
6
|
+
*
|
|
7
|
+
* Three forms are accepted, resolved in a fixed order so the outcome never depends on a guess
|
|
8
|
+
* about what the string "looks like":
|
|
9
|
+
*
|
|
10
|
+
* 1. an absolute `http(s)://` or `file://` URL — used as given
|
|
11
|
+
* 2. a path that exists on disk — read as a file, relative to the working directory
|
|
12
|
+
* 3. anything else, when a base URL is known — resolved against it, so `/v1/openapi/spec`
|
|
13
|
+
* and `openapi.json` both work next to `baseUrl`
|
|
14
|
+
*
|
|
15
|
+
* When none apply the error names every location that was tried, rather than reporting the last
|
|
16
|
+
* failure as though it were the only attempt.
|
|
17
|
+
*/
|
|
18
|
+
export async function loadSpec(source, baseUrl) {
|
|
19
|
+
const text = await readSpecSource(source, baseUrl);
|
|
20
|
+
if (text.trim() === "") {
|
|
21
|
+
throw new Error(`oat: ${source} is empty`);
|
|
22
|
+
}
|
|
23
|
+
if (looksLikeJson(text)) {
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(text);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
throw new Error(`oat: ${source} starts as JSON but does not parse: ${error instanceof Error ? error.message : String(error)}. ${diagnoseJson(text)}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const { parse } = await import("yaml");
|
|
33
|
+
return parse(text);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
throw new Error(`oat: could not parse ${source} as JSON or YAML: ${error instanceof Error ? error.message : String(error)}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function looksLikeJson(text) {
|
|
40
|
+
const first = text.trimStart()[0];
|
|
41
|
+
return first === "{" || first === "[";
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Distinguishes a malformed document from a truncated one. A spec cut short by a proxy or a
|
|
45
|
+
* download limit is by far the most common cause, and reporting it as invalid syntax sends
|
|
46
|
+
* people looking for a bug in a file that is actually fine.
|
|
47
|
+
*/
|
|
48
|
+
function diagnoseJson(text) {
|
|
49
|
+
const opens = (text.match(/[[{]/g) ?? []).length;
|
|
50
|
+
const closes = (text.match(/[\]}]/g) ?? []).length;
|
|
51
|
+
if (opens > closes) {
|
|
52
|
+
return (`The document has ${opens - closes} more opening than closing brackets, so it is most ` +
|
|
53
|
+
`likely truncated — it ends after ${text.length} bytes. Check for a download or proxy ` +
|
|
54
|
+
"size limit.");
|
|
55
|
+
}
|
|
56
|
+
return "The document appears complete, so this is a syntax error rather than truncation.";
|
|
57
|
+
}
|
|
58
|
+
async function readSpecSource(source, baseUrl) {
|
|
59
|
+
if (/^https?:\/\//.test(source))
|
|
60
|
+
return fetchText(source);
|
|
61
|
+
if (source.startsWith("file://"))
|
|
62
|
+
return readFile(fileURLToPath(source), "utf8");
|
|
63
|
+
const attempted = [];
|
|
64
|
+
/* A real file wins over a route: a spec checked into the repository is the more specific
|
|
65
|
+
* intent, and silently fetching instead would hide a typo in the path. */
|
|
66
|
+
const asPath = resolve(process.cwd(), source);
|
|
67
|
+
attempted.push(asPath);
|
|
68
|
+
try {
|
|
69
|
+
return await readFile(asPath, "utf8");
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (error.code !== "ENOENT")
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
if (baseUrl !== undefined && baseUrl !== "") {
|
|
76
|
+
const resolved = new URL(source, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`).toString();
|
|
77
|
+
attempted.push(resolved);
|
|
78
|
+
return fetchText(resolved);
|
|
79
|
+
}
|
|
80
|
+
throw new Error(`oat: could not find a specification at "${source}". Tried:\n` +
|
|
81
|
+
attempted.map((a) => ` ${a}`).join("\n") +
|
|
82
|
+
(baseUrl === undefined
|
|
83
|
+
? "\nPass --base-url (or set baseUrl in the config) to resolve it as a route."
|
|
84
|
+
: ""));
|
|
85
|
+
}
|
|
86
|
+
async function fetchText(url) {
|
|
87
|
+
const res = await fetch(url, { headers: { accept: "application/json" } });
|
|
88
|
+
if (!res.ok)
|
|
89
|
+
throw new Error(`oat: fetching ${url} returned ${res.status} ${res.statusText}`);
|
|
90
|
+
return res.text();
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Resolves internal `$ref`s in place, sharing object identity for repeated refs so the result
|
|
94
|
+
* stays compact. Cycles are preserved as shared references rather than expanded — AJV handles
|
|
95
|
+
* recursive schemas natively, and expanding them would not terminate.
|
|
96
|
+
*
|
|
97
|
+
* External refs are left untouched and reported by the caller as a coverage gap: resolving them
|
|
98
|
+
* would mean fetching arbitrary URLs, which a test tool should not do implicitly.
|
|
99
|
+
*/
|
|
100
|
+
export function dereference(doc) {
|
|
101
|
+
const externalRefs = new Set();
|
|
102
|
+
const resolving = new Map();
|
|
103
|
+
function resolvePointer(ref) {
|
|
104
|
+
const path = ref.slice(2).split("/").map(decodeSegment);
|
|
105
|
+
let node = doc;
|
|
106
|
+
for (const seg of path) {
|
|
107
|
+
if (node === null || typeof node !== "object")
|
|
108
|
+
return undefined;
|
|
109
|
+
node = node[seg];
|
|
110
|
+
}
|
|
111
|
+
return node;
|
|
112
|
+
}
|
|
113
|
+
function walk(node) {
|
|
114
|
+
if (Array.isArray(node))
|
|
115
|
+
return node.map(walk);
|
|
116
|
+
if (node === null || typeof node !== "object")
|
|
117
|
+
return node;
|
|
118
|
+
const obj = node;
|
|
119
|
+
const ref = obj.$ref;
|
|
120
|
+
if (typeof ref === "string") {
|
|
121
|
+
if (!ref.startsWith("#/")) {
|
|
122
|
+
externalRefs.add(ref);
|
|
123
|
+
return node;
|
|
124
|
+
}
|
|
125
|
+
const cached = resolving.get(ref);
|
|
126
|
+
if (cached !== undefined)
|
|
127
|
+
return cached;
|
|
128
|
+
const target = resolvePointer(ref);
|
|
129
|
+
if (target === undefined) {
|
|
130
|
+
throw new Error(`oat: unresolvable $ref ${ref}`);
|
|
131
|
+
}
|
|
132
|
+
/* Placeholder registered before recursing so a cycle lands on this same object
|
|
133
|
+
* instead of recursing forever. */
|
|
134
|
+
const placeholder = {};
|
|
135
|
+
resolving.set(ref, placeholder);
|
|
136
|
+
const resolved = walk(target);
|
|
137
|
+
Object.assign(placeholder, resolved);
|
|
138
|
+
/* Sibling keys alongside $ref (OpenAPI 3.1 allows this) override the target. */
|
|
139
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
140
|
+
if (k !== "$ref")
|
|
141
|
+
placeholder[k] = walk(v);
|
|
142
|
+
}
|
|
143
|
+
return placeholder;
|
|
144
|
+
}
|
|
145
|
+
const out = {};
|
|
146
|
+
for (const [k, v] of Object.entries(obj))
|
|
147
|
+
out[k] = walk(v);
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
return { doc: walk(doc), externalRefs: [...externalRefs] };
|
|
151
|
+
}
|
|
152
|
+
function decodeSegment(seg) {
|
|
153
|
+
return seg.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
154
|
+
}
|
|
155
|
+
/** Normalises `/things/:id` to `/things/{id}` so both path syntaxes compare equal. */
|
|
156
|
+
export function normalisePath(path) {
|
|
157
|
+
return path.replace(/:([A-Za-z_][A-Za-z0-9_]*)/g, "{$1}");
|
|
158
|
+
}
|
|
159
|
+
/** `"GET /things/:id"` → `{ method: "GET", path: "/things/{id}" }` */
|
|
160
|
+
export function parseRouteRef(ref) {
|
|
161
|
+
const match = /^\s*([A-Za-z]+)\s+(\S+)\s*$/.exec(ref);
|
|
162
|
+
if (!match?.[1] || !match[2])
|
|
163
|
+
return null;
|
|
164
|
+
return { method: match[1].toUpperCase(), path: normalisePath(match[2]) };
|
|
165
|
+
}
|
|
166
|
+
//# sourceMappingURL=load.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load.js","sourceRoot":"","sources":["../../src/spec/load.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** Minimal structural view of an OpenAPI 3.x document — only what oat reads. */
|
|
2
|
+
export type SchemaObject = Record<string, unknown>;
|
|
3
|
+
export interface ParameterObject {
|
|
4
|
+
name: string;
|
|
5
|
+
in: "path" | "query" | "header" | "cookie";
|
|
6
|
+
required?: boolean;
|
|
7
|
+
schema?: SchemaObject;
|
|
8
|
+
description?: string;
|
|
9
|
+
[k: string]: unknown;
|
|
10
|
+
}
|
|
11
|
+
export interface MediaTypeObject {
|
|
12
|
+
schema?: SchemaObject;
|
|
13
|
+
}
|
|
14
|
+
export interface ResponseObject {
|
|
15
|
+
description?: string;
|
|
16
|
+
content?: Record<string, MediaTypeObject>;
|
|
17
|
+
}
|
|
18
|
+
export interface OperationObject {
|
|
19
|
+
operationId?: string;
|
|
20
|
+
summary?: string;
|
|
21
|
+
tags?: string[];
|
|
22
|
+
parameters?: ParameterObject[];
|
|
23
|
+
requestBody?: {
|
|
24
|
+
required?: boolean;
|
|
25
|
+
content?: Record<string, MediaTypeObject>;
|
|
26
|
+
};
|
|
27
|
+
responses?: Record<string, ResponseObject>;
|
|
28
|
+
security?: Array<Record<string, string[]>>;
|
|
29
|
+
[k: `x-${string}`]: unknown;
|
|
30
|
+
}
|
|
31
|
+
export type PathItemObject = Record<string, OperationObject | unknown>;
|
|
32
|
+
export interface OpenApiDocument {
|
|
33
|
+
openapi?: string;
|
|
34
|
+
info?: Record<string, unknown>;
|
|
35
|
+
servers?: Array<{
|
|
36
|
+
url: string;
|
|
37
|
+
}>;
|
|
38
|
+
paths?: Record<string, PathItemObject>;
|
|
39
|
+
components?: {
|
|
40
|
+
schemas?: Record<string, SchemaObject>;
|
|
41
|
+
securitySchemes?: Record<string, unknown>;
|
|
42
|
+
};
|
|
43
|
+
security?: Array<Record<string, string[]>>;
|
|
44
|
+
[k: `x-${string}`]: unknown;
|
|
45
|
+
}
|
|
46
|
+
export declare const HTTP_METHODS: readonly ["get", "put", "post", "delete", "patch", "head", "options"];
|
|
47
|
+
export type HttpMethod = (typeof HTTP_METHODS)[number];
|
|
48
|
+
export declare const MUTATING_METHODS: ReadonlySet<string>;
|
|
49
|
+
/** An operation lifted out of the document with its location attached. */
|
|
50
|
+
export interface Endpoint {
|
|
51
|
+
operationId: string;
|
|
52
|
+
method: HttpMethod;
|
|
53
|
+
path: string;
|
|
54
|
+
op: OperationObject;
|
|
55
|
+
}
|
|
56
|
+
export declare function listEndpoints(doc: OpenApiDocument): Endpoint[];
|
|
57
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/spec/types.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAEhF,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAElD,MAAM,WAAW,eAAe;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,EAAE,EAAE,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,CAAA;IAC1C,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,MAAM,CAAC,EAAE,YAAY,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CACpB;AAED,MAAM,WAAW,eAAe;IAC/B,MAAM,CAAC,EAAE,YAAY,CAAA;CACrB;AAED,MAAM,WAAW,cAAc;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAA;CACzC;AAED,MAAM,WAAW,eAAe;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;IACf,UAAU,CAAC,EAAE,eAAe,EAAE,CAAA;IAC9B,WAAW,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAA;KAAE,CAAA;IAC/E,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAA;IAC1C,QAAQ,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,CAAA;IAC1C,CAAC,CAAC,EAAE,KAAK,MAAM,EAAE,GAAG,OAAO,CAAA;CAC3B;AAED,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,CAAA;AAEtE,MAAM,WAAW,eAAe;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,OAAO,CAAC,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAA;IACtC,UAAU,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;QAAC,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAA;IAClG,QAAQ,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,CAAA;IAC1C,CAAC,CAAC,EAAE,KAAK,MAAM,EAAE,GAAG,OAAO,CAAA;CAC3B;AAED,eAAO,MAAM,YAAY,uEAAwE,CAAA;AACjG,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAA;AAEtD,eAAO,MAAM,gBAAgB,EAAE,WAAW,CAAC,MAAM,CAA+C,CAAA;AAEhG,0EAA0E;AAC1E,MAAM,WAAW,QAAQ;IACxB,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,UAAU,CAAA;IAClB,IAAI,EAAE,MAAM,CAAA;IACZ,EAAE,EAAE,eAAe,CAAA;CACnB;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,eAAe,GAAG,QAAQ,EAAE,CAiB9D"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Minimal structural view of an OpenAPI 3.x document — only what oat reads. */
|
|
2
|
+
export const HTTP_METHODS = ["get", "put", "post", "delete", "patch", "head", "options"];
|
|
3
|
+
export const MUTATING_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
4
|
+
export function listEndpoints(doc) {
|
|
5
|
+
const out = [];
|
|
6
|
+
for (const [path, item] of Object.entries(doc.paths ?? {})) {
|
|
7
|
+
if (item === null || typeof item !== "object")
|
|
8
|
+
continue;
|
|
9
|
+
for (const method of HTTP_METHODS) {
|
|
10
|
+
const op = item[method];
|
|
11
|
+
if (op === null || typeof op !== "object")
|
|
12
|
+
continue;
|
|
13
|
+
const operation = op;
|
|
14
|
+
out.push({
|
|
15
|
+
method,
|
|
16
|
+
op: operation,
|
|
17
|
+
operationId: operation.operationId ?? `${method.toUpperCase()} ${path}`,
|
|
18
|
+
path,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/spec/types.ts"],"names":[],"mappings":"AAAA,gFAAgF;AA6ChF,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;YACvC,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"}
|