@zenera/rag 1.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 +144 -0
- package/dist/command.d.ts +3 -0
- package/dist/command.js +436 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +16 -0
- package/dist/present.d.ts +17 -0
- package/dist/present.js +40 -0
- package/dist/query.d.ts +7 -0
- package/dist/query.js +88 -0
- package/dist/repl.d.ts +8 -0
- package/dist/repl.js +119 -0
- package/dist/schema/build.d.ts +28 -0
- package/dist/schema/build.js +79 -0
- package/dist/schema/entities.d.ts +18 -0
- package/dist/schema/entities.js +71 -0
- package/dist/schema/files.d.ts +74 -0
- package/dist/schema/files.js +79 -0
- package/dist/schema/graph.d.ts +48 -0
- package/dist/schema/graph.js +320 -0
- package/dist/schema/hydrate.d.ts +19 -0
- package/dist/schema/hydrate.js +182 -0
- package/dist/schema/render.d.ts +11 -0
- package/dist/schema/render.js +254 -0
- package/dist/schema/schema.d.ts +11 -0
- package/dist/schema/schema.js +189 -0
- package/dist/schema/search.d.ts +50 -0
- package/dist/schema/search.js +142 -0
- package/dist/schema/spec.d.ts +58 -0
- package/dist/schema/spec.js +309 -0
- package/dist/schema/store.d.ts +32 -0
- package/dist/schema/store.js +126 -0
- package/dist/schema/subgraph.d.ts +42 -0
- package/dist/schema/subgraph.js +272 -0
- package/dist/schema/tools.d.ts +10 -0
- package/dist/schema/tools.js +242 -0
- package/dist/schema/typescript.d.ts +22 -0
- package/dist/schema/typescript.js +246 -0
- package/package.json +59 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { MultiDirectedGraph } from 'graphology';
|
|
2
|
+
import { isObject, refName } from "./schema.js";
|
|
3
|
+
import { methodTypeOf, } from "./spec.js";
|
|
4
|
+
import { Printer } from "./typescript.js";
|
|
5
|
+
export const methodId = (operationId) => `Method:${operationId}`;
|
|
6
|
+
export const typeId = (name) => `Type:${name}`;
|
|
7
|
+
export const propertyId = (parent, name) => `Property:${parent}.${name}`;
|
|
8
|
+
export const paramId = (operationId, name) => `Property:${operationId}#${name}`;
|
|
9
|
+
/** How deep an anonymous object is given a name of its own before giving up. */
|
|
10
|
+
const MAX_SYNTHESIS_DEPTH = 4;
|
|
11
|
+
export function buildGraph(corpus) {
|
|
12
|
+
return new Builder(corpus).run();
|
|
13
|
+
}
|
|
14
|
+
class Builder {
|
|
15
|
+
#corpus;
|
|
16
|
+
#graph = new MultiDirectedGraph();
|
|
17
|
+
#types;
|
|
18
|
+
/** property node -> the schema it was built from, for signatures at the end */
|
|
19
|
+
#schemas = new Map();
|
|
20
|
+
/** synthesized type -> how many levels of anonymity it sat under */
|
|
21
|
+
#depths = new Map();
|
|
22
|
+
#source;
|
|
23
|
+
#queue = [];
|
|
24
|
+
#expanded = new Set();
|
|
25
|
+
constructor(corpus) {
|
|
26
|
+
this.#corpus = corpus;
|
|
27
|
+
this.#types = { ...corpus.types };
|
|
28
|
+
this.#source = { ...corpus.typeSource };
|
|
29
|
+
this.#queue.push(...Object.keys(corpus.types));
|
|
30
|
+
}
|
|
31
|
+
run() {
|
|
32
|
+
for (const operation of this.#corpus.operations) {
|
|
33
|
+
this.#operation(operation);
|
|
34
|
+
}
|
|
35
|
+
while (this.#queue.length > 0) {
|
|
36
|
+
this.#expand(this.#queue.pop());
|
|
37
|
+
}
|
|
38
|
+
this.#signatures();
|
|
39
|
+
propagate(this.#graph);
|
|
40
|
+
return { graph: this.#graph, types: this.#types };
|
|
41
|
+
}
|
|
42
|
+
// -----------------------------------------------------------------------
|
|
43
|
+
// Operations
|
|
44
|
+
// -----------------------------------------------------------------------
|
|
45
|
+
#operation(op) {
|
|
46
|
+
const id = methodId(op.operationId);
|
|
47
|
+
this.#graph.mergeNode(id, {
|
|
48
|
+
...blank(),
|
|
49
|
+
kind: 'method',
|
|
50
|
+
name: op.operationId,
|
|
51
|
+
doc: op.summary || op.description,
|
|
52
|
+
methodType: methodTypeOf(op.method),
|
|
53
|
+
httpMethod: op.method.toUpperCase(),
|
|
54
|
+
path: op.path,
|
|
55
|
+
source: op.source,
|
|
56
|
+
});
|
|
57
|
+
for (const param of op.params) {
|
|
58
|
+
const node = paramId(op.operationId, param.name);
|
|
59
|
+
this.#graph.mergeNode(node, {
|
|
60
|
+
...blank(),
|
|
61
|
+
kind: 'property',
|
|
62
|
+
name: param.name,
|
|
63
|
+
parent: op.operationId,
|
|
64
|
+
doc: param.doc,
|
|
65
|
+
source: op.source,
|
|
66
|
+
required: param.required,
|
|
67
|
+
});
|
|
68
|
+
this.#schemas.set(node, param.schema);
|
|
69
|
+
this.#edge(id, node, { relation: 'HAS_PARAM', status: 0, in: param.in });
|
|
70
|
+
const target = this.#resolve(param.schema, `${op.operationId}_${param.name}`, 1, op.source);
|
|
71
|
+
if (target) {
|
|
72
|
+
this.#edge(node, typeId(target), edge('OF_TYPE'));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (op.requestBody) {
|
|
76
|
+
const target = this.#resolve(op.requestBody.schema, `${op.operationId}Request`, 0, op.source);
|
|
77
|
+
if (target) {
|
|
78
|
+
this.#edge(id, typeId(target), edge('TAKES_INPUT'));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const many = op.responses.length > 1;
|
|
82
|
+
for (const response of op.responses) {
|
|
83
|
+
const preferred = many
|
|
84
|
+
? `${op.operationId}Response${response.status}`
|
|
85
|
+
: `${op.operationId}Response`;
|
|
86
|
+
const target = this.#resolve(response.schema, preferred, 0, op.source);
|
|
87
|
+
if (target) {
|
|
88
|
+
this.#edge(id, typeId(target), {
|
|
89
|
+
relation: 'RETURNS_OUTPUT',
|
|
90
|
+
status: response.status,
|
|
91
|
+
in: '',
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// -----------------------------------------------------------------------
|
|
97
|
+
// Types
|
|
98
|
+
// -----------------------------------------------------------------------
|
|
99
|
+
/**
|
|
100
|
+
* The type id a schema stands for, inventing one where the document did
|
|
101
|
+
* not bother. An array is unwrapped first: `returns User[]` and
|
|
102
|
+
* `returns User` are the same edge as far as reaching `User` goes, and
|
|
103
|
+
* keeping a `UsersResponse` wrapper in between costs a hop and says nothing.
|
|
104
|
+
*/
|
|
105
|
+
#resolve(schema, preferred, depth, source) {
|
|
106
|
+
if (!isObject(schema)) {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
const ref = refName(schema);
|
|
110
|
+
if (ref !== undefined) {
|
|
111
|
+
return this.#types[ref] ? ref : undefined;
|
|
112
|
+
}
|
|
113
|
+
if (schema.type === 'array' || schema.items !== undefined) {
|
|
114
|
+
return this.#resolve(schema.items, preferred, depth, source);
|
|
115
|
+
}
|
|
116
|
+
if (!worthNaming(schema) || depth >= MAX_SYNTHESIS_DEPTH) {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
return this.#synthesize(preferred, schema, depth, source);
|
|
120
|
+
}
|
|
121
|
+
#synthesize(preferred, schema, depth, source) {
|
|
122
|
+
let name = preferred;
|
|
123
|
+
for (let n = 2; this.#types[name]; n++) {
|
|
124
|
+
name = `${preferred}${n}`;
|
|
125
|
+
}
|
|
126
|
+
this.#types[name] = schema;
|
|
127
|
+
this.#source[name] = source;
|
|
128
|
+
this.#depths.set(name, depth);
|
|
129
|
+
this.#queue.push(name);
|
|
130
|
+
return name;
|
|
131
|
+
}
|
|
132
|
+
#expand(name) {
|
|
133
|
+
if (this.#expanded.has(name)) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
this.#expanded.add(name);
|
|
137
|
+
const schema = this.#types[name];
|
|
138
|
+
if (!schema) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const id = typeId(name);
|
|
142
|
+
const depth = this.#depths.get(name) ?? 0;
|
|
143
|
+
const source = this.#source[name] ?? '';
|
|
144
|
+
this.#graph.mergeNode(id, {
|
|
145
|
+
...blank(),
|
|
146
|
+
kind: 'type',
|
|
147
|
+
name,
|
|
148
|
+
doc: docOf(schema),
|
|
149
|
+
source,
|
|
150
|
+
});
|
|
151
|
+
for (const key of ['allOf', 'anyOf', 'oneOf']) {
|
|
152
|
+
const list = schema[key];
|
|
153
|
+
if (!Array.isArray(list)) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
for (const member of list) {
|
|
157
|
+
const target = refName(member);
|
|
158
|
+
if (target && this.#types[target]) {
|
|
159
|
+
this.#edge(id, typeId(target), edge('COMPOSES'));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const items = this.#resolve(schema.items, `${name}Item`, depth + 1, source);
|
|
164
|
+
if (items) {
|
|
165
|
+
this.#edge(id, typeId(items), edge('ITEM_OF'));
|
|
166
|
+
}
|
|
167
|
+
const { properties, required } = flatten(schema, this.#types);
|
|
168
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
169
|
+
const node = propertyId(name, key);
|
|
170
|
+
this.#graph.mergeNode(node, {
|
|
171
|
+
...blank(),
|
|
172
|
+
kind: 'property',
|
|
173
|
+
name: key,
|
|
174
|
+
parent: name,
|
|
175
|
+
doc: docOf(value),
|
|
176
|
+
source,
|
|
177
|
+
required: required.has(key),
|
|
178
|
+
});
|
|
179
|
+
this.#schemas.set(node, value);
|
|
180
|
+
this.#edge(id, node, edge('HAS_PROPERTY'));
|
|
181
|
+
const target = this.#resolve(value, `${name}_${capital(key)}`, depth + 1, source);
|
|
182
|
+
if (target) {
|
|
183
|
+
this.#edge(node, typeId(target), edge('OF_TYPE'));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// -----------------------------------------------------------------------
|
|
188
|
+
#edge(from, to, attrs) {
|
|
189
|
+
if (!this.#graph.hasNode(to)) {
|
|
190
|
+
this.#graph.addNode(to, { ...blank(), kind: 'type', name: to.slice(5) });
|
|
191
|
+
}
|
|
192
|
+
this.#graph.addDirectedEdge(from, to, attrs);
|
|
193
|
+
}
|
|
194
|
+
/** One printer over the settled corpus, so every signature agrees. */
|
|
195
|
+
#signatures() {
|
|
196
|
+
const printer = new Printer(this.#types);
|
|
197
|
+
for (const [node, schema] of this.#schemas) {
|
|
198
|
+
if (this.#graph.hasNode(node)) {
|
|
199
|
+
this.#graph.setNodeAttribute(node, 'signature', printer.signature(schema));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
// Direction
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
/**
|
|
208
|
+
* Which side of a call a node lives on. Seeded from the operations — a request
|
|
209
|
+
* body is an input, a response is an output — and then pushed down through
|
|
210
|
+
* composition until it stops. A DTO used both ways comes out `both`, which is
|
|
211
|
+
* the honest answer and the reason this is a closure rather than a flag set at
|
|
212
|
+
* the point of use.
|
|
213
|
+
*/
|
|
214
|
+
export function propagate(graph) {
|
|
215
|
+
const inputs = new Set();
|
|
216
|
+
const outputs = new Set();
|
|
217
|
+
graph.forEachDirectedEdge((_edge, attrs, _source, target) => {
|
|
218
|
+
if (attrs.relation === 'TAKES_INPUT' || attrs.relation === 'HAS_PARAM') {
|
|
219
|
+
inputs.add(target);
|
|
220
|
+
}
|
|
221
|
+
else if (attrs.relation === 'RETURNS_OUTPUT') {
|
|
222
|
+
outputs.add(target);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
const DOWN = new Set(['HAS_PROPERTY', 'OF_TYPE', 'COMPOSES', 'ITEM_OF']);
|
|
226
|
+
for (const [seeds, mark] of [
|
|
227
|
+
[inputs, 'input'],
|
|
228
|
+
[outputs, 'output'],
|
|
229
|
+
]) {
|
|
230
|
+
const seen = new Set();
|
|
231
|
+
const stack = [...seeds];
|
|
232
|
+
while (stack.length > 0) {
|
|
233
|
+
const node = stack.pop();
|
|
234
|
+
if (seen.has(node) || !graph.hasNode(node)) {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
seen.add(node);
|
|
238
|
+
const was = graph.getNodeAttribute(node, 'direction');
|
|
239
|
+
graph.setNodeAttribute(node, 'direction', was === 'none' || was === mark ? mark : 'both');
|
|
240
|
+
graph.forEachOutEdge(node, (_e, attrs, _s, target) => {
|
|
241
|
+
if (DOWN.has(attrs.relation)) {
|
|
242
|
+
stack.push(target);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
function blank() {
|
|
250
|
+
return {
|
|
251
|
+
kind: 'type',
|
|
252
|
+
name: '',
|
|
253
|
+
parent: '',
|
|
254
|
+
doc: '',
|
|
255
|
+
direction: 'none',
|
|
256
|
+
methodType: 'n/a',
|
|
257
|
+
httpMethod: '',
|
|
258
|
+
path: '',
|
|
259
|
+
source: '',
|
|
260
|
+
signature: '',
|
|
261
|
+
required: false,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function edge(relation) {
|
|
265
|
+
return { relation, status: 0, in: '' };
|
|
266
|
+
}
|
|
267
|
+
/** Worth a node of its own: it has fields, or it is a choice between things. */
|
|
268
|
+
function worthNaming(schema) {
|
|
269
|
+
return (isObject(schema.properties) ||
|
|
270
|
+
Array.isArray(schema.allOf) ||
|
|
271
|
+
Array.isArray(schema.oneOf) ||
|
|
272
|
+
Array.isArray(schema.anyOf));
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* The properties a type actually has. `allOf` with an inline member is how
|
|
276
|
+
* most documents spell inheritance, and leaving those fields off the type that
|
|
277
|
+
* declares them would hide them from every search.
|
|
278
|
+
*/
|
|
279
|
+
function flatten(schema, types, seen = new Set()) {
|
|
280
|
+
const properties = {};
|
|
281
|
+
const required = new Set();
|
|
282
|
+
if (seen.has(schema)) {
|
|
283
|
+
return { properties, required };
|
|
284
|
+
}
|
|
285
|
+
seen.add(schema);
|
|
286
|
+
if (isObject(schema.properties)) {
|
|
287
|
+
Object.assign(properties, schema.properties);
|
|
288
|
+
}
|
|
289
|
+
if (Array.isArray(schema.required)) {
|
|
290
|
+
for (const key of schema.required) {
|
|
291
|
+
if (typeof key === 'string') {
|
|
292
|
+
required.add(key);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
for (const member of Array.isArray(schema.allOf) ? schema.allOf : []) {
|
|
297
|
+
// A named member is a node in its own right, reached by COMPOSES.
|
|
298
|
+
if (!isObject(member) || refName(member) !== undefined) {
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
const inner = flatten(member, types, seen);
|
|
302
|
+
Object.assign(properties, inner.properties);
|
|
303
|
+
for (const key of inner.required) {
|
|
304
|
+
required.add(key);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return { properties, required };
|
|
308
|
+
}
|
|
309
|
+
function docOf(schema) {
|
|
310
|
+
if (!isObject(schema)) {
|
|
311
|
+
return '';
|
|
312
|
+
}
|
|
313
|
+
const description = schema.description ?? schema.title;
|
|
314
|
+
return typeof description === 'string' ? description.trim() : '';
|
|
315
|
+
}
|
|
316
|
+
function capital(key) {
|
|
317
|
+
const cleaned = key.replace(/[^A-Za-z0-9]+/g, '_');
|
|
318
|
+
return cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
|
|
319
|
+
}
|
|
320
|
+
//# sourceMappingURL=graph.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type Schema } from './schema.ts';
|
|
2
|
+
import type { Operation } from './spec.ts';
|
|
3
|
+
import type { Subgraph } from './subgraph.ts';
|
|
4
|
+
export interface HydrateOptions {
|
|
5
|
+
docs?: boolean;
|
|
6
|
+
maxDoc?: number;
|
|
7
|
+
/** narrow each interface to the properties the search actually matched */
|
|
8
|
+
onlyHits?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function toTypeScript(sub: Subgraph, schemas: Readonly<Record<string, Schema>>, options?: HydrateOptions): string;
|
|
11
|
+
/**
|
|
12
|
+
* The other half of the job: when the task is to *call* the API rather than to
|
|
13
|
+
* type its payloads, a valid document beats a declaration file.
|
|
14
|
+
*/
|
|
15
|
+
export declare function toOpenApi(sub: Subgraph, schemas: Readonly<Record<string, Schema>>, operations: readonly Operation[], info?: {
|
|
16
|
+
title: string;
|
|
17
|
+
version: string;
|
|
18
|
+
}): Record<string, unknown>;
|
|
19
|
+
//# sourceMappingURL=hydrate.d.ts.map
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { refsIn } from "./schema.js";
|
|
2
|
+
import { Printer, property } from "./typescript.js";
|
|
3
|
+
export function toTypeScript(sub, schemas, options = {}) {
|
|
4
|
+
const printer = new Printer(schemas);
|
|
5
|
+
const wanted = closure(typeNames(sub), schemas);
|
|
6
|
+
const only = options.onlyHits ? hitProperties(sub) : undefined;
|
|
7
|
+
const blocks = [];
|
|
8
|
+
const operations = sub.nodes.filter((n) => n.kind === 'method');
|
|
9
|
+
if (operations.length > 0) {
|
|
10
|
+
blocks.push(operations.map((m) => routeComment(m)).join('\n'));
|
|
11
|
+
}
|
|
12
|
+
const printed = new Set([...wanted].map((name) => printer.identifier(name)));
|
|
13
|
+
for (const method of operations) {
|
|
14
|
+
const block = params(sub, method, printer, printed, options);
|
|
15
|
+
if (block) {
|
|
16
|
+
blocks.push(block);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
for (const name of [...wanted].sort()) {
|
|
20
|
+
blocks.push(printer.declaration(name, {
|
|
21
|
+
docs: options.docs,
|
|
22
|
+
maxDoc: options.maxDoc,
|
|
23
|
+
only: only?.get(name),
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
26
|
+
return blocks.filter(Boolean).join('\n');
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The other half of the job: when the task is to *call* the API rather than to
|
|
30
|
+
* type its payloads, a valid document beats a declaration file.
|
|
31
|
+
*/
|
|
32
|
+
export function toOpenApi(sub, schemas, operations, info = { title: 'Selection', version: '0' }) {
|
|
33
|
+
const names = new Set(sub.nodes.filter((n) => n.kind === 'method').map((n) => n.attributes.name));
|
|
34
|
+
const chosen = operations.filter((op) => names.has(op.operationId));
|
|
35
|
+
const wanted = closure(typeNames(sub), schemas);
|
|
36
|
+
const paths = {};
|
|
37
|
+
for (const op of chosen) {
|
|
38
|
+
for (const name of [
|
|
39
|
+
...op.params.flatMap((p) => refsIn(p.schema)),
|
|
40
|
+
...refsIn(op.requestBody?.schema),
|
|
41
|
+
...op.responses.flatMap((r) => refsIn(r.schema)),
|
|
42
|
+
]) {
|
|
43
|
+
wanted.add(name);
|
|
44
|
+
}
|
|
45
|
+
(paths[op.path] ??= {})[op.method] = {
|
|
46
|
+
operationId: op.operationId,
|
|
47
|
+
...(op.summary ? { summary: op.summary } : {}),
|
|
48
|
+
...(op.description ? { description: op.description } : {}),
|
|
49
|
+
...(op.params.length > 0
|
|
50
|
+
? {
|
|
51
|
+
parameters: op.params.map((p) => ({
|
|
52
|
+
name: p.name,
|
|
53
|
+
in: p.in,
|
|
54
|
+
required: p.required,
|
|
55
|
+
...(p.doc ? { description: p.doc } : {}),
|
|
56
|
+
schema: p.schema,
|
|
57
|
+
})),
|
|
58
|
+
}
|
|
59
|
+
: {}),
|
|
60
|
+
...(op.requestBody
|
|
61
|
+
? {
|
|
62
|
+
requestBody: {
|
|
63
|
+
required: op.requestBody.required,
|
|
64
|
+
content: { 'application/json': { schema: op.requestBody.schema } },
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
: {}),
|
|
68
|
+
responses: Object.fromEntries(op.responses.map((r) => [
|
|
69
|
+
String(r.status),
|
|
70
|
+
{
|
|
71
|
+
description: r.doc || 'Success',
|
|
72
|
+
content: { 'application/json': { schema: r.schema } },
|
|
73
|
+
},
|
|
74
|
+
])),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
// Everything was rewritten to `#/$defs/<id>` at load; a standalone document
|
|
78
|
+
// has to point at where the schemas are about to be written instead.
|
|
79
|
+
const components = Object.fromEntries([...closure(wanted, schemas)].sort().map((name) => [name, rebase(schemas[name])]));
|
|
80
|
+
return {
|
|
81
|
+
openapi: '3.1.0',
|
|
82
|
+
info,
|
|
83
|
+
paths: rebase(paths),
|
|
84
|
+
components: { schemas: components },
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
function typeNames(sub) {
|
|
89
|
+
return new Set(sub.nodes.filter((n) => n.kind === 'type').map((n) => n.attributes.name));
|
|
90
|
+
}
|
|
91
|
+
/** Every name reachable from the selection, so nothing printed dangles. */
|
|
92
|
+
function closure(names, schemas) {
|
|
93
|
+
const out = new Set();
|
|
94
|
+
const stack = [...names];
|
|
95
|
+
while (stack.length > 0) {
|
|
96
|
+
const name = stack.pop();
|
|
97
|
+
if (out.has(name) || !schemas[name]) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
out.add(name);
|
|
101
|
+
stack.push(...refsIn(schemas[name]));
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
function hitProperties(sub) {
|
|
106
|
+
const out = new Map();
|
|
107
|
+
for (const node of sub.nodes) {
|
|
108
|
+
if (node.kind !== 'property' || !node.hit || !node.attributes.parent) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const set = out.get(node.attributes.parent) ?? new Set();
|
|
112
|
+
set.add(node.attributes.name);
|
|
113
|
+
out.set(node.attributes.parent, set);
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
function routeComment(method) {
|
|
118
|
+
const a = method.attributes;
|
|
119
|
+
return `// ${a.httpMethod} ${a.path}${a.doc ? ` — ${a.doc.replace(/\s+/g, ' ')}` : ''}`;
|
|
120
|
+
}
|
|
121
|
+
/** The parameters of one operation, as something a caller can hold. */
|
|
122
|
+
function params(sub, method, printer, printed, options) {
|
|
123
|
+
const owned = sub.edges
|
|
124
|
+
.filter((e) => e.source === method.id && e.relation === 'HAS_PARAM')
|
|
125
|
+
.map((e) => ({ edge: e, node: sub.nodes.find((n) => n.id === e.target) }))
|
|
126
|
+
.filter((p) => !!p.node);
|
|
127
|
+
if (owned.length === 0) {
|
|
128
|
+
return '';
|
|
129
|
+
}
|
|
130
|
+
const lines = [`export interface ${printer.identifier(`${method.attributes.name}Params`)} {`];
|
|
131
|
+
for (const { edge, node } of owned) {
|
|
132
|
+
const a = node.attributes;
|
|
133
|
+
const note = [edge.in, a.doc].filter(Boolean).join(' — ');
|
|
134
|
+
if (options.docs && note) {
|
|
135
|
+
lines.push(` /** ${note.replace(/\s+/g, ' ')} */`);
|
|
136
|
+
}
|
|
137
|
+
lines.push(` ${property(a.name)}${a.required ? '' : '?'}: ${resolvable(a.signature, printed)};`);
|
|
138
|
+
}
|
|
139
|
+
lines.push('}\n');
|
|
140
|
+
return lines.join('\n');
|
|
141
|
+
}
|
|
142
|
+
const BUILTIN = new Set([
|
|
143
|
+
'string',
|
|
144
|
+
'number',
|
|
145
|
+
'boolean',
|
|
146
|
+
'null',
|
|
147
|
+
'unknown',
|
|
148
|
+
'never',
|
|
149
|
+
'Record',
|
|
150
|
+
'true',
|
|
151
|
+
'false',
|
|
152
|
+
]);
|
|
153
|
+
/**
|
|
154
|
+
* Signatures were computed against the whole corpus, so one may name a type
|
|
155
|
+
* this selection does not print. `unknown` is a worse answer than the name and
|
|
156
|
+
* a far better one than a file that will not compile.
|
|
157
|
+
*/
|
|
158
|
+
function resolvable(signature, printed) {
|
|
159
|
+
if (!signature) {
|
|
160
|
+
return 'unknown';
|
|
161
|
+
}
|
|
162
|
+
const names = signature.replace(/'[^']*'/g, '').match(/[A-Za-z_$][A-Za-z0-9_$]*/g) ?? [];
|
|
163
|
+
return names.every((name) => BUILTIN.has(name) || printed.has(name)) ? signature : 'unknown';
|
|
164
|
+
}
|
|
165
|
+
/** `#/$defs/X` back to `#/components/schemas/X`. */
|
|
166
|
+
function rebase(value) {
|
|
167
|
+
if (Array.isArray(value)) {
|
|
168
|
+
return value.map(rebase);
|
|
169
|
+
}
|
|
170
|
+
if (typeof value !== 'object' || value === null) {
|
|
171
|
+
return value;
|
|
172
|
+
}
|
|
173
|
+
const out = {};
|
|
174
|
+
for (const [key, inner] of Object.entries(value)) {
|
|
175
|
+
out[key] =
|
|
176
|
+
key === '$ref' && typeof inner === 'string'
|
|
177
|
+
? inner.replace(/^#\/\$defs\//, '#/components/schemas/')
|
|
178
|
+
: rebase(inner);
|
|
179
|
+
}
|
|
180
|
+
return out;
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=hydrate.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Subgraph } from './subgraph.ts';
|
|
2
|
+
export type RenderFormat = 'text' | 'mermaid' | 'mermaid-flowchart';
|
|
3
|
+
export interface RenderOptions {
|
|
4
|
+
docs?: boolean;
|
|
5
|
+
maxDoc?: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function render(sub: Subgraph, format: RenderFormat, options?: RenderOptions): string;
|
|
8
|
+
export declare function toText(sub: Subgraph, options?: RenderOptions): string;
|
|
9
|
+
export declare function toMermaid(sub: Subgraph, options?: RenderOptions): string;
|
|
10
|
+
export declare function toFlowchart(sub: Subgraph, options?: RenderOptions): string;
|
|
11
|
+
//# sourceMappingURL=render.d.ts.map
|