@rebasepro/studio 0.17.3 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +0 -1
- package/README.md +12 -4
- package/dist/{ApiKeysView-DiCurTEU.js → ApiKeysView-EIvegrUk.js} +54 -39
- package/dist/ApiKeysView-EIvegrUk.js.map +1 -0
- package/dist/{BackupsView-DNS6LdVg.js → BackupsView-DVkKzaPR.js} +29 -31
- package/dist/BackupsView-DVkKzaPR.js.map +1 -0
- package/dist/{BranchesView-CM3B-ER3.js → BranchesView-Df8hQ8wE.js} +85 -24
- package/dist/BranchesView-Df8hQ8wE.js.map +1 -0
- package/dist/{CronJobsView-BHtJZeJZ.js → CronJobsView-CLOGjb-L.js} +32 -27
- package/dist/CronJobsView-CLOGjb-L.js.map +1 -0
- package/dist/{RLSEditor-CBX4uD66.js → RLSEditor-Sa1D34ny.js} +724 -615
- package/dist/RLSEditor-Sa1D34ny.js.map +1 -0
- package/dist/{SQLEditor-ByecwX8B.js → SQLEditor-BfNA8xM3.js} +9 -4
- package/dist/{SQLEditor-ByecwX8B.js.map → SQLEditor-BfNA8xM3.js.map} +1 -1
- package/dist/{SchemaVisualizer-D6tMZgeC.js → SchemaVisualizer-DTDMYL6o.js} +84 -110
- package/dist/SchemaVisualizer-DTDMYL6o.js.map +1 -0
- package/dist/{StorageView-BGDeC5GI.js → StorageView-Ed9Q_VlB.js} +5 -32
- package/dist/StorageView-Ed9Q_VlB.js.map +1 -0
- package/dist/components/RLSEditor/saveRules.d.ts +38 -0
- package/dist/components/RebaseStudio.d.ts +1 -1
- package/dist/components/SchemaVisualizer/schema-visualizer.utils.d.ts +5 -2
- package/dist/components/SchemaVisualizer/useSchemaGraph.d.ts +0 -4
- package/dist/components/load-failure-view.d.ts +17 -0
- package/dist/components/load-failure.d.ts +47 -0
- package/dist/index.es.js +56 -36
- package/dist/index.es.js.map +1 -1
- package/dist/load-failure-C6OMJSGC.js +31 -0
- package/dist/load-failure-C6OMJSGC.js.map +1 -0
- package/dist/load-failure-view-CBTB_h-h.js +48 -0
- package/dist/load-failure-view-CBTB_h-h.js.map +1 -0
- package/package.json +36 -21
- package/dist/ApiKeysView-DiCurTEU.js.map +0 -1
- package/dist/BackupsView-DNS6LdVg.js.map +0 -1
- package/dist/BranchesView-CM3B-ER3.js.map +0 -1
- package/dist/CronJobsView-BHtJZeJZ.js.map +0 -1
- package/dist/RLSEditor-CBX4uD66.js.map +0 -1
- package/dist/SchemaVisualizer-D6tMZgeC.js.map +0 -1
- package/dist/StorageView-BGDeC5GI.js.map +0 -1
- package/dist/components/StorageView/storage-failure.d.ts +0 -34
- package/src/components/ApiExplorer/ApiExplorer.tsx +0 -299
- package/src/components/ApiExplorer/EndpointDetail.tsx +0 -279
- package/src/components/ApiExplorer/TryItPanel.tsx +0 -499
- package/src/components/ApiExplorer/parseSpec.ts +0 -104
- package/src/components/ApiExplorer/types.ts +0 -84
- package/src/components/ApiKeys/ApiKeysView.tsx +0 -479
- package/src/components/ApiKeys/CreateApiKeyDialog.tsx +0 -711
- package/src/components/ApiKeys/permissions.ts +0 -145
- package/src/components/AuthSimulationSelector.tsx +0 -72
- package/src/components/Backups/BackupsView.tsx +0 -185
- package/src/components/Branches/BranchesView.tsx +0 -386
- package/src/components/CronJobs/CronJobsView.tsx +0 -413
- package/src/components/JSEditor/JSEditor.tsx +0 -1100
- package/src/components/JSEditor/JSEditorSidebar.tsx +0 -350
- package/src/components/JSEditor/JSMonacoEditor.tsx +0 -410
- package/src/components/LogsExplorer/LogsExplorer.tsx +0 -273
- package/src/components/LogsExplorer/useLogTail.ts +0 -356
- package/src/components/RLSEditor/PolicyEditor.tsx +0 -399
- package/src/components/RLSEditor/RLSEditor.tsx +0 -1128
- package/src/components/RLSEditor/index.ts +0 -1
- package/src/components/RLSEditor/policy-presets.ts +0 -176
- package/src/components/RLSEditor/saveSecurityRules.ts +0 -48
- package/src/components/RebaseStudio.tsx +0 -174
- package/src/components/SQLEditor/ExplainVisualizer.tsx +0 -128
- package/src/components/SQLEditor/MonacoEditor.tsx +0 -203
- package/src/components/SQLEditor/SQLEditor.tsx +0 -1518
- package/src/components/SQLEditor/SQLEditorSidebar.tsx +0 -173
- package/src/components/SQLEditor/SchemaBrowser.tsx +0 -168
- package/src/components/SQLEditor/sql_editor_types.ts +0 -18
- package/src/components/SchemaVisualizer/RelationEdge.tsx +0 -102
- package/src/components/SchemaVisualizer/SchemaVisualizer.tsx +0 -710
- package/src/components/SchemaVisualizer/TableNode.tsx +0 -257
- package/src/components/SchemaVisualizer/schema-visualizer.utils.ts +0 -140
- package/src/components/SchemaVisualizer/useLiveRls.ts +0 -86
- package/src/components/SchemaVisualizer/useSchemaGraph.ts +0 -426
- package/src/components/StorageView/StorageView.tsx +0 -1588
- package/src/components/StorageView/storage-failure.ts +0 -63
- package/src/components/StudioHomePage.tsx +0 -430
- package/src/index.ts +0 -31
- package/src/utils/parseSpec.test.ts +0 -309
- package/src/utils/sql_utils.test.ts +0 -265
- package/src/utils/sql_utils.ts +0 -291
|
@@ -1,309 +0,0 @@
|
|
|
1
|
-
import { parseOpenApiSpec, resolveRefName, resolveRef } from "../components/ApiExplorer/parseSpec";
|
|
2
|
-
import type { OpenApiParameter, OpenApiSpec } from "../components/ApiExplorer/types";
|
|
3
|
-
|
|
4
|
-
function makeSpec(overrides: Partial<OpenApiSpec> = {}): OpenApiSpec {
|
|
5
|
-
return {
|
|
6
|
-
// `openapi` and `info` are required on `OpenApiSpec` and this builder
|
|
7
|
-
// supplied neither, so every fixture below was one cast away from being
|
|
8
|
-
// a document no parser would accept.
|
|
9
|
-
openapi: "3.0.0",
|
|
10
|
-
info: { title: "Test API",
|
|
11
|
-
version: "1.0.0" },
|
|
12
|
-
paths: {},
|
|
13
|
-
tags: [],
|
|
14
|
-
...overrides
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
describe("parseOpenApiSpec", () => {
|
|
19
|
-
it("returns empty groups and endpoints for spec with no paths", () => {
|
|
20
|
-
const result = parseOpenApiSpec(makeSpec());
|
|
21
|
-
expect(result.groups).toEqual([]);
|
|
22
|
-
expect(result.allEndpoints).toEqual([]);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it("parses a single GET endpoint", () => {
|
|
26
|
-
const spec = makeSpec({
|
|
27
|
-
paths: {
|
|
28
|
-
"/api/data/users": {
|
|
29
|
-
get: {
|
|
30
|
-
summary: "List users",
|
|
31
|
-
tags: ["Users"]
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
});
|
|
36
|
-
const result = parseOpenApiSpec(spec);
|
|
37
|
-
expect(result.allEndpoints).toHaveLength(1);
|
|
38
|
-
expect(result.allEndpoints[0].method).toBe("get");
|
|
39
|
-
expect(result.allEndpoints[0].path).toBe("/api/data/users");
|
|
40
|
-
expect(result.allEndpoints[0].summary).toBe("List users");
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("groups endpoints by tag", () => {
|
|
44
|
-
const spec = makeSpec({
|
|
45
|
-
paths: {
|
|
46
|
-
"/api/data/users": {
|
|
47
|
-
get: { tags: ["Users"],
|
|
48
|
-
summary: "List users" }
|
|
49
|
-
},
|
|
50
|
-
"/api/data/posts": {
|
|
51
|
-
get: { tags: ["Posts"],
|
|
52
|
-
summary: "List posts" }
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
const result = parseOpenApiSpec(spec);
|
|
57
|
-
expect(result.groups).toHaveLength(2);
|
|
58
|
-
expect(result.groups.map(g => g.tag).sort()).toEqual(["Posts", "Users"]);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("assigns endpoints with no tags to 'Other' group", () => {
|
|
62
|
-
const spec = makeSpec({
|
|
63
|
-
paths: {
|
|
64
|
-
"/api/data/misc": {
|
|
65
|
-
get: { summary: "Misc endpoint" }
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
const result = parseOpenApiSpec(spec);
|
|
70
|
-
expect(result.groups).toHaveLength(1);
|
|
71
|
-
expect(result.groups[0].tag).toBe("Other");
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("strips /api/data prefix from shortPath", () => {
|
|
75
|
-
const spec = makeSpec({
|
|
76
|
-
paths: {
|
|
77
|
-
"/api/data/users": {
|
|
78
|
-
get: { tags: ["Users"],
|
|
79
|
-
summary: "List" }
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
const result = parseOpenApiSpec(spec);
|
|
84
|
-
expect(result.allEndpoints[0].shortPath).toBe("/users");
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("uses / as shortPath when path is exactly /api/data", () => {
|
|
88
|
-
const spec = makeSpec({
|
|
89
|
-
paths: {
|
|
90
|
-
"/api/data": {
|
|
91
|
-
get: { tags: ["Root"],
|
|
92
|
-
summary: "Root" }
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
const result = parseOpenApiSpec(spec);
|
|
97
|
-
expect(result.allEndpoints[0].shortPath).toBe("/");
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it("sorts endpoints within a group by path then method", () => {
|
|
101
|
-
const spec = makeSpec({
|
|
102
|
-
paths: {
|
|
103
|
-
"/api/data/users": {
|
|
104
|
-
delete: { tags: ["Users"],
|
|
105
|
-
summary: "Delete" },
|
|
106
|
-
get: { tags: ["Users"],
|
|
107
|
-
summary: "List" },
|
|
108
|
-
post: { tags: ["Users"],
|
|
109
|
-
summary: "Create" }
|
|
110
|
-
},
|
|
111
|
-
"/api/data/users/{id}": {
|
|
112
|
-
get: { tags: ["Users"],
|
|
113
|
-
summary: "Get one" }
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
const result = parseOpenApiSpec(spec);
|
|
118
|
-
const users = result.groups.find(g => g.tag === "Users");
|
|
119
|
-
expect(users).toBeDefined();
|
|
120
|
-
// Path first, then the CRUD method order — not the order the spec
|
|
121
|
-
// happened to declare them in (delete, get, post above). Asserting only
|
|
122
|
-
// `[0]` left both halves of that free to be wrong.
|
|
123
|
-
expect(users!.endpoints.map(e => `${e.path}:${e.method}`)).toEqual([
|
|
124
|
-
"/api/data/users:get",
|
|
125
|
-
"/api/data/users:post",
|
|
126
|
-
"/api/data/users:delete",
|
|
127
|
-
"/api/data/users/{id}:get"
|
|
128
|
-
]);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it("respects tag order from spec.tags", () => {
|
|
132
|
-
const spec = makeSpec({
|
|
133
|
-
tags: [
|
|
134
|
-
{ name: "Posts",
|
|
135
|
-
description: "Blog posts" },
|
|
136
|
-
{ name: "Users",
|
|
137
|
-
description: "User management" }
|
|
138
|
-
],
|
|
139
|
-
paths: {
|
|
140
|
-
"/api/data/users": {
|
|
141
|
-
get: { tags: ["Users"],
|
|
142
|
-
summary: "List users" }
|
|
143
|
-
},
|
|
144
|
-
"/api/data/posts": {
|
|
145
|
-
get: { tags: ["Posts"],
|
|
146
|
-
summary: "List posts" }
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
});
|
|
150
|
-
const result = parseOpenApiSpec(spec);
|
|
151
|
-
expect(result.groups[0].tag).toBe("Posts");
|
|
152
|
-
expect(result.groups[1].tag).toBe("Users");
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it("includes tag descriptions from spec.tags", () => {
|
|
156
|
-
const spec = makeSpec({
|
|
157
|
-
tags: [
|
|
158
|
-
{ name: "Users",
|
|
159
|
-
description: "User management endpoints" }
|
|
160
|
-
],
|
|
161
|
-
paths: {
|
|
162
|
-
"/api/data/users": {
|
|
163
|
-
get: { tags: ["Users"],
|
|
164
|
-
summary: "List" }
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
});
|
|
168
|
-
const result = parseOpenApiSpec(spec);
|
|
169
|
-
expect(result.groups[0].description).toBe("User management endpoints");
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
it("extracts parameters and requestBody", () => {
|
|
173
|
-
// Annotated: inferred, `in` widens to `string` and the array is not an
|
|
174
|
-
// `OpenApiParameter[]` — the very shape this test says it extracts.
|
|
175
|
-
const params: OpenApiParameter[] = [{ name: "id",
|
|
176
|
-
in: "path",
|
|
177
|
-
required: true }];
|
|
178
|
-
const requestBody = { content: { "application/json": { schema: {} } } };
|
|
179
|
-
const spec = makeSpec({
|
|
180
|
-
paths: {
|
|
181
|
-
"/api/data/users/{id}": {
|
|
182
|
-
put: {
|
|
183
|
-
tags: ["Users"],
|
|
184
|
-
parameters: params,
|
|
185
|
-
requestBody
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
});
|
|
190
|
-
const result = parseOpenApiSpec(spec);
|
|
191
|
-
expect(result.allEndpoints[0].parameters).toEqual(params);
|
|
192
|
-
expect(result.allEndpoints[0].requestBody).toEqual(requestBody);
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
it("handles multiple HTTP methods on same path", () => {
|
|
196
|
-
const spec = makeSpec({
|
|
197
|
-
paths: {
|
|
198
|
-
"/api/data/users": {
|
|
199
|
-
get: { tags: ["Users"],
|
|
200
|
-
summary: "List" },
|
|
201
|
-
post: { tags: ["Users"],
|
|
202
|
-
summary: "Create" }
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
});
|
|
206
|
-
const result = parseOpenApiSpec(spec);
|
|
207
|
-
expect(result.allEndpoints).toHaveLength(2);
|
|
208
|
-
const methods = result.allEndpoints.map(e => e.method).sort();
|
|
209
|
-
expect(methods).toEqual(["get", "post"]);
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
it("filters out non-standard HTTP methods", () => {
|
|
213
|
-
const spec = makeSpec({
|
|
214
|
-
paths: {
|
|
215
|
-
"/api/data/users": {
|
|
216
|
-
get: { tags: ["Users"],
|
|
217
|
-
summary: "List" },
|
|
218
|
-
options: { tags: ["Users"],
|
|
219
|
-
summary: "Options" },
|
|
220
|
-
parameters: [{ name: "test" }] as unknown as Record<string, unknown>
|
|
221
|
-
} as unknown as Record<string, never>
|
|
222
|
-
}
|
|
223
|
-
});
|
|
224
|
-
const result = parseOpenApiSpec(spec);
|
|
225
|
-
// Only 'get' should be included (not options, not parameters)
|
|
226
|
-
expect(result.allEndpoints).toHaveLength(1);
|
|
227
|
-
expect(result.allEndpoints[0].method).toBe("get");
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
it("generates unique endpoint IDs", () => {
|
|
231
|
-
const spec = makeSpec({
|
|
232
|
-
paths: {
|
|
233
|
-
"/api/data/users": {
|
|
234
|
-
get: { tags: ["Users"] },
|
|
235
|
-
post: { tags: ["Users"] }
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
});
|
|
239
|
-
const result = parseOpenApiSpec(spec);
|
|
240
|
-
const ids = result.allEndpoints.map(e => e.id);
|
|
241
|
-
expect(ids[0]).toBe("get:/api/data/users");
|
|
242
|
-
expect(ids[1]).toBe("post:/api/data/users");
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
it("defaults summary and description to empty strings", () => {
|
|
246
|
-
const spec = makeSpec({
|
|
247
|
-
paths: {
|
|
248
|
-
"/api/data/users": {
|
|
249
|
-
get: { tags: ["Users"] }
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
});
|
|
253
|
-
const result = parseOpenApiSpec(spec);
|
|
254
|
-
expect(result.allEndpoints[0].summary).toBe("");
|
|
255
|
-
expect(result.allEndpoints[0].description).toBe("");
|
|
256
|
-
});
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
describe("resolveRefName", () => {
|
|
260
|
-
it("extracts schema name from standard $ref", () => {
|
|
261
|
-
expect(resolveRefName("#/components/schemas/Author")).toBe("Author");
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
it("extracts last segment from deep path", () => {
|
|
265
|
-
expect(resolveRefName("#/components/schemas/nested/Type")).toBe("Type");
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
it("handles single-segment ref", () => {
|
|
269
|
-
expect(resolveRefName("Author")).toBe("Author");
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
it("handles empty string", () => {
|
|
273
|
-
expect(resolveRefName("")).toBe("");
|
|
274
|
-
});
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
describe("resolveRef", () => {
|
|
278
|
-
it("resolves a valid schema ref", () => {
|
|
279
|
-
const spec = makeSpec({
|
|
280
|
-
components: {
|
|
281
|
-
schemas: {
|
|
282
|
-
Author: { type: "object",
|
|
283
|
-
properties: { name: { type: "string" } } }
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
const result = resolveRef(spec, "#/components/schemas/Author");
|
|
289
|
-
expect(result).toEqual({
|
|
290
|
-
type: "object",
|
|
291
|
-
properties: { name: { type: "string" } }
|
|
292
|
-
});
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
it("returns empty object for invalid ref", () => {
|
|
296
|
-
const spec = makeSpec();
|
|
297
|
-
const result = resolveRef(spec, "#/components/schemas/NonExistent");
|
|
298
|
-
expect(result).toEqual({});
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
it("resolves nested refs", () => {
|
|
302
|
-
const spec = {
|
|
303
|
-
info: { title: "Test API" }
|
|
304
|
-
} as unknown as OpenApiSpec;
|
|
305
|
-
|
|
306
|
-
const result = resolveRef(spec, "#/info/title");
|
|
307
|
-
expect(result).toBe("Test API");
|
|
308
|
-
});
|
|
309
|
-
});
|
|
@@ -1,265 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import { describe, it, expect } from "@jest/globals";
|
|
3
|
-
import { determineTableAndPK, extractTablesFromQuery, resolveQueryCollections } from "./sql_utils";
|
|
4
|
-
import type { TableInfo } from "../components/SQLEditor/sql_editor_types";
|
|
5
|
-
import type { AdminCollection } from "@rebasepro/cms-types";
|
|
6
|
-
|
|
7
|
-
const mockSchemas: Record<string, TableInfo[]> = {
|
|
8
|
-
"public": [
|
|
9
|
-
{
|
|
10
|
-
schemaName: "public",
|
|
11
|
-
tableName: "users",
|
|
12
|
-
columns: [
|
|
13
|
-
{ name: "id",
|
|
14
|
-
dataType: "integer",
|
|
15
|
-
isPrimaryKey: true },
|
|
16
|
-
{ name: "email",
|
|
17
|
-
dataType: "text",
|
|
18
|
-
isPrimaryKey: false },
|
|
19
|
-
{ name: "created_at",
|
|
20
|
-
dataType: "timestamp",
|
|
21
|
-
isPrimaryKey: false }
|
|
22
|
-
]
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
schemaName: "public",
|
|
26
|
-
tableName: "roles",
|
|
27
|
-
columns: [
|
|
28
|
-
{ name: "id",
|
|
29
|
-
dataType: "integer",
|
|
30
|
-
isPrimaryKey: true },
|
|
31
|
-
{ name: "role_name",
|
|
32
|
-
dataType: "text",
|
|
33
|
-
isPrimaryKey: false },
|
|
34
|
-
{ name: "created_at",
|
|
35
|
-
dataType: "timestamp",
|
|
36
|
-
isPrimaryKey: false }
|
|
37
|
-
]
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
schemaName: "public",
|
|
41
|
-
tableName: "settings", // No primary key
|
|
42
|
-
columns: [
|
|
43
|
-
{ name: "key",
|
|
44
|
-
dataType: "text",
|
|
45
|
-
isPrimaryKey: false },
|
|
46
|
-
{ name: "value",
|
|
47
|
-
dataType: "text",
|
|
48
|
-
isPrimaryKey: false }
|
|
49
|
-
]
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
schemaName: "public",
|
|
53
|
-
tableName: "blog_posts",
|
|
54
|
-
columns: [
|
|
55
|
-
{ name: "id",
|
|
56
|
-
dataType: "integer",
|
|
57
|
-
isPrimaryKey: true },
|
|
58
|
-
{ name: "title",
|
|
59
|
-
dataType: "text",
|
|
60
|
-
isPrimaryKey: false },
|
|
61
|
-
{ name: "author_id",
|
|
62
|
-
dataType: "integer",
|
|
63
|
-
isPrimaryKey: false }
|
|
64
|
-
]
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
schemaName: "public",
|
|
68
|
-
tableName: "order_items", // Composite PK
|
|
69
|
-
columns: [
|
|
70
|
-
{ name: "order_id",
|
|
71
|
-
dataType: "integer",
|
|
72
|
-
isPrimaryKey: true },
|
|
73
|
-
{ name: "item_id",
|
|
74
|
-
dataType: "integer",
|
|
75
|
-
isPrimaryKey: true },
|
|
76
|
-
{ name: "quantity",
|
|
77
|
-
dataType: "integer",
|
|
78
|
-
isPrimaryKey: false }
|
|
79
|
-
]
|
|
80
|
-
}
|
|
81
|
-
]
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
const mockCollections: AdminCollection[] = [
|
|
85
|
-
{
|
|
86
|
-
slug: "users",
|
|
87
|
-
name: "Users",
|
|
88
|
-
table: "users",
|
|
89
|
-
properties: {}
|
|
90
|
-
} as AdminCollection,
|
|
91
|
-
{
|
|
92
|
-
slug: "roles",
|
|
93
|
-
name: "Roles",
|
|
94
|
-
table: "roles",
|
|
95
|
-
properties: {}
|
|
96
|
-
} as AdminCollection,
|
|
97
|
-
{
|
|
98
|
-
slug: "blogPosts",
|
|
99
|
-
name: "Blog Posts",
|
|
100
|
-
table: "blog_posts",
|
|
101
|
-
properties: {}
|
|
102
|
-
} as AdminCollection,
|
|
103
|
-
{
|
|
104
|
-
slug: "blog-entries", // slug with hyphen, no table → falls back to snake_case "blog_entries"
|
|
105
|
-
name: "Blog Entries",
|
|
106
|
-
table: "",
|
|
107
|
-
properties: {}
|
|
108
|
-
} as AdminCollection
|
|
109
|
-
];
|
|
110
|
-
|
|
111
|
-
describe("determineTableAndPK", () => {
|
|
112
|
-
it("resolves basic SELECT with actual PK from schema", () => {
|
|
113
|
-
const sql = "SELECT * FROM users";
|
|
114
|
-
const result = determineTableAndPK(sql, "email", mockSchemas);
|
|
115
|
-
expect(result.tableName).toBe("users");
|
|
116
|
-
expect(result.primaryKeys).toEqual([{ dbColumn: "id",
|
|
117
|
-
resultColumn: "id" }]);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it("resolves aliased FROM", () => {
|
|
121
|
-
const sql = "SELECT u.email FROM users u";
|
|
122
|
-
const result = determineTableAndPK(sql, "email", mockSchemas);
|
|
123
|
-
expect(result.tableName).toBe("users");
|
|
124
|
-
expect(result.primaryKeys).toEqual([{ dbColumn: "id",
|
|
125
|
-
resultColumn: "id" }]);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it("resolves simple unambiguous JOIN", () => {
|
|
129
|
-
const sql = "SELECT * FROM users u JOIN roles r ON u.role_id = r.id";
|
|
130
|
-
const result = determineTableAndPK(sql, "email", mockSchemas);
|
|
131
|
-
expect(result.tableName).toBe("users");
|
|
132
|
-
expect(result.primaryKeys).toEqual([{ dbColumn: "id",
|
|
133
|
-
resultColumn: "id" }]);
|
|
134
|
-
|
|
135
|
-
const result2 = determineTableAndPK(sql, "role_name", mockSchemas);
|
|
136
|
-
expect(result2.tableName).toBe("roles");
|
|
137
|
-
expect(result2.primaryKeys).toEqual([{ dbColumn: "id",
|
|
138
|
-
resultColumn: "id" }]);
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it("resolves aliased PK columns in JOINs", () => {
|
|
142
|
-
const sql = "SELECT u.id AS user_id, u.email, r.id AS role_id, r.role_name FROM users u JOIN roles r ON u.role_id = r.id";
|
|
143
|
-
const result = determineTableAndPK(sql, "email", mockSchemas);
|
|
144
|
-
expect(result.tableName).toBe("users");
|
|
145
|
-
expect(result.primaryKeys).toEqual([{ dbColumn: "id",
|
|
146
|
-
resultColumn: "user_id" }]);
|
|
147
|
-
|
|
148
|
-
const result2 = determineTableAndPK(sql, "role_name", mockSchemas);
|
|
149
|
-
expect(result2.tableName).toBe("roles");
|
|
150
|
-
expect(result2.primaryKeys).toEqual([{ dbColumn: "id",
|
|
151
|
-
resultColumn: "role_id" }]);
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it("resolves aliased edit columns back to DB names", () => {
|
|
155
|
-
const sql = "SELECT u.email AS user_email FROM users u";
|
|
156
|
-
const result = determineTableAndPK(sql, "user_email", mockSchemas);
|
|
157
|
-
expect(result.tableName).toBe("users");
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it("returns error for ambiguous JOIN columns", () => {
|
|
161
|
-
const sql = "SELECT * FROM users u JOIN roles r ON u.role_id = r.id";
|
|
162
|
-
// Both tables have created_at
|
|
163
|
-
const result = determineTableAndPK(sql, "created_at", mockSchemas);
|
|
164
|
-
expect(result.error).toContain("Ambiguous column");
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it("returns error for unsupported non-SELECT queries", () => {
|
|
168
|
-
const sql = "DELETE FROM users WHERE id = 1";
|
|
169
|
-
const result = determineTableAndPK(sql, "email", mockSchemas);
|
|
170
|
-
expect(result.error).toContain("only supported for SELECT");
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it("returns error for tables without primary keys", () => {
|
|
174
|
-
const sql = "SELECT * FROM settings";
|
|
175
|
-
const result = determineTableAndPK(sql, "key", mockSchemas);
|
|
176
|
-
expect(result.error).toContain("has no primary key defined");
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it("supports composite primary keys", () => {
|
|
180
|
-
const sql = "SELECT * FROM order_items";
|
|
181
|
-
const result = determineTableAndPK(sql, "quantity", mockSchemas);
|
|
182
|
-
expect(result.tableName).toBe("order_items");
|
|
183
|
-
expect(result.primaryKeys).toHaveLength(2);
|
|
184
|
-
expect(result.primaryKeys).toEqual([
|
|
185
|
-
{ dbColumn: "order_id",
|
|
186
|
-
resultColumn: "order_id" },
|
|
187
|
-
{ dbColumn: "item_id",
|
|
188
|
-
resultColumn: "item_id" }
|
|
189
|
-
]);
|
|
190
|
-
});
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
describe("extractTablesFromQuery", () => {
|
|
194
|
-
it("extracts a single table", () => {
|
|
195
|
-
const tables = extractTablesFromQuery("SELECT * FROM users");
|
|
196
|
-
expect(tables).toEqual([{ name: "users",
|
|
197
|
-
alias: undefined }]);
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
it("extracts aliased tables", () => {
|
|
201
|
-
const tables = extractTablesFromQuery("SELECT u.email FROM users u");
|
|
202
|
-
expect(tables).toEqual([{ name: "users",
|
|
203
|
-
alias: "u" }]);
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it("extracts multiple tables from JOINs", () => {
|
|
207
|
-
const tables = extractTablesFromQuery("SELECT * FROM users u JOIN roles r ON u.role_id = r.id");
|
|
208
|
-
expect(tables).toHaveLength(2);
|
|
209
|
-
expect(tables[0].name).toBe("users");
|
|
210
|
-
expect(tables[1].name).toBe("roles");
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
it("returns empty for non-SELECT queries", () => {
|
|
214
|
-
expect(extractTablesFromQuery("DELETE FROM users")).toEqual([]);
|
|
215
|
-
expect(extractTablesFromQuery("INSERT INTO users (id) VALUES (1)")).toEqual([]);
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
it("returns empty for invalid SQL", () => {
|
|
219
|
-
expect(extractTablesFromQuery("NOT VALID SQL AT ALL ???")).toEqual([]);
|
|
220
|
-
});
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
describe("resolveQueryCollections", () => {
|
|
224
|
-
it("matches a single admin collection", () => {
|
|
225
|
-
const result = resolveQueryCollections("SELECT * FROM users", mockSchemas, mockCollections);
|
|
226
|
-
expect(result).toHaveLength(1);
|
|
227
|
-
expect(result[0].tableName).toBe("users");
|
|
228
|
-
expect(result[0].collection.name).toBe("Users");
|
|
229
|
-
expect(result[0].columns).toContain("id");
|
|
230
|
-
expect(result[0].columns).toContain("email");
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
it("matches multiple admin collections in a JOIN", () => {
|
|
234
|
-
const result = resolveQueryCollections(
|
|
235
|
-
"SELECT * FROM users u JOIN roles r ON u.role_id = r.id",
|
|
236
|
-
mockSchemas,
|
|
237
|
-
mockCollections
|
|
238
|
-
);
|
|
239
|
-
expect(result).toHaveLength(2);
|
|
240
|
-
expect(result.map(r => r.collection.name).sort()).toEqual(["Roles", "Users"]);
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
it("returns empty when no tables match any collection", () => {
|
|
244
|
-
const result = resolveQueryCollections("SELECT * FROM settings", mockSchemas, mockCollections);
|
|
245
|
-
expect(result).toHaveLength(0);
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
it("matches collection by table (not slug)", () => {
|
|
249
|
-
const result = resolveQueryCollections("SELECT * FROM blog_posts", mockSchemas, mockCollections);
|
|
250
|
-
expect(result).toHaveLength(1);
|
|
251
|
-
expect(result[0].collection.slug).toBe("blogPosts");
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
it("returns empty for non-SELECT queries", () => {
|
|
255
|
-
const result = resolveQueryCollections("DELETE FROM users WHERE id = 1", mockSchemas, mockCollections);
|
|
256
|
-
expect(result).toHaveLength(0);
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
it("preserves table alias information", () => {
|
|
260
|
-
const result = resolveQueryCollections("SELECT u.* FROM users u", mockSchemas, mockCollections);
|
|
261
|
-
expect(result).toHaveLength(1);
|
|
262
|
-
expect(result[0].tableAlias).toBe("u");
|
|
263
|
-
});
|
|
264
|
-
});
|
|
265
|
-
|