@m6d/cortex-server 1.1.0 → 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.
Files changed (42) hide show
  1. package/README.md +38 -38
  2. package/dist/src/factory.d.ts +13 -1
  3. package/dist/src/ws/index.d.ts +1 -1
  4. package/package.json +54 -54
  5. package/src/adapters/database.ts +21 -28
  6. package/src/adapters/minio.ts +69 -69
  7. package/src/adapters/mssql.ts +171 -195
  8. package/src/adapters/storage.ts +4 -4
  9. package/src/ai/fetch.ts +31 -31
  10. package/src/ai/helpers.ts +18 -22
  11. package/src/ai/index.ts +101 -113
  12. package/src/ai/interceptors/resolve-captured-files.ts +42 -49
  13. package/src/ai/prompt.ts +80 -83
  14. package/src/ai/tools/call-endpoint.tool.ts +75 -82
  15. package/src/ai/tools/capture-files.tool.ts +15 -17
  16. package/src/ai/tools/execute-code.tool.ts +73 -80
  17. package/src/ai/tools/query-graph.tool.ts +17 -17
  18. package/src/auth/middleware.ts +51 -51
  19. package/src/cli/extract-endpoints.ts +436 -474
  20. package/src/config.ts +124 -134
  21. package/src/db/migrate.ts +13 -13
  22. package/src/db/migrations/20260309012148_cloudy_maria_hill/snapshot.json +303 -303
  23. package/src/db/schema.ts +46 -58
  24. package/src/factory.ts +136 -139
  25. package/src/graph/generate-cypher.ts +97 -97
  26. package/src/graph/helpers.ts +37 -37
  27. package/src/graph/index.ts +20 -20
  28. package/src/graph/neo4j.ts +82 -89
  29. package/src/graph/resolver.ts +201 -211
  30. package/src/graph/seed.ts +101 -114
  31. package/src/graph/types.ts +88 -88
  32. package/src/graph/validate.ts +55 -57
  33. package/src/index.ts +5 -5
  34. package/src/routes/chat.ts +23 -23
  35. package/src/routes/files.ts +75 -80
  36. package/src/routes/threads.ts +52 -54
  37. package/src/routes/ws.ts +22 -22
  38. package/src/types.ts +30 -30
  39. package/src/ws/connections.ts +11 -11
  40. package/src/ws/events.ts +2 -2
  41. package/src/ws/index.ts +1 -5
  42. 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
- name: string;
9
- required: boolean;
10
- type: string;
11
- isArray?: boolean;
12
- properties?: Prop[];
8
+ name: string;
9
+ required: boolean;
10
+ type: string;
11
+ isArray?: boolean;
12
+ properties?: Prop[];
13
13
  };
14
14
  type Endpoint = {
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[];
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
- swaggerUrl: string;
28
- domainsDir: string;
29
- write?: boolean;
27
+ swaggerUrl: string;
28
+ domainsDir: string;
29
+ write?: boolean;
30
30
  };
31
31
 
32
32
  const METHODS: Array<[string, HttpMethod]> = [
33
- ["get", "GET"],
34
- ["post", "POST"],
35
- ["put", "PUT"],
36
- ["delete", "DELETE"],
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
- s
44
- .split(".")
45
- .map((seg) => seg.replace(/^[A-Z]/, (c) => c.toLowerCase()))
46
- .join(".");
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
- Array.isArray(v) ? v.filter((x): x is string => typeof x === "string") : [];
51
+ Array.isArray(v) ? v.filter((x): x is string => typeof x === "string") : [];
53
52
  const dedupe = (items: Prop[]): Prop[] => {
54
- const seen = new Set<string>();
55
- return items.filter((x) =>
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
- let p = raw.startsWith("/") ? raw : `/${raw}`;
62
- p =
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
- dir: string,
71
- accept: (filePath: string) => boolean,
72
- ): string[] {
73
- if (!fs.existsSync(dir)) return [];
74
- const out: string[] = [];
75
- function walk(d: string) {
76
- for (const e of fs.readdirSync(d, { withFileTypes: true })) {
77
- const p = path.join(d, e.name);
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
- walk(dir);
83
- return out;
73
+ walk(dir);
74
+ return out;
84
75
  }
85
76
 
86
77
  function endpointFiles(root: string): Map<string, string> {
87
- const out = new Map<string, string>();
88
- for (const filePath of walkFiles(root, (p) => p.endsWith(".endpoint.ts"))) {
89
- const c = fs.readFileSync(filePath, "utf8");
90
- const method = c.match(/^\s*method:\s*(['"])([^'"]+)\1/m)?.[2];
91
- const route = c.match(/^\s*path:\s*(['"])([^'"]+)\1/m)?.[2];
92
- if (method && route) out.set(`${method}:${normalizePath(route)}`, filePath);
93
- }
94
- return out;
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
- if (!ref.startsWith("#/")) return null;
99
- let cur: unknown = doc;
100
- for (const part of ref.slice(2).split("/")) {
101
- const key = part.replace(/~1/g, "/").replace(/~0/g, "~");
102
- if (!isObj(cur) || !(key in cur)) return null;
103
- cur = cur[key];
104
- }
105
- return cur;
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
- if (!isObj(v)) return {};
110
- let x = v;
111
- const seen = new Set<string>();
112
- while (typeof x.$ref === "string") {
113
- if (seen.has(x.$ref)) return {};
114
- seen.add(x.$ref);
115
- x = obj(resolvePointer(doc, x.$ref));
116
- }
117
- return x;
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
- const t = Array.isArray(s.type)
122
- ? s.type.find((x) => typeof x === "string" && x !== "null")
123
- : s.type;
124
- if (typeof t === "string") return t;
125
- if (isObj(s.properties)) return "object";
126
- if (isObj(s.items)) return "array";
127
- return "";
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
- isObj(v) &&
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
- const p = obj(s.properties);
137
- return (
138
- schemaType(s) === "object" &&
139
- "options" in p &&
140
- "parent" in p &&
141
- "root" in p &&
142
- isJsonNodeRef(p.parent) &&
143
- isJsonNodeRef(p.root)
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
- if (Array.isArray(s.enum) && s.enum.every((v) => typeof v === "string")) {
149
- return s.enum.map((v) => `'${String(v).replace(/'/g, "\\'")}'`).join(" | ");
150
- }
151
- const t = schemaType(s);
152
- const f = typeof s.format === "string" ? s.format : "";
153
- if (f === "uuid") return "uuid";
154
- if (f === "date") return "date";
155
- if (f === "date-time") return "datetime";
156
- if (t === "integer" || t === "number") return "number";
157
- if (t === "boolean") return "boolean";
158
- if (t === "string") return "string";
159
- if (t === "object") return "object";
160
- return "unknown";
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
- if (depth > MAX_DEPTH) return [];
165
- const s = deref(doc, schemaIn);
166
- if (isJsonNodeRef(schemaIn) || isJsonNodeSchema(s)) return [];
167
- const req = new Set(strings(s.required));
168
- return dedupe(
169
- Object.entries(obj(s.properties)).map(([name, child]) =>
170
- toProp(doc, name, child, req.has(name), depth + 1),
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
- doc: unknown,
177
- name: string,
178
- schemaIn: unknown,
179
- required: boolean,
180
- depth = 0,
181
- ): Prop {
182
- if (isJsonNodeRef(schemaIn)) return { name, required, type: "object" };
183
-
184
- const s = deref(doc, schemaIn);
185
- const req = required && s.nullable !== true;
186
-
187
- if (isJsonNodeSchema(s)) return { name, required: req, type: "object" };
188
-
189
- const t = schemaType(s);
190
- if (t === "array") {
191
- const item = deref(doc, s.items);
192
- if (schemaType(item) === "object") {
193
- const properties = toProps(doc, item, depth + 1);
194
- return {
195
- name,
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
- if (t === "object") {
206
- const properties = toProps(doc, s, depth + 1);
207
- return {
208
- name,
209
- required: req,
210
- type: "object",
211
- ...(properties.length ? { properties } : {}),
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
- return { name, required: req, type: scalarType(s) };
198
+ return { name, required: req, type: scalarType(s) };
216
199
  }
217
200
 
218
- function pickContent(
219
- contentIn: unknown,
220
- ): { schema: unknown; mimes: string[] } | null {
221
- const content = obj(contentIn);
222
- const mimes = Object.keys(content);
223
- if (!mimes.length) return null;
224
- const mime =
225
- mimes.find((x) => x.toLowerCase() === "application/json") ??
226
- mimes.find((x) => x.toLowerCase().includes("json")) ??
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
- doc: unknown,
233
- responsesIn: unknown,
213
+ doc: unknown,
214
+ responsesIn: unknown,
234
215
  ): {
235
- successStatus: number;
236
- errorStatuses: number[];
237
- responseKind: ResponseKind;
238
- response: Prop[];
216
+ successStatus: number;
217
+ errorStatuses: number[];
218
+ responseKind: ResponseKind;
219
+ response: Prop[];
239
220
  } {
240
- const responses = obj(responsesIn);
241
- const statuses = Object.keys(responses)
242
- .map(Number)
243
- .filter(Number.isInteger)
244
- .sort((a, b) => a - b);
245
- const successStatus = statuses.find((s) => s >= 200 && s < 300) ?? 200;
246
- const errorStatuses = [...new Set(statuses.filter((s) => s >= 400))];
247
-
248
- if (successStatus === 204)
249
- return {
250
- successStatus,
251
- errorStatuses,
252
- responseKind: "none",
253
- response: [],
254
- };
255
-
256
- const success = deref(doc, responses[String(successStatus)]);
257
- const content = pickContent(success.content);
258
- if (!content)
259
- return {
260
- successStatus,
261
- errorStatuses,
262
- responseKind: "none",
263
- response: [],
264
- };
265
-
266
- const s = deref(doc, content.schema);
267
- const t = schemaType(s);
268
- const isFile =
269
- content.mimes.some((m) =>
270
- /(application\/octet-stream|application\/pdf|application\/vnd\.|text\/csv)/i.test(
271
- m,
272
- ),
273
- ) ||
274
- (t === "string" && ["binary", "base64"].includes(String(s.format ?? "")));
275
-
276
- if (isFile)
277
- return {
278
- successStatus,
279
- errorStatuses,
280
- responseKind: "file",
281
- response: [],
282
- };
283
-
284
- if (t === "array") {
285
- const item = deref(doc, s.items);
286
- return {
287
- successStatus,
288
- errorStatuses,
289
- responseKind: "array",
290
- response:
291
- schemaType(item) === "object"
292
- ? toProps(doc, item)
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
- return {
321
- successStatus,
322
- errorStatuses,
323
- responseKind: "object",
324
- response: toProps(doc, s),
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
- if (Object.keys(s).length) {
329
- return {
330
- successStatus,
331
- errorStatuses,
332
- responseKind: "object",
333
- response: [toProp(doc, "value", s, true)],
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
- return { successStatus, errorStatuses, responseKind: "none", response: [] };
313
+ return { successStatus, errorStatuses, responseKind: "none", response: [] };
338
314
  }
339
315
 
340
316
  function extractEndpoint(
341
- doc: unknown,
342
- route: string,
343
- method: HttpMethod,
344
- pathItemIn: unknown,
345
- operationIn: unknown,
317
+ doc: unknown,
318
+ route: string,
319
+ method: HttpMethod,
320
+ pathItemIn: unknown,
321
+ operationIn: unknown,
346
322
  ): Endpoint {
347
- const pathItem = obj(pathItemIn);
348
- const operation = obj(operationIn);
349
-
350
- const rawParams = [
351
- ...(Array.isArray(pathItem.parameters) ? pathItem.parameters : []),
352
- ...(Array.isArray(operation.parameters) ? operation.parameters : []),
353
- ]
354
- .map((x) => deref(doc, x))
355
- .filter(
356
- (x) =>
357
- ["path", "query"].includes(String(x.in ?? "")) &&
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
- // Collapse JsonNode-expanded dot-notation query params into a single
362
- // object param. Swagger expands JsonNode properties (Options, Parent,
363
- // Root, …) into many individual params like "CustomFilter.Parent.Root".
364
- const jsonNodeRoots = new Set<string>();
365
- for (const x of rawParams) {
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(".")) return true;
341
+ if (!name.includes(".")) continue;
378
342
  const root = name.split(".")[0]!;
379
- return !jsonNodeRoots.has(root);
380
- })
381
- .map((x) =>
382
- toProp(
383
- doc,
384
- toCamelCase(String(x.name)),
385
- x.schema,
386
- String(x.in) === "path" || x.required === true,
387
- ),
388
- )
389
- .concat(
390
- [...jsonNodeRoots].map((root) => ({
391
- name: toCamelCase(root),
392
- required: false,
393
- type: "object",
394
- })),
395
- ),
396
- );
397
-
398
- const body: Prop[] = [];
399
- const requestBody = deref(doc, operation.requestBody);
400
- const requestContent = pickContent(requestBody.content);
401
- if (requestContent) {
402
- const s = deref(doc, requestContent.schema);
403
- const t = schemaType(s);
404
- if (t === "object") body.push(...toProps(doc, s));
405
- else if (t === "array")
406
- body.push(toProp(doc, "items", s, requestBody.required === true));
407
- else body.push(toProp(doc, "body", s, requestBody.required === true));
408
- }
409
-
410
- const parsed = parseResponse(doc, operation.responses);
411
- const normalizedPath = normalizePath(route);
412
-
413
- return {
414
- key: `${method}:${normalizedPath}`,
415
- method,
416
- path: normalizedPath,
417
- params,
418
- body: dedupe(body),
419
- response: dedupe(parsed.response),
420
- responseKind: parsed.responseKind,
421
- successStatus: parsed.successStatus,
422
- errorStatuses: parsed.errorStatuses,
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
- const out = new Map<string, Endpoint>();
428
- const doc = obj(swagger);
429
- for (const [route, pathItem] of Object.entries(obj(doc.paths))) {
430
- for (const [openApiMethod, method] of METHODS) {
431
- const operation = obj(pathItem)[openApiMethod];
432
- if (!isObj(operation)) continue;
433
- const endpoint = extractEndpoint(doc, route, method, pathItem, operation);
434
- out.set(endpoint.key, endpoint);
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
- return props
442
- .map((p) => {
443
- const indent = " ".repeat(depth);
444
- const childIndent = " ".repeat(depth + 1);
445
- const typeLiteral = p.type.includes("'")
446
- ? `"${p.type.replace(/"/g, '\\"')}"`
447
- : `'${p.type}'`;
448
-
449
- const parts = [
450
- `name: '${p.name}'`,
451
- `required: ${p.required}`,
452
- `type: ${typeLiteral}`,
453
- ];
454
- if (p.isArray) parts.push("isArray: true");
455
- if (p.properties?.length)
456
- parts.push(
457
- `properties: [\n${serializeProps(p.properties, depth + 1)}\n${childIndent}]`,
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
- return ` autoGenerated: {
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
- if (!filePaths.length) return;
484
- try {
485
- Bun.spawnSync(["bunx", "prettier", "--write", ...filePaths], {
486
- cwd,
487
- stdio: ["ignore", "ignore", "ignore"],
488
- });
489
- } catch (error) {
490
- const message = error instanceof Error ? error.message : String(error);
491
- console.warn(`WARN failed to format generated files: ${message}`);
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
- const response = await fetch(url, {
497
- headers: { Accept: "application/json" },
498
- });
499
- if (!response.ok) {
500
- throw new Error(`Swagger request failed (${response.status})`);
501
- }
502
- return response.json();
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
- key: string,
507
- extracted: Map<string, Endpoint>,
508
- ): Endpoint | undefined {
509
- const [method, route] = key.split(":");
510
- if (!method || !route) return undefined;
511
- return (
512
- extracted.get(key) ??
513
- extracted.get(`${method}:${normalizePath(`/api${route}`)}`) ??
514
- (route.startsWith("/api/")
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
- options: ExtractEndpointsOptions,
522
- ): Promise<void> {
523
- const { swaggerUrl, domainsDir, write = false } = options;
524
-
525
- console.log(`Fetching Swagger from ${swaggerUrl}`);
526
-
527
- const extracted = parseSwaggerEndpoints(await fetchJson(swaggerUrl));
528
- const files = endpointFiles(domainsDir);
529
-
530
- console.log(`Parsed ${extracted.size} endpoints from Swagger`);
531
- console.log(`Found ${files.size} graph endpoint files`);
532
-
533
- let matched = 0;
534
- let missing = 0;
535
- let updated = 0;
536
- const updatedFiles: string[] = [];
537
-
538
- for (const [key, filePath] of files.entries()) {
539
- const endpoint = resolveEndpoint(key, extracted);
540
- if (!endpoint) {
541
- missing++;
542
- console.log(
543
- `MISSING backend endpoint for ${path.relative(domainsDir, filePath)} (${key})`,
544
- );
545
- continue;
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
- matched++;
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
- console.log("\n--- Summary ---");
583
- console.log(`Matched: ${matched}/${files.size}`);
584
- console.log(`Missing: ${missing}`);
585
- console.log(
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
  }