@m6d/cortex-server 1.1.1 → 1.1.2
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 +38 -38
- package/dist/src/factory.d.ts +13 -1
- package/dist/src/ws/index.d.ts +1 -1
- package/package.json +54 -54
- package/src/adapters/database.ts +21 -28
- package/src/adapters/minio.ts +69 -69
- package/src/adapters/mssql.ts +171 -195
- package/src/adapters/storage.ts +4 -4
- package/src/ai/fetch.ts +31 -31
- package/src/ai/helpers.ts +18 -22
- package/src/ai/index.ts +101 -113
- package/src/ai/interceptors/resolve-captured-files.ts +42 -49
- package/src/ai/prompt.ts +80 -83
- package/src/ai/tools/call-endpoint.tool.ts +75 -82
- package/src/ai/tools/capture-files.tool.ts +15 -17
- package/src/ai/tools/execute-code.tool.ts +73 -80
- package/src/ai/tools/query-graph.tool.ts +17 -17
- package/src/auth/middleware.ts +51 -51
- package/src/cli/extract-endpoints.ts +436 -474
- package/src/config.ts +124 -134
- package/src/db/migrate.ts +13 -13
- package/src/db/migrations/20260309012148_cloudy_maria_hill/snapshot.json +303 -303
- package/src/db/schema.ts +46 -58
- package/src/factory.ts +136 -139
- package/src/graph/generate-cypher.ts +97 -97
- package/src/graph/helpers.ts +37 -37
- package/src/graph/index.ts +20 -20
- package/src/graph/neo4j.ts +82 -89
- package/src/graph/resolver.ts +201 -211
- package/src/graph/seed.ts +101 -114
- package/src/graph/types.ts +88 -88
- package/src/graph/validate.ts +55 -57
- package/src/index.ts +5 -5
- package/src/routes/chat.ts +23 -23
- package/src/routes/files.ts +75 -80
- package/src/routes/threads.ts +52 -54
- package/src/routes/ws.ts +22 -22
- package/src/types.ts +30 -30
- package/src/ws/connections.ts +11 -11
- package/src/ws/events.ts +2 -2
- package/src/ws/index.ts +1 -5
- package/src/ws/notify.ts +4 -4
|
@@ -5,465 +5,434 @@ type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
|
|
|
5
5
|
type ResponseKind = "object" | "array" | "paginated" | "file" | "none";
|
|
6
6
|
type Obj = Record<string, unknown>;
|
|
7
7
|
type Prop = {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
name: string;
|
|
9
|
+
required: boolean;
|
|
10
|
+
type: string;
|
|
11
|
+
isArray?: boolean;
|
|
12
|
+
properties?: Prop[];
|
|
13
13
|
};
|
|
14
14
|
type Endpoint = {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
15
|
+
key: string;
|
|
16
|
+
method: HttpMethod;
|
|
17
|
+
path: string;
|
|
18
|
+
params: Prop[];
|
|
19
|
+
body: Prop[];
|
|
20
|
+
response: Prop[];
|
|
21
|
+
responseKind: ResponseKind;
|
|
22
|
+
successStatus: number;
|
|
23
|
+
errorStatuses: number[];
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
export type ExtractEndpointsOptions = {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
swaggerUrl: string;
|
|
28
|
+
domainsDir: string;
|
|
29
|
+
write?: boolean;
|
|
30
30
|
};
|
|
31
31
|
|
|
32
32
|
const METHODS: Array<[string, HttpMethod]> = [
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
["get", "GET"],
|
|
34
|
+
["post", "POST"],
|
|
35
|
+
["put", "PUT"],
|
|
36
|
+
["delete", "DELETE"],
|
|
37
37
|
];
|
|
38
38
|
const AUTO_START = "// @auto-generated-start";
|
|
39
39
|
const AUTO_END = "// @auto-generated-end";
|
|
40
40
|
const MAX_DEPTH = 8;
|
|
41
41
|
|
|
42
42
|
const toCamelCase = (s: string): string =>
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
s
|
|
44
|
+
.split(".")
|
|
45
|
+
.map((seg) => seg.replace(/^[A-Z]/, (c) => c.toLowerCase()))
|
|
46
|
+
.join(".");
|
|
47
47
|
|
|
48
|
-
const isObj = (v: unknown): v is Obj =>
|
|
49
|
-
typeof v === "object" && v !== null && !Array.isArray(v);
|
|
48
|
+
const isObj = (v: unknown): v is Obj => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
50
49
|
const obj = (v: unknown): Obj => (isObj(v) ? v : {});
|
|
51
50
|
const strings = (v: unknown): string[] =>
|
|
52
|
-
|
|
51
|
+
Array.isArray(v) ? v.filter((x): x is string => typeof x === "string") : [];
|
|
53
52
|
const dedupe = (items: Prop[]): Prop[] => {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
seen.has(x.name) ? false : (seen.add(x.name), true),
|
|
57
|
-
);
|
|
53
|
+
const seen = new Set<string>();
|
|
54
|
+
return items.filter((x) => (seen.has(x.name) ? false : (seen.add(x.name), true)));
|
|
58
55
|
};
|
|
59
56
|
|
|
60
57
|
function normalizePath(raw: string): string {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
p
|
|
64
|
-
.replace(/\{([^}:?]+)(?::[^}]+)?\??\}/g, "{$1}")
|
|
65
|
-
.replace(/^\/api(?=\/|$)/i, "") || "/";
|
|
66
|
-
return p.length > 1 ? p.replace(/\/+$/, "") : p;
|
|
58
|
+
let p = raw.startsWith("/") ? raw : `/${raw}`;
|
|
59
|
+
p = p.replace(/\{([^}:?]+)(?::[^}]+)?\??\}/g, "{$1}").replace(/^\/api(?=\/|$)/i, "") || "/";
|
|
60
|
+
return p.length > 1 ? p.replace(/\/+$/, "") : p;
|
|
67
61
|
}
|
|
68
62
|
|
|
69
|
-
function walkFiles(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (e.isDirectory()) walk(p);
|
|
79
|
-
else if (accept(p)) out.push(p);
|
|
63
|
+
function walkFiles(dir: string, accept: (filePath: string) => boolean): string[] {
|
|
64
|
+
if (!fs.existsSync(dir)) return [];
|
|
65
|
+
const out: string[] = [];
|
|
66
|
+
function walk(d: string) {
|
|
67
|
+
for (const e of fs.readdirSync(d, { withFileTypes: true })) {
|
|
68
|
+
const p = path.join(d, e.name);
|
|
69
|
+
if (e.isDirectory()) walk(p);
|
|
70
|
+
else if (accept(p)) out.push(p);
|
|
71
|
+
}
|
|
80
72
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return out;
|
|
73
|
+
walk(dir);
|
|
74
|
+
return out;
|
|
84
75
|
}
|
|
85
76
|
|
|
86
77
|
function endpointFiles(root: string): Map<string, string> {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
78
|
+
const out = new Map<string, string>();
|
|
79
|
+
for (const filePath of walkFiles(root, (p) => p.endsWith(".endpoint.ts"))) {
|
|
80
|
+
const c = fs.readFileSync(filePath, "utf8");
|
|
81
|
+
const method = c.match(/^\s*method:\s*(['"])([^'"]+)\1/m)?.[2];
|
|
82
|
+
const route = c.match(/^\s*path:\s*(['"])([^'"]+)\1/m)?.[2];
|
|
83
|
+
if (method && route) out.set(`${method}:${normalizePath(route)}`, filePath);
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
95
86
|
}
|
|
96
87
|
|
|
97
88
|
function resolvePointer(doc: unknown, ref: string): unknown {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
89
|
+
if (!ref.startsWith("#/")) return null;
|
|
90
|
+
let cur: unknown = doc;
|
|
91
|
+
for (const part of ref.slice(2).split("/")) {
|
|
92
|
+
const key = part.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
93
|
+
if (!isObj(cur) || !(key in cur)) return null;
|
|
94
|
+
cur = cur[key];
|
|
95
|
+
}
|
|
96
|
+
return cur;
|
|
106
97
|
}
|
|
107
98
|
|
|
108
99
|
function deref(doc: unknown, v: unknown): Obj {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
100
|
+
if (!isObj(v)) return {};
|
|
101
|
+
let x = v;
|
|
102
|
+
const seen = new Set<string>();
|
|
103
|
+
while (typeof x.$ref === "string") {
|
|
104
|
+
if (seen.has(x.$ref)) return {};
|
|
105
|
+
seen.add(x.$ref);
|
|
106
|
+
x = obj(resolvePointer(doc, x.$ref));
|
|
107
|
+
}
|
|
108
|
+
return x;
|
|
118
109
|
}
|
|
119
110
|
|
|
120
111
|
function schemaType(s: Obj): string {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
112
|
+
const t = Array.isArray(s.type)
|
|
113
|
+
? s.type.find((x) => typeof x === "string" && x !== "null")
|
|
114
|
+
: s.type;
|
|
115
|
+
if (typeof t === "string") return t;
|
|
116
|
+
if (isObj(s.properties)) return "object";
|
|
117
|
+
if (isObj(s.items)) return "array";
|
|
118
|
+
return "";
|
|
128
119
|
}
|
|
129
120
|
|
|
130
121
|
const isJsonNodeRef = (v: unknown) =>
|
|
131
|
-
|
|
132
|
-
typeof v.$ref === "string" &&
|
|
133
|
-
/\/System\.Text\.Json\.Nodes\.JsonNode$/.test(v.$ref);
|
|
122
|
+
isObj(v) && typeof v.$ref === "string" && /\/System\.Text\.Json\.Nodes\.JsonNode$/.test(v.$ref);
|
|
134
123
|
|
|
135
124
|
function isJsonNodeSchema(s: Obj) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
125
|
+
const p = obj(s.properties);
|
|
126
|
+
return (
|
|
127
|
+
schemaType(s) === "object" &&
|
|
128
|
+
"options" in p &&
|
|
129
|
+
"parent" in p &&
|
|
130
|
+
"root" in p &&
|
|
131
|
+
isJsonNodeRef(p.parent) &&
|
|
132
|
+
isJsonNodeRef(p.root)
|
|
133
|
+
);
|
|
145
134
|
}
|
|
146
135
|
|
|
147
136
|
function scalarType(s: Obj): string {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
137
|
+
if (Array.isArray(s.enum) && s.enum.every((v) => typeof v === "string")) {
|
|
138
|
+
return s.enum.map((v) => `'${String(v).replace(/'/g, "\\'")}'`).join(" | ");
|
|
139
|
+
}
|
|
140
|
+
const t = schemaType(s);
|
|
141
|
+
const f = typeof s.format === "string" ? s.format : "";
|
|
142
|
+
if (f === "uuid") return "uuid";
|
|
143
|
+
if (f === "date") return "date";
|
|
144
|
+
if (f === "date-time") return "datetime";
|
|
145
|
+
if (t === "integer" || t === "number") return "number";
|
|
146
|
+
if (t === "boolean") return "boolean";
|
|
147
|
+
if (t === "string") return "string";
|
|
148
|
+
if (t === "object") return "object";
|
|
149
|
+
return "unknown";
|
|
161
150
|
}
|
|
162
151
|
|
|
163
152
|
function toProps(doc: unknown, schemaIn: unknown, depth = 0): Prop[] {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
153
|
+
if (depth > MAX_DEPTH) return [];
|
|
154
|
+
const s = deref(doc, schemaIn);
|
|
155
|
+
if (isJsonNodeRef(schemaIn) || isJsonNodeSchema(s)) return [];
|
|
156
|
+
const req = new Set(strings(s.required));
|
|
157
|
+
return dedupe(
|
|
158
|
+
Object.entries(obj(s.properties)).map(([name, child]) =>
|
|
159
|
+
toProp(doc, name, child, req.has(name), depth + 1),
|
|
160
|
+
),
|
|
161
|
+
);
|
|
173
162
|
}
|
|
174
163
|
|
|
175
|
-
function toProp(
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
):
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
required: req,
|
|
197
|
-
type: "object",
|
|
198
|
-
isArray: true,
|
|
199
|
-
...(properties.length ? { properties } : {}),
|
|
200
|
-
};
|
|
164
|
+
function toProp(doc: unknown, name: string, schemaIn: unknown, required: boolean, depth = 0): Prop {
|
|
165
|
+
if (isJsonNodeRef(schemaIn)) return { name, required, type: "object" };
|
|
166
|
+
|
|
167
|
+
const s = deref(doc, schemaIn);
|
|
168
|
+
const req = required && s.nullable !== true;
|
|
169
|
+
|
|
170
|
+
if (isJsonNodeSchema(s)) return { name, required: req, type: "object" };
|
|
171
|
+
|
|
172
|
+
const t = schemaType(s);
|
|
173
|
+
if (t === "array") {
|
|
174
|
+
const item = deref(doc, s.items);
|
|
175
|
+
if (schemaType(item) === "object") {
|
|
176
|
+
const properties = toProps(doc, item, depth + 1);
|
|
177
|
+
return {
|
|
178
|
+
name,
|
|
179
|
+
required: req,
|
|
180
|
+
type: "object",
|
|
181
|
+
isArray: true,
|
|
182
|
+
...(properties.length ? { properties } : {}),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
return { name, required: req, type: scalarType(item), isArray: true };
|
|
201
186
|
}
|
|
202
|
-
return { name, required: req, type: scalarType(item), isArray: true };
|
|
203
|
-
}
|
|
204
187
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
188
|
+
if (t === "object") {
|
|
189
|
+
const properties = toProps(doc, s, depth + 1);
|
|
190
|
+
return {
|
|
191
|
+
name,
|
|
192
|
+
required: req,
|
|
193
|
+
type: "object",
|
|
194
|
+
...(properties.length ? { properties } : {}),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
214
197
|
|
|
215
|
-
|
|
198
|
+
return { name, required: req, type: scalarType(s) };
|
|
216
199
|
}
|
|
217
200
|
|
|
218
|
-
function pickContent(
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
mimes[0];
|
|
228
|
-
return { schema: obj(content[mime!]).schema, mimes };
|
|
201
|
+
function pickContent(contentIn: unknown): { schema: unknown; mimes: string[] } | null {
|
|
202
|
+
const content = obj(contentIn);
|
|
203
|
+
const mimes = Object.keys(content);
|
|
204
|
+
if (!mimes.length) return null;
|
|
205
|
+
const mime =
|
|
206
|
+
mimes.find((x) => x.toLowerCase() === "application/json") ??
|
|
207
|
+
mimes.find((x) => x.toLowerCase().includes("json")) ??
|
|
208
|
+
mimes[0];
|
|
209
|
+
return { schema: obj(content[mime!]).schema, mimes };
|
|
229
210
|
}
|
|
230
211
|
|
|
231
212
|
function parseResponse(
|
|
232
|
-
|
|
233
|
-
|
|
213
|
+
doc: unknown,
|
|
214
|
+
responsesIn: unknown,
|
|
234
215
|
): {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
216
|
+
successStatus: number;
|
|
217
|
+
errorStatuses: number[];
|
|
218
|
+
responseKind: ResponseKind;
|
|
219
|
+
response: Prop[];
|
|
239
220
|
} {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
: [toProp(doc, "item", item, true)],
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
if (t === "object") {
|
|
298
|
-
const props = obj(s.properties);
|
|
299
|
-
const hasPaging = [
|
|
300
|
-
"totalCount",
|
|
301
|
-
"pageNumber",
|
|
302
|
-
"pageSize",
|
|
303
|
-
"totalPages",
|
|
304
|
-
].some((k) => k in props);
|
|
305
|
-
const list = deref(doc, props.items ?? props.data ?? props.results);
|
|
306
|
-
|
|
307
|
-
if (hasPaging && schemaType(list) === "array") {
|
|
308
|
-
const item = deref(doc, list.items);
|
|
309
|
-
return {
|
|
310
|
-
successStatus,
|
|
311
|
-
errorStatuses,
|
|
312
|
-
responseKind: "paginated",
|
|
313
|
-
response:
|
|
314
|
-
schemaType(item) === "object"
|
|
315
|
-
? toProps(doc, item)
|
|
316
|
-
: [toProp(doc, "item", item, true)],
|
|
317
|
-
};
|
|
221
|
+
const responses = obj(responsesIn);
|
|
222
|
+
const statuses = Object.keys(responses)
|
|
223
|
+
.map(Number)
|
|
224
|
+
.filter(Number.isInteger)
|
|
225
|
+
.sort((a, b) => a - b);
|
|
226
|
+
const successStatus = statuses.find((s) => s >= 200 && s < 300) ?? 200;
|
|
227
|
+
const errorStatuses = [...new Set(statuses.filter((s) => s >= 400))];
|
|
228
|
+
|
|
229
|
+
if (successStatus === 204)
|
|
230
|
+
return {
|
|
231
|
+
successStatus,
|
|
232
|
+
errorStatuses,
|
|
233
|
+
responseKind: "none",
|
|
234
|
+
response: [],
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const success = deref(doc, responses[String(successStatus)]);
|
|
238
|
+
const content = pickContent(success.content);
|
|
239
|
+
if (!content)
|
|
240
|
+
return {
|
|
241
|
+
successStatus,
|
|
242
|
+
errorStatuses,
|
|
243
|
+
responseKind: "none",
|
|
244
|
+
response: [],
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const s = deref(doc, content.schema);
|
|
248
|
+
const t = schemaType(s);
|
|
249
|
+
const isFile =
|
|
250
|
+
content.mimes.some((m) =>
|
|
251
|
+
/(application\/octet-stream|application\/pdf|application\/vnd\.|text\/csv)/i.test(m),
|
|
252
|
+
) ||
|
|
253
|
+
(t === "string" && ["binary", "base64"].includes(String(s.format ?? "")));
|
|
254
|
+
|
|
255
|
+
if (isFile)
|
|
256
|
+
return {
|
|
257
|
+
successStatus,
|
|
258
|
+
errorStatuses,
|
|
259
|
+
responseKind: "file",
|
|
260
|
+
response: [],
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
if (t === "array") {
|
|
264
|
+
const item = deref(doc, s.items);
|
|
265
|
+
return {
|
|
266
|
+
successStatus,
|
|
267
|
+
errorStatuses,
|
|
268
|
+
responseKind: "array",
|
|
269
|
+
response:
|
|
270
|
+
schemaType(item) === "object"
|
|
271
|
+
? toProps(doc, item)
|
|
272
|
+
: [toProp(doc, "item", item, true)],
|
|
273
|
+
};
|
|
318
274
|
}
|
|
319
275
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
276
|
+
if (t === "object") {
|
|
277
|
+
const props = obj(s.properties);
|
|
278
|
+
const hasPaging = ["totalCount", "pageNumber", "pageSize", "totalPages"].some(
|
|
279
|
+
(k) => k in props,
|
|
280
|
+
);
|
|
281
|
+
const list = deref(doc, props.items ?? props.data ?? props.results);
|
|
282
|
+
|
|
283
|
+
if (hasPaging && schemaType(list) === "array") {
|
|
284
|
+
const item = deref(doc, list.items);
|
|
285
|
+
return {
|
|
286
|
+
successStatus,
|
|
287
|
+
errorStatuses,
|
|
288
|
+
responseKind: "paginated",
|
|
289
|
+
response:
|
|
290
|
+
schemaType(item) === "object"
|
|
291
|
+
? toProps(doc, item)
|
|
292
|
+
: [toProp(doc, "item", item, true)],
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
successStatus,
|
|
298
|
+
errorStatuses,
|
|
299
|
+
responseKind: "object",
|
|
300
|
+
response: toProps(doc, s),
|
|
301
|
+
};
|
|
302
|
+
}
|
|
327
303
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
304
|
+
if (Object.keys(s).length) {
|
|
305
|
+
return {
|
|
306
|
+
successStatus,
|
|
307
|
+
errorStatuses,
|
|
308
|
+
responseKind: "object",
|
|
309
|
+
response: [toProp(doc, "value", s, true)],
|
|
310
|
+
};
|
|
311
|
+
}
|
|
336
312
|
|
|
337
|
-
|
|
313
|
+
return { successStatus, errorStatuses, responseKind: "none", response: [] };
|
|
338
314
|
}
|
|
339
315
|
|
|
340
316
|
function extractEndpoint(
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
317
|
+
doc: unknown,
|
|
318
|
+
route: string,
|
|
319
|
+
method: HttpMethod,
|
|
320
|
+
pathItemIn: unknown,
|
|
321
|
+
operationIn: unknown,
|
|
346
322
|
): Endpoint {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
typeof x.name === "string",
|
|
359
|
-
);
|
|
323
|
+
const pathItem = obj(pathItemIn);
|
|
324
|
+
const operation = obj(operationIn);
|
|
325
|
+
|
|
326
|
+
const rawParams = [
|
|
327
|
+
...(Array.isArray(pathItem.parameters) ? pathItem.parameters : []),
|
|
328
|
+
...(Array.isArray(operation.parameters) ? operation.parameters : []),
|
|
329
|
+
]
|
|
330
|
+
.map((x) => deref(doc, x))
|
|
331
|
+
.filter(
|
|
332
|
+
(x) => ["path", "query"].includes(String(x.in ?? "")) && typeof x.name === "string",
|
|
333
|
+
);
|
|
360
334
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
const name = String(x.name);
|
|
367
|
-
if (!name.includes(".")) continue;
|
|
368
|
-
const root = name.split(".")[0]!;
|
|
369
|
-
const rest = name.slice(root.length + 1);
|
|
370
|
-
if (/^(Options|Parent|Root)\b/.test(rest)) jsonNodeRoots.add(root);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
const params = dedupe(
|
|
374
|
-
rawParams
|
|
375
|
-
.filter((x) => {
|
|
335
|
+
// Collapse JsonNode-expanded dot-notation query params into a single
|
|
336
|
+
// object param. Swagger expands JsonNode properties (Options, Parent,
|
|
337
|
+
// Root, …) into many individual params like "CustomFilter.Parent.Root".
|
|
338
|
+
const jsonNodeRoots = new Set<string>();
|
|
339
|
+
for (const x of rawParams) {
|
|
376
340
|
const name = String(x.name);
|
|
377
|
-
if (!name.includes("."))
|
|
341
|
+
if (!name.includes(".")) continue;
|
|
378
342
|
const root = name.split(".")[0]!;
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
343
|
+
const rest = name.slice(root.length + 1);
|
|
344
|
+
if (/^(Options|Parent|Root)\b/.test(rest)) jsonNodeRoots.add(root);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const params = dedupe(
|
|
348
|
+
rawParams
|
|
349
|
+
.filter((x) => {
|
|
350
|
+
const name = String(x.name);
|
|
351
|
+
if (!name.includes(".")) return true;
|
|
352
|
+
const root = name.split(".")[0]!;
|
|
353
|
+
return !jsonNodeRoots.has(root);
|
|
354
|
+
})
|
|
355
|
+
.map((x) =>
|
|
356
|
+
toProp(
|
|
357
|
+
doc,
|
|
358
|
+
toCamelCase(String(x.name)),
|
|
359
|
+
x.schema,
|
|
360
|
+
String(x.in) === "path" || x.required === true,
|
|
361
|
+
),
|
|
362
|
+
)
|
|
363
|
+
.concat(
|
|
364
|
+
[...jsonNodeRoots].map((root) => ({
|
|
365
|
+
name: toCamelCase(root),
|
|
366
|
+
required: false,
|
|
367
|
+
type: "object",
|
|
368
|
+
})),
|
|
369
|
+
),
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
const body: Prop[] = [];
|
|
373
|
+
const requestBody = deref(doc, operation.requestBody);
|
|
374
|
+
const requestContent = pickContent(requestBody.content);
|
|
375
|
+
if (requestContent) {
|
|
376
|
+
const s = deref(doc, requestContent.schema);
|
|
377
|
+
const t = schemaType(s);
|
|
378
|
+
if (t === "object") body.push(...toProps(doc, s));
|
|
379
|
+
else if (t === "array") body.push(toProp(doc, "items", s, requestBody.required === true));
|
|
380
|
+
else body.push(toProp(doc, "body", s, requestBody.required === true));
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const parsed = parseResponse(doc, operation.responses);
|
|
384
|
+
const normalizedPath = normalizePath(route);
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
key: `${method}:${normalizedPath}`,
|
|
388
|
+
method,
|
|
389
|
+
path: normalizedPath,
|
|
390
|
+
params,
|
|
391
|
+
body: dedupe(body),
|
|
392
|
+
response: dedupe(parsed.response),
|
|
393
|
+
responseKind: parsed.responseKind,
|
|
394
|
+
successStatus: parsed.successStatus,
|
|
395
|
+
errorStatuses: parsed.errorStatuses,
|
|
396
|
+
};
|
|
424
397
|
}
|
|
425
398
|
|
|
426
399
|
function parseSwaggerEndpoints(swagger: unknown): Map<string, Endpoint> {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
400
|
+
const out = new Map<string, Endpoint>();
|
|
401
|
+
const doc = obj(swagger);
|
|
402
|
+
for (const [route, pathItem] of Object.entries(obj(doc.paths))) {
|
|
403
|
+
for (const [openApiMethod, method] of METHODS) {
|
|
404
|
+
const operation = obj(pathItem)[openApiMethod];
|
|
405
|
+
if (!isObj(operation)) continue;
|
|
406
|
+
const endpoint = extractEndpoint(doc, route, method, pathItem, operation);
|
|
407
|
+
out.set(endpoint.key, endpoint);
|
|
408
|
+
}
|
|
435
409
|
}
|
|
436
|
-
|
|
437
|
-
return out;
|
|
410
|
+
return out;
|
|
438
411
|
}
|
|
439
412
|
|
|
440
413
|
function serializeProps(props: Prop[], depth: number): string {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
);
|
|
459
|
-
|
|
460
|
-
return `${indent} { ${parts.join(", ")} },`;
|
|
461
|
-
})
|
|
462
|
-
.join("\n");
|
|
414
|
+
return props
|
|
415
|
+
.map((p) => {
|
|
416
|
+
const indent = " ".repeat(depth);
|
|
417
|
+
const childIndent = " ".repeat(depth + 1);
|
|
418
|
+
const typeLiteral = p.type.includes("'")
|
|
419
|
+
? `"${p.type.replace(/"/g, '\\"')}"`
|
|
420
|
+
: `'${p.type}'`;
|
|
421
|
+
|
|
422
|
+
const parts = [`name: '${p.name}'`, `required: ${p.required}`, `type: ${typeLiteral}`];
|
|
423
|
+
if (p.isArray) parts.push("isArray: true");
|
|
424
|
+
if (p.properties?.length)
|
|
425
|
+
parts.push(
|
|
426
|
+
`properties: [\n${serializeProps(p.properties, depth + 1)}\n${childIndent}]`,
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
return `${indent} { ${parts.join(", ")} },`;
|
|
430
|
+
})
|
|
431
|
+
.join("\n");
|
|
463
432
|
}
|
|
464
433
|
|
|
465
434
|
function blockFor(endpoint: Endpoint): string {
|
|
466
|
-
|
|
435
|
+
return ` autoGenerated: {
|
|
467
436
|
params: [
|
|
468
437
|
${serializeProps(endpoint.params, 2)}
|
|
469
438
|
] as const,
|
|
@@ -480,109 +449,102 @@ ${serializeProps(endpoint.response, 2)}
|
|
|
480
449
|
}
|
|
481
450
|
|
|
482
451
|
function formatGeneratedFiles(cwd: string, filePaths: string[]) {
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
452
|
+
if (!filePaths.length) return;
|
|
453
|
+
try {
|
|
454
|
+
Bun.spawnSync(["bunx", "prettier", "--write", ...filePaths], {
|
|
455
|
+
cwd,
|
|
456
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
457
|
+
});
|
|
458
|
+
} catch (error) {
|
|
459
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
460
|
+
console.warn(`WARN failed to format generated files: ${message}`);
|
|
461
|
+
}
|
|
493
462
|
}
|
|
494
463
|
|
|
495
464
|
async function fetchJson(url: string): Promise<unknown> {
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
465
|
+
const response = await fetch(url, {
|
|
466
|
+
headers: { Accept: "application/json" },
|
|
467
|
+
});
|
|
468
|
+
if (!response.ok) {
|
|
469
|
+
throw new Error(`Swagger request failed (${response.status})`);
|
|
470
|
+
}
|
|
471
|
+
return response.json();
|
|
503
472
|
}
|
|
504
473
|
|
|
505
|
-
function resolveEndpoint(
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
? extracted.get(`${method}:${normalizePath(route.slice(4))}`)
|
|
516
|
-
: undefined)
|
|
517
|
-
);
|
|
474
|
+
function resolveEndpoint(key: string, extracted: Map<string, Endpoint>): Endpoint | undefined {
|
|
475
|
+
const [method, route] = key.split(":");
|
|
476
|
+
if (!method || !route) return undefined;
|
|
477
|
+
return (
|
|
478
|
+
extracted.get(key) ??
|
|
479
|
+
extracted.get(`${method}:${normalizePath(`/api${route}`)}`) ??
|
|
480
|
+
(route.startsWith("/api/")
|
|
481
|
+
? extracted.get(`${method}:${normalizePath(route.slice(4))}`)
|
|
482
|
+
: undefined)
|
|
483
|
+
);
|
|
518
484
|
}
|
|
519
485
|
|
|
520
|
-
export async function extractEndpoints(
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
486
|
+
export async function extractEndpoints(options: ExtractEndpointsOptions): Promise<void> {
|
|
487
|
+
const { swaggerUrl, domainsDir, write = false } = options;
|
|
488
|
+
|
|
489
|
+
console.log(`Fetching Swagger from ${swaggerUrl}`);
|
|
490
|
+
|
|
491
|
+
const extracted = parseSwaggerEndpoints(await fetchJson(swaggerUrl));
|
|
492
|
+
const files = endpointFiles(domainsDir);
|
|
493
|
+
|
|
494
|
+
console.log(`Parsed ${extracted.size} endpoints from Swagger`);
|
|
495
|
+
console.log(`Found ${files.size} graph endpoint files`);
|
|
496
|
+
|
|
497
|
+
let matched = 0;
|
|
498
|
+
let missing = 0;
|
|
499
|
+
let updated = 0;
|
|
500
|
+
const updatedFiles: string[] = [];
|
|
501
|
+
|
|
502
|
+
for (const [key, filePath] of files.entries()) {
|
|
503
|
+
const endpoint = resolveEndpoint(key, extracted);
|
|
504
|
+
if (!endpoint) {
|
|
505
|
+
missing++;
|
|
506
|
+
console.log(
|
|
507
|
+
`MISSING backend endpoint for ${path.relative(domainsDir, filePath)} (${key})`,
|
|
508
|
+
);
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
matched++;
|
|
513
|
+
if (!write) continue;
|
|
514
|
+
|
|
515
|
+
const current = fs.readFileSync(filePath, "utf8");
|
|
516
|
+
const start = current.indexOf(AUTO_START);
|
|
517
|
+
const end = current.indexOf(AUTO_END);
|
|
518
|
+
|
|
519
|
+
let next: string;
|
|
520
|
+
if (start >= 0 && end > start) {
|
|
521
|
+
// Markers exist — replace content between them
|
|
522
|
+
next =
|
|
523
|
+
current.slice(0, start + AUTO_START.length) +
|
|
524
|
+
`\n${blockFor(endpoint)}\n ` +
|
|
525
|
+
current.slice(end);
|
|
526
|
+
} else {
|
|
527
|
+
// No markers — insert before the closing `});`
|
|
528
|
+
const closingIndex = current.lastIndexOf("});");
|
|
529
|
+
if (closingIndex < 0) continue;
|
|
530
|
+
next =
|
|
531
|
+
current.slice(0, closingIndex) +
|
|
532
|
+
`\n ${AUTO_START}\n${blockFor(endpoint)}\n ${AUTO_END}\n});` +
|
|
533
|
+
current.slice(closingIndex + 3);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (next === current) continue;
|
|
537
|
+
|
|
538
|
+
fs.writeFileSync(filePath, next);
|
|
539
|
+
updatedFiles.push(filePath);
|
|
540
|
+
updated++;
|
|
541
|
+
console.log(`UPDATED ${path.relative(domainsDir, filePath)}`);
|
|
546
542
|
}
|
|
547
543
|
|
|
548
|
-
|
|
549
|
-
if (!write) continue;
|
|
550
|
-
|
|
551
|
-
const current = fs.readFileSync(filePath, "utf8");
|
|
552
|
-
const start = current.indexOf(AUTO_START);
|
|
553
|
-
const end = current.indexOf(AUTO_END);
|
|
554
|
-
|
|
555
|
-
let next: string;
|
|
556
|
-
if (start >= 0 && end > start) {
|
|
557
|
-
// Markers exist — replace content between them
|
|
558
|
-
next =
|
|
559
|
-
current.slice(0, start + AUTO_START.length) +
|
|
560
|
-
`\n${blockFor(endpoint)}\n ` +
|
|
561
|
-
current.slice(end);
|
|
562
|
-
} else {
|
|
563
|
-
// No markers — insert before the closing `});`
|
|
564
|
-
const closingIndex = current.lastIndexOf("});");
|
|
565
|
-
if (closingIndex < 0) continue;
|
|
566
|
-
next =
|
|
567
|
-
current.slice(0, closingIndex) +
|
|
568
|
-
`\n ${AUTO_START}\n${blockFor(endpoint)}\n ${AUTO_END}\n});` +
|
|
569
|
-
current.slice(closingIndex + 3);
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
if (next === current) continue;
|
|
573
|
-
|
|
574
|
-
fs.writeFileSync(filePath, next);
|
|
575
|
-
updatedFiles.push(filePath);
|
|
576
|
-
updated++;
|
|
577
|
-
console.log(`UPDATED ${path.relative(domainsDir, filePath)}`);
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
if (write) formatGeneratedFiles(domainsDir, updatedFiles);
|
|
544
|
+
if (write) formatGeneratedFiles(domainsDir, updatedFiles);
|
|
581
545
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
write ? `Updated: ${updated}` : "Run with --write to update files",
|
|
587
|
-
);
|
|
546
|
+
console.log("\n--- Summary ---");
|
|
547
|
+
console.log(`Matched: ${matched}/${files.size}`);
|
|
548
|
+
console.log(`Missing: ${missing}`);
|
|
549
|
+
console.log(write ? `Updated: ${updated}` : "Run with --write to update files");
|
|
588
550
|
}
|