@m6d/cortex-cli 0.12.0 → 0.13.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.
Files changed (74) hide show
  1. package/README.md +135 -130
  2. package/package.json +16 -5
  3. package/src/cli.ts +17 -12
  4. package/src/commands/apply.ts +169 -0
  5. package/src/commands/{cc.ts → management.ts} +150 -100
  6. package/src/commands/new.ts +50 -74
  7. package/src/contracts/charts/chart-spec.ts +1 -1
  8. package/src/contracts/client-tools/index.ts +3 -3
  9. package/src/contracts/{runtime → datasets}/datasets.ts +7 -42
  10. package/src/contracts/datasets/index.ts +3 -0
  11. package/src/contracts/egress-policy/index.ts +16 -0
  12. package/src/contracts/{egress → egress-policy}/scrub.ts +34 -5
  13. package/src/contracts/{egress → egress-policy}/url.ts +22 -9
  14. package/src/contracts/errors/index.ts +38 -0
  15. package/src/contracts/json/index.ts +5 -0
  16. package/src/contracts/manifest/index.ts +932 -0
  17. package/src/contracts/manifest-file/index.ts +16 -0
  18. package/src/contracts/markdown/index.ts +1 -1
  19. package/src/contracts/pattern/index.ts +276 -0
  20. package/src/contracts/server/index.ts +247 -0
  21. package/src/contracts/server/reserved-names.fixtures.json +18 -0
  22. package/src/contracts/triggers/index.ts +112 -0
  23. package/src/contracts/wire/index.ts +61 -32
  24. package/src/{cc → management}/client.ts +19 -7
  25. package/src/{cc → management}/flags.ts +2 -1
  26. package/src/{cc → management}/profiles.ts +5 -5
  27. package/src/scaffold/files.ts +362 -237
  28. package/src/scaffold/ports.ts +13 -24
  29. package/src/scaffold/templates/datasets.ts.txt +52 -0
  30. package/src/scaffold/templates/dockerignore.txt +7 -0
  31. package/src/scaffold/templates/fallowrc.jsonc.txt +8 -12
  32. package/src/scaffold/templates/gitignore.txt +2 -1
  33. package/src/scaffold/templates/server.ts.txt +40 -0
  34. package/src/scaffold/templates/setup.ts.txt +52 -0
  35. package/src/ui/prompts.ts +4 -31
  36. package/src/ui/report.ts +5 -15
  37. package/tsconfig.json +2 -3
  38. package/src/commands/graph.ts +0 -228
  39. package/src/commands/swagger.ts +0 -247
  40. package/src/config/load.ts +0 -122
  41. package/src/config/validate.ts +0 -46
  42. package/src/contracts/credentials/index.ts +0 -183
  43. package/src/contracts/egress/index.ts +0 -120
  44. package/src/contracts/graph/clients/embed.ts +0 -45
  45. package/src/contracts/graph/clients/neo4j.ts +0 -85
  46. package/src/contracts/graph/helpers.ts +0 -55
  47. package/src/contracts/graph/index.ts +0 -30
  48. package/src/contracts/graph/schema.ts +0 -65
  49. package/src/contracts/graph/types.ts +0 -125
  50. package/src/contracts/rich-text/index.ts +0 -220
  51. package/src/contracts/runtime/api.ts +0 -345
  52. package/src/contracts/runtime/auth.ts +0 -41
  53. package/src/contracts/runtime/grammar.ts +0 -169
  54. package/src/contracts/runtime/index.ts +0 -15
  55. package/src/contracts/runtime/models.ts +0 -50
  56. package/src/contracts/runtime/signature.ts +0 -255
  57. package/src/contracts/runtime/sync.ts +0 -196
  58. package/src/contracts/runtime/triggers.ts +0 -267
  59. package/src/graph/expand-domains.ts +0 -286
  60. package/src/graph/generate-cypher.ts +0 -201
  61. package/src/graph/seed.ts +0 -199
  62. package/src/graph/validate.ts +0 -78
  63. package/src/scaffold/features.ts +0 -190
  64. package/src/scaffold/templates/domains-README.md.txt +0 -16
  65. package/src/scaffold/templates/index.ts.txt +0 -18
  66. package/src/swagger/extract-endpoints.ts +0 -269
  67. package/src/swagger/generated-block.ts +0 -74
  68. package/src/swagger/openapi-schema.ts +0 -168
  69. package/src/swagger/parse-response.ts +0 -88
  70. package/src/swagger/types.ts +0 -25
  71. /package/src/contracts/{runtime → datasets}/dataset-sql-guide.ts +0 -0
  72. /package/src/contracts/{runtime → datasets}/dataset-sql.ts +0 -0
  73. /package/src/contracts/{runtime → datasets}/datasets.fixtures.json +0 -0
  74. /package/src/{cc → management}/output.ts +0 -0
@@ -1,18 +0,0 @@
1
- import { createCortex } from "@m6d/cortex-server";
2
- import config from "./cortex.config";
3
-
4
- const cortex = createCortex(config);
5
-
6
- const server = await cortex.serve();
7
-
8
- // Your own routes hang off the same Hono app the agent runtime serves from:
9
- // server.app.get("/hello", function (c) {
10
- // return c.text("hello");
11
- // });
12
-
13
- export default {
14
- port: server.port,
15
- fetch: server.fetch,
16
- websocket: server.websocket,
17
- idleTimeout: 0,
18
- };
@@ -1,269 +0,0 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
- import { isRecord } from "@cortex/contracts/json";
4
- import type { Endpoint, HttpMethod, Obj } from "./types";
5
- import { dedupe, deref, obj, pickContent, schemaType, toProp, toProps } from "./openapi-schema";
6
- import { parseResponse } from "./parse-response";
7
- import { withGeneratedBlock } from "./generated-block";
8
-
9
- type ExtractEndpointsOptions = {
10
- swaggerUrls: string[];
11
- domainsDir: string;
12
- write: boolean;
13
- };
14
-
15
- const METHODS = ["GET", "POST", "PUT", "DELETE"] as const satisfies readonly HttpMethod[];
16
-
17
- /**
18
- * ASP.NET conventions the extractor normalizes away so an `.endpoint.ts` path
19
- * matches the spec whichever way the API was mounted: routes are keyed without
20
- * the `/api` prefix (either side may carry it), and `JsonNode`-typed query
21
- * parameters, which Swagger expands into `Root.Options`, `Root.Parent` and
22
- * `Root.Root` dot-paths, collapse back to one object parameter.
23
- */
24
- const API_PREFIX = /^\/api(?=\/|$)/i;
25
- const JSON_NODE_MEMBER = /^(Options|Parent|Root)\b/;
26
-
27
- function toCamelCase(s: string) {
28
- return s
29
- .split(".")
30
- .map((seg) => seg.replace(/^[A-Z]/, (c) => c.toLowerCase()))
31
- .join(".");
32
- }
33
-
34
- /** `Root` for a dotted parameter like `Root.Options`; undefined for a plain one. */
35
- function dottedRoot(name: string) {
36
- const dot = name.indexOf(".");
37
- return dot < 0 ? undefined : name.slice(0, dot);
38
- }
39
-
40
- function normalizePath(raw: string) {
41
- let p = raw.startsWith("/") ? raw : `/${raw}`;
42
- p = p.replace(/\{([^}:?]+)(?::[^}]+)?\??\}/g, "{$1}").replace(API_PREFIX, "") || "/";
43
- return p.length > 1 ? p.replace(/\/+$/, "") : p;
44
- }
45
-
46
- function walkFiles(dir: string) {
47
- if (!fs.existsSync(dir)) return [];
48
- const out: string[] = [];
49
- function walk(d: string) {
50
- for (const e of fs.readdirSync(d, { withFileTypes: true })) {
51
- const p = path.join(d, e.name);
52
- if (e.isDirectory()) walk(p);
53
- else out.push(p);
54
- }
55
- }
56
- walk(dir);
57
- return out;
58
- }
59
-
60
- /**
61
- * The `method` and `path` an endpoint file declares, read the way the server's
62
- * own file walk reads them.
63
- */
64
- export function declaredRoute(source: string) {
65
- return {
66
- method: source.match(/^\s*method:\s*(['"])([^'"]+)\1/m)?.[2],
67
- path: source.match(/^\s*path:\s*(['"])([^'"]+)\1/m)?.[2],
68
- };
69
- }
70
-
71
- function endpointFiles(root: string) {
72
- const out = new Map<string, string>();
73
- for (const filePath of walkFiles(root).filter((p) => p.endsWith(".endpoint.ts"))) {
74
- const { method, path: route } = declaredRoute(fs.readFileSync(filePath, "utf8"));
75
- if (method && route) out.set(`${method}:${normalizePath(route)}`, filePath);
76
- }
77
- return out;
78
- }
79
-
80
- /**
81
- * Path and query parameters, path-level first, with each `JsonNode` parameter
82
- * collapsed back to one object.
83
- */
84
- function extractParams(doc: unknown, pathItem: Obj, operation: Obj) {
85
- const rawParams = [
86
- ...(Array.isArray(pathItem.parameters) ? (pathItem.parameters as unknown[]) : []),
87
- ...(Array.isArray(operation.parameters) ? (operation.parameters as unknown[]) : []),
88
- ]
89
- .map((param) => deref(doc, param))
90
- .filter(
91
- (param): param is Obj & { name: string; in: string } =>
92
- typeof param.name === "string" &&
93
- typeof param.in === "string" &&
94
- ["path", "query"].includes(param.in),
95
- );
96
-
97
- const jsonNodeRoots = new Set<string>();
98
- for (const { name } of rawParams) {
99
- const root = dottedRoot(name);
100
- if (root !== undefined && JSON_NODE_MEMBER.test(name.slice(root.length + 1))) {
101
- jsonNodeRoots.add(root);
102
- }
103
- }
104
-
105
- return dedupe(
106
- rawParams
107
- .filter((param) => {
108
- const root = dottedRoot(param.name);
109
- return root === undefined || !jsonNodeRoots.has(root);
110
- })
111
- .map((param) =>
112
- toProp(
113
- doc,
114
- toCamelCase(param.name),
115
- param.schema,
116
- param.in === "path" || param.required === true,
117
- ),
118
- )
119
- .concat(
120
- [...jsonNodeRoots].map((root) => ({
121
- name: toCamelCase(root),
122
- required: false,
123
- type: "object",
124
- })),
125
- ),
126
- );
127
- }
128
-
129
- /** An object body contributes its own fields; anything else is one `items` or `body` field. */
130
- function extractBody(doc: unknown, operation: Obj) {
131
- const requestBody = deref(doc, operation.requestBody);
132
- const content = pickContent(requestBody.content);
133
- if (!content) return [];
134
- const schema = deref(doc, content.schema);
135
- const type = schemaType(schema);
136
- if (type === "object") return toProps(doc, schema);
137
- const name = type === "array" ? "items" : "body";
138
- return [toProp(doc, name, schema, requestBody.required === true)];
139
- }
140
-
141
- function extractEndpoint(
142
- doc: unknown,
143
- route: string,
144
- method: HttpMethod,
145
- pathItem: Obj,
146
- operation: Obj,
147
- ) {
148
- const normalizedPath = normalizePath(route);
149
- return {
150
- key: `${method}:${normalizedPath}`,
151
- method,
152
- path: normalizedPath,
153
- params: extractParams(doc, pathItem, operation),
154
- body: extractBody(doc, operation),
155
- ...parseResponse(doc, operation.responses),
156
- } satisfies Endpoint;
157
- }
158
-
159
- function parseSwaggerEndpoints(swagger: unknown) {
160
- const out = new Map<string, Endpoint>();
161
- const doc = obj(swagger);
162
- for (const [route, pathItemIn] of Object.entries(obj(doc.paths))) {
163
- const pathItem = obj(pathItemIn);
164
- for (const method of METHODS) {
165
- const operation = pathItem[method.toLowerCase()];
166
- if (!isRecord(operation)) continue;
167
- const endpoint = extractEndpoint(doc, route, method, pathItem, operation);
168
- out.set(endpoint.key, endpoint);
169
- }
170
- }
171
- return out;
172
- }
173
-
174
- function formatGeneratedFiles(filePaths: string[]) {
175
- if (!filePaths.length) return;
176
- try {
177
- // Absolute paths: a relative domainsDir yields paths relative to the
178
- // process cwd, which a prettier run elsewhere would resolve wrongly.
179
- Bun.spawnSync(["bunx", "prettier", "--write", ...filePaths.map((p) => path.resolve(p))], {
180
- stdio: ["ignore", "ignore", "ignore"],
181
- });
182
- } catch {
183
- // Formatting is cosmetic; a missing prettier must not fail the run.
184
- }
185
- }
186
-
187
- async function fetchJson(url: string) {
188
- const response = await fetch(url, {
189
- headers: { Accept: "application/json" },
190
- });
191
- if (!response.ok) {
192
- throw new Error(`Swagger request failed (${response.status})`);
193
- }
194
- return response.json();
195
- }
196
-
197
- function resolveEndpoint(key: string, extracted: Map<string, Endpoint>) {
198
- const [method, route] = key.split(":");
199
- if (!method || !route) return undefined;
200
- return (
201
- extracted.get(key) ??
202
- extracted.get(`${method}:${normalizePath(`/api${route}`)}`) ??
203
- (route.startsWith("/api/")
204
- ? extracted.get(`${method}:${normalizePath(route.slice(4))}`)
205
- : undefined)
206
- );
207
- }
208
-
209
- // The post-write prettier pass reformats generated blocks, so an exact string
210
- // compare would report freshly synced files as drift forever. Compare modulo
211
- // the axes prettier varies: quote style, whitespace, and trailing commas.
212
- function sameModuloFormatting(a: string, b: string) {
213
- function norm(s: string) {
214
- return s
215
- .replace(/['"]/g, '"')
216
- .replace(/\s+/g, "")
217
- .replace(/,(?=[}\])])/g, "");
218
- }
219
- return norm(a) === norm(b);
220
- }
221
-
222
- export async function extractEndpoints(options: ExtractEndpointsOptions) {
223
- const { swaggerUrls, domainsDir, write } = options;
224
-
225
- // Merge every document first: a file missing from one swagger doc may be
226
- // matched by another. Last write wins on duplicate keys, so per-agent docs
227
- // deliberately override the server-level one (`swaggerUrls` lists it first).
228
- const extracted = new Map<string, Endpoint>();
229
- for (const swaggerUrl of swaggerUrls) {
230
- for (const [key, endpoint] of parseSwaggerEndpoints(await fetchJson(swaggerUrl))) {
231
- extracted.set(key, endpoint);
232
- }
233
- }
234
-
235
- let matched = 0;
236
- const missing: string[] = [];
237
- const drift: string[] = [];
238
- const updated: string[] = [];
239
- const updatedFiles: string[] = [];
240
-
241
- for (const [key, filePath] of endpointFiles(domainsDir).entries()) {
242
- const relativePath = path.relative(domainsDir, filePath);
243
- const endpoint = resolveEndpoint(key, extracted);
244
- if (!endpoint) {
245
- missing.push(relativePath);
246
- continue;
247
- }
248
-
249
- const current = fs.readFileSync(filePath, "utf8");
250
- const next = withGeneratedBlock(current, endpoint);
251
- if (next === null || next === current || sameModuloFormatting(next, current)) {
252
- matched++;
253
- continue;
254
- }
255
-
256
- if (!write) {
257
- drift.push(relativePath);
258
- continue;
259
- }
260
-
261
- fs.writeFileSync(filePath, next);
262
- updatedFiles.push(filePath);
263
- updated.push(relativePath);
264
- }
265
-
266
- if (write) formatGeneratedFiles(updatedFiles);
267
-
268
- return { matched, missing, drift, updated };
269
- }
@@ -1,74 +0,0 @@
1
- /**
2
- * Splicing the generated block into an endpoint definition the developer also edits
3
- * by hand. The markers are the contract: everything between them belongs to the
4
- * generator, everything outside is the developer's.
5
- */
6
- import type { Endpoint, Prop } from "./types";
7
-
8
- const AUTO_START = "// @auto-generated-start";
9
- const AUTO_END = "// @auto-generated-end";
10
-
11
- function serializeProps(props: Prop[], depth: number) {
12
- return props
13
- .map((p) => {
14
- const indent = " ".repeat(depth);
15
- const childIndent = " ".repeat(depth + 1);
16
- const typeLiteral = p.type.includes("'")
17
- ? `"${p.type.replace(/"/g, '\\"')}"`
18
- : `'${p.type}'`;
19
-
20
- const parts = [`name: '${p.name}'`, `required: ${p.required}`, `type: ${typeLiteral}`];
21
- if (p.isArray) parts.push("isArray: true");
22
- if (p.properties?.length)
23
- parts.push(
24
- `properties: [\n${serializeProps(p.properties, depth + 1)}\n${childIndent}]`,
25
- );
26
-
27
- return `${indent} { ${parts.join(", ")} },`;
28
- })
29
- .join("\n");
30
- }
31
-
32
- function blockFor(endpoint: Endpoint) {
33
- return ` autoGenerated: {
34
- params: [
35
- ${serializeProps(endpoint.params, 2)}
36
- ] as const,
37
- body: [
38
- ${serializeProps(endpoint.body, 2)}
39
- ] as const,
40
- response: [
41
- ${serializeProps(endpoint.response, 2)}
42
- ] as const,
43
- successStatus: ${endpoint.successStatus},
44
- errorStatuses: [${endpoint.errorStatuses.join(", ")}],
45
- responseKind: '${endpoint.responseKind}',
46
- } as const,`;
47
- }
48
-
49
- /**
50
- * Replaces the marked region when it exists, otherwise inserts one just inside the
51
- * closing `});`. Returns null when there is nowhere to put it, which means the file
52
- * is not shaped like an endpoint definition.
53
- */
54
- export function withGeneratedBlock(current: string, endpoint: Endpoint) {
55
- const start = current.indexOf(AUTO_START);
56
- const end = current.indexOf(AUTO_END);
57
-
58
- if (start >= 0 && end > start) {
59
- return (
60
- current.slice(0, start + AUTO_START.length) +
61
- `\n${blockFor(endpoint)}\n ` +
62
- current.slice(end)
63
- );
64
- }
65
-
66
- const closingIndex = current.lastIndexOf("});");
67
- if (closingIndex < 0) return null;
68
-
69
- return (
70
- current.slice(0, closingIndex) +
71
- `\n ${AUTO_START}\n${blockFor(endpoint)}\n ${AUTO_END}\n});` +
72
- current.slice(closingIndex + 3)
73
- );
74
- }
@@ -1,168 +0,0 @@
1
- /**
2
- * Reading an OpenAPI document: following $ref, deciding a schema's type, and turning
3
- * a schema into the flat property list the generator emits. Nothing here knows what
4
- * the properties are for.
5
- */
6
- import { isRecord } from "@cortex/contracts/json";
7
- import type { Obj, Prop } from "./types";
8
-
9
- const MAX_DEPTH = 8;
10
-
11
- export function obj(v: unknown) {
12
- return isRecord(v) ? v : {};
13
- }
14
-
15
- function strings(v: unknown) {
16
- return Array.isArray(v) ? v.filter((x): x is string => typeof x === "string") : [];
17
- }
18
-
19
- /** The first prop of each name. */
20
- export function dedupe(items: Prop[]) {
21
- const seen = new Set<string>();
22
- return items.filter((prop) => {
23
- if (seen.has(prop.name)) return false;
24
- seen.add(prop.name);
25
- return true;
26
- });
27
- }
28
-
29
- function resolvePointer(doc: unknown, ref: string) {
30
- if (!ref.startsWith("#/")) return null;
31
- let cur: unknown = doc;
32
- for (const part of ref.slice(2).split("/")) {
33
- const key = part.replace(/~1/g, "/").replace(/~0/g, "~");
34
- if (!isRecord(cur) || !(key in cur)) return null;
35
- cur = cur[key];
36
- }
37
- return cur;
38
- }
39
-
40
- export function deref(doc: unknown, v: unknown) {
41
- if (!isRecord(v)) return {};
42
- let x = v;
43
- const seen = new Set<string>();
44
- while (typeof x.$ref === "string") {
45
- if (seen.has(x.$ref)) return {};
46
- seen.add(x.$ref);
47
- x = obj(resolvePointer(doc, x.$ref));
48
- }
49
- return x;
50
- }
51
-
52
- export function schemaType(s: Obj) {
53
- const t = Array.isArray(s.type)
54
- ? (s.type as unknown[]).find((x) => typeof x === "string" && x !== "null")
55
- : s.type;
56
- if (typeof t === "string") return t;
57
- if (isRecord(s.properties)) return "object";
58
- if (isRecord(s.items)) return "array";
59
- return "";
60
- }
61
-
62
- function isJsonNodeRef(v: unknown) {
63
- return (
64
- isRecord(v) &&
65
- typeof v.$ref === "string" &&
66
- /\/System\.Text\.Json\.Nodes\.JsonNode$/.test(v.$ref)
67
- );
68
- }
69
-
70
- function isJsonNodeSchema(s: Obj) {
71
- const p = obj(s.properties);
72
- return (
73
- schemaType(s) === "object" &&
74
- "options" in p &&
75
- "parent" in p &&
76
- "root" in p &&
77
- isJsonNodeRef(p.parent) &&
78
- isJsonNodeRef(p.root)
79
- );
80
- }
81
-
82
- function scalarType(s: Obj) {
83
- if (Array.isArray(s.enum) && s.enum.every((v) => typeof v === "string")) {
84
- return s.enum.map((v) => `'${String(v).replace(/'/g, "\\'")}'`).join(" | ");
85
- }
86
- const t = schemaType(s);
87
- const f = typeof s.format === "string" ? s.format : "";
88
- if (f === "uuid") return "uuid";
89
- if (f === "date") return "date";
90
- if (f === "date-time") return "datetime";
91
- if (t === "integer" || t === "number") return "number";
92
- if (t === "boolean") return "boolean";
93
- if (t === "string") return "string";
94
- if (t === "object") return "object";
95
- return "unknown";
96
- }
97
-
98
- export function toProps(doc: unknown, schemaIn: unknown, depth = 0) {
99
- if (depth > MAX_DEPTH) return [];
100
- const s = deref(doc, schemaIn);
101
- if (isJsonNodeRef(schemaIn) || isJsonNodeSchema(s)) return [];
102
- const req = new Set(strings(s.required));
103
- return dedupe(
104
- Object.entries(obj(s.properties)).map(([name, child]) =>
105
- toProp(doc, name, child, req.has(name), depth + 1),
106
- ),
107
- );
108
- }
109
-
110
- export function toProp(
111
- doc: unknown,
112
- name: string,
113
- schemaIn: unknown,
114
- required: boolean,
115
- depth = 0,
116
- // eslint-disable-next-line m6d/no-explicit-return-type -- mutually recursive with toProps, so the return type cannot be inferred
117
- ): Prop {
118
- if (isJsonNodeRef(schemaIn)) return { name, required, type: "object" };
119
-
120
- const s = deref(doc, schemaIn);
121
- const req = required && s.nullable !== true;
122
-
123
- if (isJsonNodeSchema(s)) return { name, required: req, type: "object" };
124
-
125
- const t = schemaType(s);
126
- if (t === "array") {
127
- const item = deref(doc, s.items);
128
- if (schemaType(item) === "object") {
129
- const properties = toProps(doc, item, depth + 1);
130
- return {
131
- name,
132
- required: req,
133
- type: "object",
134
- isArray: true,
135
- ...(properties.length ? { properties } : {}),
136
- };
137
- }
138
- return { name, required: req, type: scalarType(item), isArray: true };
139
- }
140
-
141
- if (t === "object") {
142
- const properties = toProps(doc, s, depth + 1);
143
- return {
144
- name,
145
- required: req,
146
- type: "object",
147
- ...(properties.length ? { properties } : {}),
148
- };
149
- }
150
-
151
- return { name, required: req, type: scalarType(s) };
152
- }
153
-
154
- /**
155
- * The schema under `application/json`, else any JSON type, else the first
156
- * type, along with every type offered.
157
- */
158
- export function pickContent(contentIn: unknown) {
159
- const content = obj(contentIn);
160
- const mimes = Object.keys(content);
161
- const [first] = mimes;
162
- if (first === undefined) return null;
163
- const mime =
164
- mimes.find((x) => x.toLowerCase() === "application/json") ??
165
- mimes.find((x) => x.toLowerCase().includes("json")) ??
166
- first;
167
- return { schema: obj(content[mime]).schema, mimes };
168
- }
@@ -1,88 +0,0 @@
1
- /**
2
- * Deciding what an endpoint answers with: which statuses it uses, and what shape the
3
- * success body takes. The shape is what drives code generation, so a paged envelope
4
- * is reported as the rows it wraps rather than as its own page fields.
5
- */
6
- import type { Obj, Prop, ResponseKind } from "./types";
7
- import { deref, obj, pickContent, schemaType, toProp, toProps } from "./openapi-schema";
8
-
9
- type ResponseBody = { responseKind: ResponseKind; response: Prop[] };
10
-
11
- const NO_BODY = { responseKind: "none", response: [] } satisfies ResponseBody;
12
-
13
- const FILE_MIME = /(application\/octet-stream|application\/pdf|application\/vnd\.|text\/csv)/i;
14
-
15
- /** An object contributes its own fields; anything else stands as a single `item`. */
16
- function itemProps(doc: unknown, item: Obj) {
17
- return schemaType(item) === "object" ? toProps(doc, item) : [toProp(doc, "item", item, true)];
18
- }
19
-
20
- function isFileResponse(mimes: string[], schema: Obj) {
21
- if (mimes.some((mime) => FILE_MIME.test(mime))) return true;
22
-
23
- return (
24
- schemaType(schema) === "string" &&
25
- typeof schema.format === "string" &&
26
- ["binary", "base64"].includes(schema.format)
27
- );
28
- }
29
-
30
- /** A paged envelope is described by the items it wraps, not by its own page fields. */
31
- function parseObjectBody(doc: unknown, schema: Obj) {
32
- const props = obj(schema.properties);
33
- const hasPaging = ["totalCount", "pageNumber", "pageSize", "totalPages"].some(
34
- (k) => k in props,
35
- );
36
- const list = deref(doc, props.items ?? props.data ?? props.results);
37
-
38
- if (hasPaging && schemaType(list) === "array") {
39
- return {
40
- responseKind: "paginated",
41
- response: itemProps(doc, deref(doc, list.items)),
42
- } satisfies ResponseBody;
43
- }
44
-
45
- return { responseKind: "object", response: toProps(doc, schema) } satisfies ResponseBody;
46
- }
47
-
48
- function parseResponseBody(doc: unknown, schema: Obj, mimes: string[]) {
49
- if (isFileResponse(mimes, schema))
50
- return { responseKind: "file", response: [] } satisfies ResponseBody;
51
-
52
- const type = schemaType(schema);
53
- if (type === "array") {
54
- return {
55
- responseKind: "array",
56
- response: itemProps(doc, deref(doc, schema.items)),
57
- } satisfies ResponseBody;
58
- }
59
- if (type === "object") return parseObjectBody(doc, schema);
60
- if (Object.keys(schema).length) {
61
- return {
62
- responseKind: "object",
63
- response: [toProp(doc, "value", schema, true)],
64
- } satisfies ResponseBody;
65
- }
66
-
67
- return NO_BODY;
68
- }
69
-
70
- export function parseResponse(doc: unknown, responsesIn: unknown) {
71
- const responses = obj(responsesIn);
72
- const statuses = Object.keys(responses)
73
- .map(Number)
74
- .filter(Number.isInteger)
75
- .sort((a, b) => a - b);
76
- const successStatus = statuses.find((s) => s >= 200 && s < 300) ?? 200;
77
- const errorStatuses = [...new Set(statuses.filter((s) => s >= 400))];
78
-
79
- // 204 says there is no body at all, so there is nothing further to read.
80
- if (successStatus === 204) return { successStatus, errorStatuses, ...NO_BODY };
81
-
82
- const success = deref(doc, responses[String(successStatus)]);
83
- const content = pickContent(success.content);
84
- if (!content) return { successStatus, errorStatuses, ...NO_BODY };
85
-
86
- const body = parseResponseBody(doc, deref(doc, content.schema), content.mimes);
87
- return { successStatus, errorStatuses, ...body };
88
- }
@@ -1,25 +0,0 @@
1
- /**
2
- * The shapes shared across endpoint extraction: what an OpenAPI document looks like
3
- * on the way in, and what one generated endpoint looks like on the way out.
4
- */
5
- export type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
6
- export type ResponseKind = "object" | "array" | "paginated" | "file" | "none";
7
- export type Obj = Record<string, unknown>;
8
- export type Prop = {
9
- name: string;
10
- required: boolean;
11
- type: string;
12
- isArray?: boolean;
13
- properties?: Prop[];
14
- };
15
- export type Endpoint = {
16
- key: string;
17
- method: HttpMethod;
18
- path: string;
19
- params: Prop[];
20
- body: Prop[];
21
- response: Prop[];
22
- responseKind: ResponseKind;
23
- successStatus: number;
24
- errorStatuses: number[];
25
- };
File without changes