@rebasepro/studio 0.17.3 → 0.18.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 (81) hide show
  1. package/LICENSE +0 -1
  2. package/README.md +12 -4
  3. package/dist/{ApiKeysView-DiCurTEU.js → ApiKeysView-EIvegrUk.js} +54 -39
  4. package/dist/ApiKeysView-EIvegrUk.js.map +1 -0
  5. package/dist/{BackupsView-DNS6LdVg.js → BackupsView-DVkKzaPR.js} +29 -31
  6. package/dist/BackupsView-DVkKzaPR.js.map +1 -0
  7. package/dist/{BranchesView-CM3B-ER3.js → BranchesView-Df8hQ8wE.js} +85 -24
  8. package/dist/BranchesView-Df8hQ8wE.js.map +1 -0
  9. package/dist/{CronJobsView-BHtJZeJZ.js → CronJobsView-CLOGjb-L.js} +32 -27
  10. package/dist/CronJobsView-CLOGjb-L.js.map +1 -0
  11. package/dist/{RLSEditor-CBX4uD66.js → RLSEditor-Sa1D34ny.js} +724 -615
  12. package/dist/RLSEditor-Sa1D34ny.js.map +1 -0
  13. package/dist/{SQLEditor-ByecwX8B.js → SQLEditor-BfNA8xM3.js} +9 -4
  14. package/dist/{SQLEditor-ByecwX8B.js.map → SQLEditor-BfNA8xM3.js.map} +1 -1
  15. package/dist/{SchemaVisualizer-D6tMZgeC.js → SchemaVisualizer-DTDMYL6o.js} +84 -110
  16. package/dist/SchemaVisualizer-DTDMYL6o.js.map +1 -0
  17. package/dist/{StorageView-BGDeC5GI.js → StorageView-Ed9Q_VlB.js} +5 -32
  18. package/dist/StorageView-Ed9Q_VlB.js.map +1 -0
  19. package/dist/components/RLSEditor/saveRules.d.ts +38 -0
  20. package/dist/components/RebaseStudio.d.ts +1 -1
  21. package/dist/components/SchemaVisualizer/schema-visualizer.utils.d.ts +5 -2
  22. package/dist/components/SchemaVisualizer/useSchemaGraph.d.ts +0 -4
  23. package/dist/components/load-failure-view.d.ts +17 -0
  24. package/dist/components/load-failure.d.ts +47 -0
  25. package/dist/index.es.js +56 -36
  26. package/dist/index.es.js.map +1 -1
  27. package/dist/load-failure-C6OMJSGC.js +31 -0
  28. package/dist/load-failure-C6OMJSGC.js.map +1 -0
  29. package/dist/load-failure-view-CBTB_h-h.js +48 -0
  30. package/dist/load-failure-view-CBTB_h-h.js.map +1 -0
  31. package/package.json +36 -21
  32. package/dist/ApiKeysView-DiCurTEU.js.map +0 -1
  33. package/dist/BackupsView-DNS6LdVg.js.map +0 -1
  34. package/dist/BranchesView-CM3B-ER3.js.map +0 -1
  35. package/dist/CronJobsView-BHtJZeJZ.js.map +0 -1
  36. package/dist/RLSEditor-CBX4uD66.js.map +0 -1
  37. package/dist/SchemaVisualizer-D6tMZgeC.js.map +0 -1
  38. package/dist/StorageView-BGDeC5GI.js.map +0 -1
  39. package/dist/components/StorageView/storage-failure.d.ts +0 -34
  40. package/src/components/ApiExplorer/ApiExplorer.tsx +0 -299
  41. package/src/components/ApiExplorer/EndpointDetail.tsx +0 -279
  42. package/src/components/ApiExplorer/TryItPanel.tsx +0 -499
  43. package/src/components/ApiExplorer/parseSpec.ts +0 -104
  44. package/src/components/ApiExplorer/types.ts +0 -84
  45. package/src/components/ApiKeys/ApiKeysView.tsx +0 -479
  46. package/src/components/ApiKeys/CreateApiKeyDialog.tsx +0 -711
  47. package/src/components/ApiKeys/permissions.ts +0 -145
  48. package/src/components/AuthSimulationSelector.tsx +0 -72
  49. package/src/components/Backups/BackupsView.tsx +0 -185
  50. package/src/components/Branches/BranchesView.tsx +0 -386
  51. package/src/components/CronJobs/CronJobsView.tsx +0 -413
  52. package/src/components/JSEditor/JSEditor.tsx +0 -1100
  53. package/src/components/JSEditor/JSEditorSidebar.tsx +0 -350
  54. package/src/components/JSEditor/JSMonacoEditor.tsx +0 -410
  55. package/src/components/LogsExplorer/LogsExplorer.tsx +0 -273
  56. package/src/components/LogsExplorer/useLogTail.ts +0 -356
  57. package/src/components/RLSEditor/PolicyEditor.tsx +0 -399
  58. package/src/components/RLSEditor/RLSEditor.tsx +0 -1128
  59. package/src/components/RLSEditor/index.ts +0 -1
  60. package/src/components/RLSEditor/policy-presets.ts +0 -176
  61. package/src/components/RLSEditor/saveSecurityRules.ts +0 -48
  62. package/src/components/RebaseStudio.tsx +0 -174
  63. package/src/components/SQLEditor/ExplainVisualizer.tsx +0 -128
  64. package/src/components/SQLEditor/MonacoEditor.tsx +0 -203
  65. package/src/components/SQLEditor/SQLEditor.tsx +0 -1518
  66. package/src/components/SQLEditor/SQLEditorSidebar.tsx +0 -173
  67. package/src/components/SQLEditor/SchemaBrowser.tsx +0 -168
  68. package/src/components/SQLEditor/sql_editor_types.ts +0 -18
  69. package/src/components/SchemaVisualizer/RelationEdge.tsx +0 -102
  70. package/src/components/SchemaVisualizer/SchemaVisualizer.tsx +0 -710
  71. package/src/components/SchemaVisualizer/TableNode.tsx +0 -257
  72. package/src/components/SchemaVisualizer/schema-visualizer.utils.ts +0 -140
  73. package/src/components/SchemaVisualizer/useLiveRls.ts +0 -86
  74. package/src/components/SchemaVisualizer/useSchemaGraph.ts +0 -426
  75. package/src/components/StorageView/StorageView.tsx +0 -1588
  76. package/src/components/StorageView/storage-failure.ts +0 -63
  77. package/src/components/StudioHomePage.tsx +0 -430
  78. package/src/index.ts +0 -31
  79. package/src/utils/parseSpec.test.ts +0 -309
  80. package/src/utils/sql_utils.test.ts +0 -265
  81. package/src/utils/sql_utils.ts +0 -291
@@ -1,499 +0,0 @@
1
- import React, { useState, useCallback, useMemo, useEffect } from "react";
2
- import {
3
- Button,
4
- cls,
5
- defaultBorderMixin,
6
- IconButton,
7
- iconSize,
8
- LoaderIcon,
9
- PlusIcon,
10
- SendIcon,
11
- TextField,
12
- Typography,
13
- XIcon
14
- } from "@rebasepro/ui";
15
- import { useRebaseContext, UserSelectPopover, SelectableUser } from "@rebasepro/app";
16
- import { AuthSimulationSelector } from "../AuthSimulationSelector";
17
- import type { ParsedEndpoint } from "./types";
18
- import type { User } from "@rebasepro/types";
19
-
20
- interface TryItPanelProps {
21
- endpoint: ParsedEndpoint;
22
- apiUrl: string;
23
- getAuthToken: () => Promise<string | null | undefined>;
24
- user: User | null;
25
- basePath?: string;
26
- }
27
-
28
- /**
29
- * Interactive "Try It" panel that lets the user execute API requests
30
- * using their current JWT token directly from the Studio.
31
- */
32
- export function TryItPanel({ endpoint, apiUrl, getAuthToken, user, basePath = "" }: TryItPanelProps) {
33
- const storageKey = `rebase_apiexplorer_${endpoint.method}_${endpoint.path}`;
34
-
35
- const [pathParams, setPathParams] = useState<Record<string, string>>(() => {
36
- try { const v = localStorage.getItem(`${storageKey}_path`); if (v) return JSON.parse(v); } catch { /* ignore */ }
37
- return {};
38
- });
39
- const [queryParams, setQueryParams] = useState<Record<string, string>>(() => {
40
- try { const v = localStorage.getItem(`${storageKey}_query`); if (v) return JSON.parse(v); } catch { /* ignore */ }
41
- return {};
42
- });
43
- const [customHeaders, setCustomHeaders] = useState<Array<{ key: string; value: string }>>(() => {
44
- try { const v = localStorage.getItem(`${storageKey}_headers`); if (v) return JSON.parse(v); } catch { /* ignore */ }
45
- return [{ key: "rebase-branch",
46
- value: "" }];
47
- });
48
- const [body, setBody] = useState(() => {
49
- try { const v = localStorage.getItem(`${storageKey}_body`); if (v) return JSON.parse(v); } catch { /* ignore */ }
50
- return buildBodyTemplate(endpoint);
51
- });
52
- const [response, setResponse] = useState<{ status: number; statusText: string; body: string; time: number } | null>(
53
- null
54
- );
55
- const [loading, setLoading] = useState(false);
56
- const [authMode, setAuthMode] = useState<"jwt" | "none">("jwt");
57
- const [validationError, setValidationError] = useState<string | null>(null);
58
-
59
- const rebaseContext = useRebaseContext();
60
- const currentUser = rebaseContext.authController?.user;
61
-
62
- const users = useMemo((): SelectableUser[] => {
63
- const managed: SelectableUser[] = [];
64
- if (currentUser) {
65
- managed.push({
66
- uid: currentUser.uid,
67
- displayName: currentUser.displayName,
68
- email: currentUser.email,
69
- photoURL: currentUser.photoURL,
70
- roles: currentUser.roles
71
- });
72
- }
73
- return managed;
74
- }, [currentUser]);
75
-
76
- const currentSelectableUser = useMemo((): SelectableUser | null => {
77
- if (!currentUser) return null;
78
- return {
79
- uid: currentUser.uid,
80
- displayName: currentUser.displayName,
81
- email: currentUser.email,
82
- photoURL: currentUser.photoURL,
83
- roles: currentUser.roles
84
- };
85
- }, [currentUser]);
86
-
87
- const [selectedUser, setSelectedUser] = useState<SelectableUser | null>(null);
88
-
89
- useEffect(() => {
90
- localStorage.setItem(`${storageKey}_path`, JSON.stringify(pathParams));
91
- localStorage.setItem(`${storageKey}_query`, JSON.stringify(queryParams));
92
- localStorage.setItem(`${storageKey}_headers`, JSON.stringify(customHeaders));
93
- localStorage.setItem(`${storageKey}_body`, JSON.stringify(body));
94
- }, [storageKey, pathParams, queryParams, customHeaders, body]);
95
-
96
- // Build the final URL
97
- const resolvedUrl = useMemo(() => {
98
- const base = basePath.startsWith("/") ? basePath : `/${basePath}`;
99
- const cleanBase = base === "/" ? "" : base;
100
- let url = `${apiUrl.replace(/\/+$/, "")}${cleanBase}${endpoint.path}`;
101
- // Replace path params
102
- for (const [key, val] of Object.entries(pathParams)) {
103
- url = url.replace(`{${key}}`, encodeURIComponent(val));
104
- }
105
- // Append query params
106
- const qp = Object.entries(queryParams).filter(([, v]) => v.trim() !== "");
107
- if (qp.length > 0) {
108
- url += "?" + qp.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
109
- }
110
- return url;
111
- }, [apiUrl, basePath, endpoint.path, pathParams, queryParams]);
112
-
113
- const pathParamDefs = endpoint.parameters.filter((p) => p.in === "path");
114
- const queryParamDefs = endpoint.parameters.filter((p) => p.in === "query");
115
- const hasBody = ["post", "put", "patch"].includes(endpoint.method);
116
-
117
- const execute = useCallback(async () => {
118
- setValidationError(null);
119
- if (hasBody && body.trim()) {
120
- try {
121
- JSON.parse(body);
122
- } catch (err: unknown) {
123
- setValidationError(`Invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
124
- return;
125
- }
126
- }
127
-
128
- setLoading(true);
129
- setResponse(null);
130
- const start = performance.now();
131
-
132
- try {
133
- const headers: Record<string, string> = { "Content-Type": "application/json" };
134
-
135
- for (const h of customHeaders) {
136
- if (h.key.trim()) headers[h.key.trim()] = h.value;
137
- }
138
-
139
- if (authMode === "jwt") {
140
- const token = await getAuthToken();
141
- if (token) headers["Authorization"] = `Bearer ${token}`;
142
- if (selectedUser && selectedUser.uid !== currentUser?.uid) {
143
- headers["x-rebase-impersonate"] = selectedUser.uid;
144
- }
145
- }
146
-
147
- const res = await fetch(resolvedUrl, {
148
- method: endpoint.method.toUpperCase(),
149
- headers,
150
- body: hasBody && body.trim() ? body : undefined
151
- });
152
-
153
- const elapsed = Math.round(performance.now() - start);
154
- let text: string;
155
-
156
- const rawText = await res.text();
157
- try {
158
- const json = JSON.parse(rawText);
159
- text = JSON.stringify(json, null, 2);
160
- } catch {
161
- text = rawText;
162
- }
163
-
164
- setResponse({ status: res.status,
165
- statusText: res.statusText,
166
- body: text,
167
- time: elapsed });
168
- } catch (err: unknown) {
169
- setResponse({
170
- status: 0,
171
- statusText: "Network Error",
172
- body: err instanceof Error ? err.message : "Request failed",
173
- time: Math.round(performance.now() - start)
174
- });
175
- } finally {
176
- setLoading(false);
177
- }
178
- }, [resolvedUrl, endpoint.method, hasBody, body, authMode, getAuthToken, customHeaders, selectedUser, currentUser?.uid]);
179
-
180
- return (
181
- <div className="flex flex-col h-full">
182
- <div className="p-5 space-y-5 overflow-y-auto flex-1">
183
- {/* Auth Mode */}
184
- <AuthSimulationSelector
185
- authMode={authMode}
186
- setAuthMode={setAuthMode}
187
- selectedUser={selectedUser}
188
- setSelectedUser={setSelectedUser}
189
- users={users}
190
- loading={false}
191
- currentUser={currentSelectableUser}
192
- />
193
-
194
- {/* Path Params */}
195
- {pathParamDefs.length > 0 && (
196
- <ParamSection
197
- title="Path Parameters"
198
- params={pathParamDefs}
199
- values={pathParams}
200
- onChange={(k, v) => setPathParams((prev) => ({ ...prev,
201
- [k]: v }))}
202
- />
203
- )}
204
-
205
- {/* Query Params */}
206
- {queryParamDefs.length > 0 && (
207
- <ParamSection
208
- title="Query Parameters"
209
- params={queryParamDefs}
210
- values={queryParams}
211
- onChange={(k, v) => setQueryParams((prev) => ({ ...prev,
212
- [k]: v }))}
213
- />
214
- )}
215
-
216
- {/* Custom Headers */}
217
- <CustomKeyValueSection
218
- title="Custom Headers"
219
- values={customHeaders}
220
- onChange={(i, k, v) => {
221
- const next = [...customHeaders];
222
- next[i] = { key: k,
223
- value: v };
224
- setCustomHeaders(next);
225
- }}
226
- onAdd={() => setCustomHeaders((prev) => [...prev, { key: "",
227
- value: "" }])}
228
- onRemove={(i) => {
229
- const next = [...customHeaders];
230
- next.splice(i, 1);
231
- setCustomHeaders(next);
232
- }}
233
- />
234
-
235
- {/* Request Body */}
236
- {hasBody && (
237
- <div>
238
- <Typography
239
- variant="caption"
240
- className="text-text-secondary dark:text-text-secondary-dark text-xs font-semibold uppercase tracking-wider mb-2 block"
241
- >
242
- Request Body
243
- </Typography>
244
- <TextField
245
- multiline
246
- minRows={10}
247
- aria-label="Request Body"
248
- value={body}
249
- onChange={(e) => { setBody(e.target.value); setValidationError(null); }}
250
- spellCheck={false}
251
- error={!!validationError}
252
- className="w-full"
253
- inputClassName="font-mono text-xs p-3 resize-y"
254
- />
255
- {validationError && (
256
- <Typography variant="caption" className="text-red-500 mt-1 block text-xs">
257
- {validationError}
258
- </Typography>
259
- )}
260
- </div>
261
- )}
262
-
263
- {/* URL Preview */}
264
- <div className="rounded-lg bg-surface-100 dark:bg-surface-900 p-3">
265
- <Typography
266
- variant="caption"
267
- className="text-text-secondary dark:text-text-secondary-dark text-[10px] uppercase tracking-wider block mb-1"
268
- >
269
- Request URL
270
- </Typography>
271
- <code className="text-xs font-mono text-text-primary dark:text-text-primary-dark break-all">
272
- {resolvedUrl}
273
- </code>
274
- </div>
275
-
276
- {/* Execute Button */}
277
- <Button variant="filled" onClick={execute} disabled={loading} className="w-full">
278
- {loading ? (
279
- <span className="flex items-center gap-2">
280
- <LoaderIcon size={iconSize.small} className="animate-spin" />
281
- Sending…
282
- </span>
283
- ) : (
284
- <span className="flex items-center gap-2">
285
- <SendIcon size={iconSize.small} />
286
- Send Request
287
- </span>
288
- )}
289
- </Button>
290
-
291
- {/* Response */}
292
- {response && (
293
- <div className={cls("rounded-lg border overflow-hidden", defaultBorderMixin)}>
294
- <div
295
- className={cls(
296
- "flex items-center justify-between px-4 py-2.5",
297
- "bg-surface-50 dark:bg-surface-900/50"
298
- )}
299
- >
300
- <div className="flex items-center gap-3">
301
- <ResponseBadge status={response.status}/>
302
- <Typography
303
- variant="caption"
304
- className="text-text-secondary dark:text-text-secondary-dark text-xs"
305
- >
306
- {response.statusText}
307
- </Typography>
308
- </div>
309
- <Typography
310
- variant="caption"
311
- className="text-text-secondary dark:text-text-secondary-dark text-xs font-mono"
312
- >
313
- {response.time}ms
314
- </Typography>
315
- </div>
316
- <pre
317
- className={cls(
318
- "p-4 text-xs font-mono overflow-auto max-h-96",
319
- "bg-surface-950 text-emerald-400",
320
- "dark:bg-surface-900 dark:text-emerald-400"
321
- )}
322
- >
323
- {response.body}
324
- </pre>
325
- </div>
326
- )}
327
- </div>
328
- </div>
329
- );
330
- }
331
-
332
- /* ── Param Section ────────────────────────────────────────────────── */
333
-
334
- function ParamSection({
335
- title,
336
- params,
337
- values,
338
- onChange
339
- }: {
340
- title: string;
341
- params: ParsedEndpoint["parameters"];
342
- values: Record<string, string>;
343
- onChange: (_key: string, _value: string) => void;
344
- }) {
345
- const paramIdBase = React.useId();
346
- return (
347
- <div>
348
- <Typography
349
- variant="caption"
350
- className="text-text-secondary dark:text-text-secondary-dark text-xs font-semibold uppercase tracking-wider mb-2 block"
351
- >
352
- {title}
353
- </Typography>
354
- <div className="space-y-2">
355
- {params.map((p, index) => (
356
- <div key={p.name} className="flex items-center gap-3">
357
- <label className="w-32 shrink-0" htmlFor={`${paramIdBase}-${index}-param`}>
358
- <code className="text-xs font-mono font-semibold">{p.name}</code>
359
- {p.required && <span className="text-red-500 ml-0.5 text-xs">*</span>}
360
- </label>
361
- <TextField
362
- id={`${paramIdBase}-${index}-param`}
363
- size="small"
364
- placeholder={p.description ?? p.name}
365
- value={values[p.name] ?? ""}
366
- onChange={(e) => onChange(p.name, e.target.value)}
367
- className="flex-1"
368
- inputClassName="font-mono text-xs"
369
- />
370
- </div>
371
- ))}
372
- </div>
373
- </div>
374
- );
375
- }
376
-
377
- function CustomKeyValueSection({
378
- title,
379
- values,
380
- onChange,
381
- onAdd,
382
- onRemove
383
- }: {
384
- title: string;
385
- values: Array<{ key: string; value: string }>;
386
- onChange: (index: number, key: string, value: string) => void;
387
- onAdd: () => void;
388
- onRemove: (index: number) => void;
389
- }) {
390
- return (
391
- <div>
392
- <div className="flex items-center justify-between mb-2">
393
- <Typography
394
- variant="caption"
395
- className="text-text-secondary dark:text-text-secondary-dark text-xs font-semibold uppercase tracking-wider block"
396
- >
397
- {title}
398
- </Typography>
399
- <Button
400
- variant="text"
401
- size="small"
402
- color="primary"
403
- onClick={onAdd}
404
- className="text-xs p-0 min-h-0"
405
- >
406
- <PlusIcon size={iconSize.small} className="mr-1" /> Add Header
407
- </Button>
408
- </div>
409
- <div className="space-y-2">
410
- {values.map((v, i) => (
411
- <div key={i} className="flex items-center gap-2">
412
- <TextField
413
- size="small"
414
- aria-label={`Header ${i + 1} name`}
415
- placeholder="Header name"
416
- value={v.key}
417
- onChange={(e) => onChange(i, e.target.value, v.value)}
418
- className="w-1/3"
419
- inputClassName="font-mono text-xs"
420
- />
421
- <TextField
422
- size="small"
423
- aria-label={`Header ${i + 1} value`}
424
- placeholder="Value"
425
- value={v.value}
426
- onChange={(e) => onChange(i, v.key, e.target.value)}
427
- className="flex-1"
428
- inputClassName="font-mono text-xs"
429
- />
430
- <IconButton
431
- size="small"
432
- onClick={() => onRemove(i)}
433
- className="text-text-secondary hover:text-red-500 shrink-0"
434
- title="Remove"
435
- >
436
- <XIcon size={iconSize.small} />
437
- </IconButton>
438
- </div>
439
- ))}
440
- {values.length === 0 && (
441
- <Typography variant="caption" className="text-text-secondary/50 italic text-xs">
442
- No custom headers added.
443
- </Typography>
444
- )}
445
- </div>
446
- </div>
447
- );
448
- }
449
-
450
- /* ── Helpers ──────────────────────────────────────────────────────── */
451
-
452
- function ResponseBadge({ status }: { status: number }) {
453
- let colorClass = "text-red-500";
454
- if (status >= 200 && status < 300) colorClass = "text-emerald-500";
455
- else if (status >= 300 && status < 400) colorClass = "text-blue-500";
456
- else if (status >= 400 && status < 500) colorClass = "text-amber-500";
457
-
458
- return (
459
- <span className={cls("text-xs font-semibold font-mono", colorClass)}>
460
- {status || "ERR"}
461
- </span>
462
- );
463
- }
464
-
465
- function buildBodyTemplate(endpoint: ParsedEndpoint): string {
466
- if (!endpoint.requestBody?.content) return "{\n \n}";
467
- const json = endpoint.requestBody.content["application/json"];
468
- if (!json?.schema?.properties) return "{\n \n}";
469
-
470
- const props = json.schema.properties;
471
- const lines: string[] = ["{"];
472
- const keys = Object.keys(props);
473
- keys.forEach((key, i) => {
474
- const prop = props[key];
475
- if (prop.readOnly) return;
476
- const comma = i < keys.length - 1 ? "," : "";
477
- const val = defaultValue(prop);
478
- lines.push(` "${key}": ${val}${comma}`);
479
- });
480
- lines.push("}");
481
- return lines.join("\n");
482
- }
483
-
484
- function defaultValue(schema: { type?: string; format?: string; enum?: (string | number)[] }): string {
485
- if (schema.enum) return JSON.stringify(schema.enum[0]);
486
- switch (schema.type) {
487
- case "string":
488
- return schema.format === "date-time" ? `"${new Date().toISOString()}"` : '""';
489
- case "number":
490
- case "integer":
491
- return "0";
492
- case "boolean":
493
- return "false";
494
- case "array":
495
- return "[]";
496
- default:
497
- return "null";
498
- }
499
- }
@@ -1,104 +0,0 @@
1
- import type { OpenApiSpec, ParsedEndpoint, EndpointGroup } from "./types";
2
-
3
- /**
4
- * Parse an OpenAPI 3.x spec into grouped, sorted endpoints for the sidebar.
5
- */
6
- export function parseOpenApiSpec(spec: OpenApiSpec): {
7
- groups: EndpointGroup[];
8
- allEndpoints: ParsedEndpoint[];
9
- } {
10
- const allEndpoints: ParsedEndpoint[] = [];
11
- const tagMap = new Map<string, ParsedEndpoint[]>();
12
-
13
- // Build tag description lookup
14
- const tagDescriptions = new Map<string, string>();
15
- for (const t of spec.tags ?? []) {
16
- tagDescriptions.set(t.name, t.description ?? "");
17
- }
18
-
19
- for (const [path, methods] of Object.entries(spec.paths ?? {})) {
20
- for (const [method, op] of Object.entries(methods)) {
21
- if (["get", "post", "put", "patch", "delete"].indexOf(method) === -1) continue;
22
-
23
- const tags = op.tags?.length ? op.tags : ["Other"];
24
- const shortPath = path.replace(/^\/api\/data/, "");
25
-
26
- const endpoint: ParsedEndpoint = {
27
- id: `${method}:${path}`,
28
- method,
29
- path,
30
- shortPath: shortPath || "/",
31
- summary: op.summary ?? "",
32
- description: op.description ?? "",
33
- tags,
34
- parameters: op.parameters ?? [],
35
- requestBody: op.requestBody,
36
- responses: op.responses ?? {},
37
- security: op.security,
38
- operationId: op.operationId
39
- };
40
-
41
- allEndpoints.push(endpoint);
42
- for (const tag of tags) {
43
- if (!tagMap.has(tag)) tagMap.set(tag, []);
44
- tagMap.get(tag)!.push(endpoint);
45
- }
46
- }
47
- }
48
-
49
- // Method sort order
50
- const ORDER: Record<string, number> = { get: 0,
51
- post: 1,
52
- put: 2,
53
- patch: 3,
54
- delete: 4 };
55
-
56
- const groups: EndpointGroup[] = [];
57
- // Sort tags: use spec.tags order if available, else alphabetical
58
- const tagOrder = (spec.tags ?? []).map((t) => t.name);
59
- const sortedTags = [...tagMap.keys()].sort((a, b) => {
60
- const ai = tagOrder.indexOf(a);
61
- const bi = tagOrder.indexOf(b);
62
- if (ai !== -1 && bi !== -1) return ai - bi;
63
- if (ai !== -1) return -1;
64
- if (bi !== -1) return 1;
65
- return a.localeCompare(b);
66
- });
67
-
68
- for (const tag of sortedTags) {
69
- const endpoints = tagMap.get(tag)!;
70
- endpoints.sort((a, b) => {
71
- const pa = a.path.localeCompare(b.path);
72
- if (pa !== 0) return pa;
73
- return (ORDER[a.method] ?? 99) - (ORDER[b.method] ?? 99);
74
- });
75
- groups.push({
76
- tag,
77
- description: tagDescriptions.get(tag),
78
- endpoints
79
- });
80
- }
81
-
82
- return { groups,
83
- allEndpoints };
84
- }
85
-
86
- /**
87
- * Resolve a $ref string (e.g. "#/components/schemas/Author") to a schema name.
88
- */
89
- export function resolveRefName(ref: string): string {
90
- const parts = ref.split("/");
91
- return parts[parts.length - 1];
92
- }
93
-
94
- /**
95
- * Resolve a $ref to its actual schema from the spec.
96
- */
97
- export function resolveRef(spec: OpenApiSpec, ref: string): unknown {
98
- const parts = ref.replace("#/", "").split("/");
99
- let current: unknown = spec;
100
- for (const part of parts) {
101
- current = (current as Record<string, unknown>)?.[part];
102
- }
103
- return current ?? {};
104
- }
@@ -1,84 +0,0 @@
1
- /** Minimal OpenAPI 3.x types we care about */
2
- export interface OpenApiSpec {
3
- openapi: string;
4
- info: { title: string; version: string; description?: string };
5
- servers?: { url: string; description?: string }[];
6
- paths: Record<string, Record<string, OpenApiOperation>>;
7
- components?: {
8
- schemas?: Record<string, OpenApiSchema>;
9
- securitySchemes?: Record<string, unknown>;
10
- };
11
- tags?: { name: string; description?: string }[];
12
- security?: Record<string, string[]>[];
13
- }
14
-
15
- export interface OpenApiOperation {
16
- operationId?: string;
17
- summary?: string;
18
- description?: string;
19
- tags?: string[];
20
- parameters?: OpenApiParameter[];
21
- requestBody?: {
22
- required?: boolean;
23
- content?: Record<string, { schema?: OpenApiSchema }>;
24
- };
25
- responses?: Record<
26
- string,
27
- {
28
- description?: string;
29
- content?: Record<string, { schema?: OpenApiSchema }>;
30
- }
31
- >;
32
- security?: Record<string, string[]>[];
33
- }
34
-
35
- export interface OpenApiParameter {
36
- name: string;
37
- in: "query" | "path" | "header" | "cookie";
38
- description?: string;
39
- required?: boolean;
40
- schema?: OpenApiSchema;
41
- }
42
-
43
- export interface OpenApiSchema {
44
- type?: string;
45
- format?: string;
46
- description?: string;
47
- properties?: Record<string, OpenApiSchema>;
48
- required?: string[];
49
- items?: OpenApiSchema;
50
- enum?: (string | number)[];
51
- oneOf?: OpenApiSchema[];
52
- anyOf?: OpenApiSchema[];
53
- allOf?: OpenApiSchema[];
54
- $ref?: string;
55
- readOnly?: boolean;
56
- minimum?: number;
57
- maximum?: number;
58
- maxLength?: number;
59
- minLength?: number;
60
- default?: unknown;
61
- example?: unknown;
62
- additionalProperties?: boolean | OpenApiSchema;
63
- }
64
-
65
- export interface ParsedEndpoint {
66
- id: string;
67
- method: string;
68
- path: string;
69
- shortPath: string;
70
- summary: string;
71
- description: string;
72
- tags: string[];
73
- parameters: OpenApiParameter[];
74
- requestBody?: OpenApiOperation["requestBody"];
75
- responses: NonNullable<OpenApiOperation["responses"]>;
76
- security?: OpenApiOperation["security"];
77
- operationId?: string;
78
- }
79
-
80
- export interface EndpointGroup {
81
- tag: string;
82
- description?: string;
83
- endpoints: ParsedEndpoint[];
84
- }