@velajs/studio-protocol 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +21 -0
- package/dist/index.d.ts +1285 -0
- package/dist/index.js +775 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,775 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
//#region src/errors.ts
|
|
3
|
+
/**
|
|
4
|
+
* The closed set of Studio-specific error codes. `const` array + derived union
|
|
5
|
+
* so the compile-time type and any runtime membership check can never drift.
|
|
6
|
+
* Parenthetical statuses document the intended HTTP mapping (the wire carries
|
|
7
|
+
* the concrete status on {@link AdminErrorBody}).
|
|
8
|
+
*/
|
|
9
|
+
const STUDIO_ERROR_CODES = [
|
|
10
|
+
"STUDIO_DISABLED",
|
|
11
|
+
"STUDIO_UNAUTHORIZED",
|
|
12
|
+
"STUDIO_UNKNOWN_OP",
|
|
13
|
+
"STUDIO_OP_FORBIDDEN",
|
|
14
|
+
"STUDIO_SUB_TOKEN_INVALID",
|
|
15
|
+
"STUDIO_RATE_LIMITED",
|
|
16
|
+
"STUDIO_CONFIRM_REQUIRED",
|
|
17
|
+
"STUDIO_UNKNOWN_MODEL",
|
|
18
|
+
"DATA_EDIT_DISABLED",
|
|
19
|
+
"TIMETRAVEL_UNAVAILABLE",
|
|
20
|
+
"TIMETRAVEL_SCHEMA_MISMATCH",
|
|
21
|
+
"FEATURE_UNCONFIGURED"
|
|
22
|
+
];
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/capabilities.ts
|
|
25
|
+
/**
|
|
26
|
+
* The closed set of feature keys the UI navigates by. `const` array + derived
|
|
27
|
+
* union so a feature panel can light up iff the app actually wired that package.
|
|
28
|
+
*/
|
|
29
|
+
const STUDIO_FEATURE_KEYS = [
|
|
30
|
+
"app",
|
|
31
|
+
"openapi",
|
|
32
|
+
"data",
|
|
33
|
+
"timeTravel",
|
|
34
|
+
"transfer",
|
|
35
|
+
"auth",
|
|
36
|
+
"authOrganizations",
|
|
37
|
+
"queue",
|
|
38
|
+
"schedule",
|
|
39
|
+
"flags",
|
|
40
|
+
"logs",
|
|
41
|
+
"audit",
|
|
42
|
+
"live",
|
|
43
|
+
"presence"
|
|
44
|
+
];
|
|
45
|
+
//#endregion
|
|
46
|
+
//#region src/data.ts
|
|
47
|
+
/**
|
|
48
|
+
* Data-browser wire contract. Model discovery + row access shapes.
|
|
49
|
+
*
|
|
50
|
+
* `StudioFilterOperator` is a compatible subset of `@velajs/crud`'s
|
|
51
|
+
* `FilterOperator` (`crud/packages/core/src/adapter/query-types.ts`), and
|
|
52
|
+
* `StudioPageInfo` is a structural mirror of crud's `PageInfo` (snake_case
|
|
53
|
+
* preserved). Drift guards against the real crud package land in the server
|
|
54
|
+
* package; see the report for mirror source paths.
|
|
55
|
+
*/
|
|
56
|
+
/**
|
|
57
|
+
* Grid filter operators — a subset of crud's `FILTER_OPERATORS`, chosen so every
|
|
58
|
+
* member is assignment-compatible with crud's `FilterOperator`. `const` array +
|
|
59
|
+
* derived union so compile-time type and runtime membership can't drift.
|
|
60
|
+
*/
|
|
61
|
+
const STUDIO_FILTER_OPERATORS = [
|
|
62
|
+
"eq",
|
|
63
|
+
"ne",
|
|
64
|
+
"gt",
|
|
65
|
+
"gte",
|
|
66
|
+
"lt",
|
|
67
|
+
"lte",
|
|
68
|
+
"in",
|
|
69
|
+
"nin",
|
|
70
|
+
"like",
|
|
71
|
+
"ilike",
|
|
72
|
+
"null",
|
|
73
|
+
"between"
|
|
74
|
+
];
|
|
75
|
+
//#endregion
|
|
76
|
+
//#region src/connection.ts
|
|
77
|
+
function isRecord(value) {
|
|
78
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
79
|
+
}
|
|
80
|
+
function isLocalPath(value) {
|
|
81
|
+
return typeof value === "string" && value.startsWith("/") && !value.startsWith("//") && !/[\\\\?#\s]/.test(value);
|
|
82
|
+
}
|
|
83
|
+
function parseStudioConnection(value) {
|
|
84
|
+
if (!isRecord(value) || value.protocolVersion !== 2 || !isLocalPath(value.routerBasePath) || !isLocalPath(value.adminBasePath) || !isLocalPath(value.apiRequestPath) || typeof value.sessionToken !== "string" || value.sessionToken.length === 0) throw new Error("Invalid Studio connection configuration.");
|
|
85
|
+
return {
|
|
86
|
+
protocolVersion: value.protocolVersion,
|
|
87
|
+
routerBasePath: value.routerBasePath,
|
|
88
|
+
adminBasePath: value.adminBasePath,
|
|
89
|
+
apiRequestPath: value.apiRequestPath,
|
|
90
|
+
sessionToken: value.sessionToken
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region src/app.ts
|
|
95
|
+
function stringMap(input) {
|
|
96
|
+
if (input === void 0) return void 0;
|
|
97
|
+
if (!isRecord(input)) throw new Error("Expected string-valued headers or query parameters.");
|
|
98
|
+
const result = {};
|
|
99
|
+
for (const [key, item] of Object.entries(input)) {
|
|
100
|
+
if (typeof item !== "string") throw new Error("Expected string-valued headers or query parameters.");
|
|
101
|
+
result[key] = item;
|
|
102
|
+
}
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
/** Validate JSON from the local API explorer before any request can be sent. */
|
|
106
|
+
function parseTryItRequest(value) {
|
|
107
|
+
if (!isRecord(value) || typeof value.method !== "string" || !/^(GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS)$/i.test(value.method) || typeof value.path !== "string" || !value.path.startsWith("/") || value.path.startsWith("//") || /[\\\\#\s{}]/.test(value.path)) throw new Error("Expected an HTTP method and a resolved absolute path on the Worker.");
|
|
108
|
+
return {
|
|
109
|
+
method: value.method.toUpperCase(),
|
|
110
|
+
path: value.path,
|
|
111
|
+
query: stringMap(value.query),
|
|
112
|
+
headers: stringMap(value.headers),
|
|
113
|
+
body: value.body
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function parseTryItResponse(value) {
|
|
117
|
+
if (!isRecord(value) || typeof value.status !== "number" || !Number.isInteger(value.status) || value.status < 100 || value.status > 599 || !isRecord(value.headers) || !("body" in value)) throw new Error("Invalid API response.");
|
|
118
|
+
const headers = {};
|
|
119
|
+
for (const [key, item] of Object.entries(value.headers)) {
|
|
120
|
+
if (typeof item !== "string") throw new Error("Invalid API response headers.");
|
|
121
|
+
headers[key] = item;
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
status: value.status,
|
|
125
|
+
headers,
|
|
126
|
+
body: value.body
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
//#endregion
|
|
130
|
+
//#region src/ops.ts
|
|
131
|
+
/**
|
|
132
|
+
* Op meta for every op. `as const satisfies` enforces total key coverage while
|
|
133
|
+
* preserving literal types (so `destructive` narrows exactly), which drives the
|
|
134
|
+
* type-level destructive-confirm guard in the tests.
|
|
135
|
+
*/
|
|
136
|
+
const STUDIO_OP_META = {
|
|
137
|
+
"studio.capabilities": {
|
|
138
|
+
mode: "read",
|
|
139
|
+
feature: "app"
|
|
140
|
+
},
|
|
141
|
+
"app.routes": {
|
|
142
|
+
mode: "read",
|
|
143
|
+
feature: "app"
|
|
144
|
+
},
|
|
145
|
+
"app.modules": {
|
|
146
|
+
mode: "read",
|
|
147
|
+
feature: "app"
|
|
148
|
+
},
|
|
149
|
+
"app.entrypoints": {
|
|
150
|
+
mode: "read",
|
|
151
|
+
feature: "app"
|
|
152
|
+
},
|
|
153
|
+
"app.openapi": {
|
|
154
|
+
mode: "read",
|
|
155
|
+
feature: "openapi"
|
|
156
|
+
},
|
|
157
|
+
/** Authorizes a host HTTP request; server gates and audits this operational action. */
|
|
158
|
+
"api.authorizeTryIt": {
|
|
159
|
+
mode: "write",
|
|
160
|
+
feature: "openapi",
|
|
161
|
+
gate: "opsEditable"
|
|
162
|
+
},
|
|
163
|
+
"data.listModels": {
|
|
164
|
+
mode: "read",
|
|
165
|
+
feature: "data"
|
|
166
|
+
},
|
|
167
|
+
"data.describeModel": {
|
|
168
|
+
mode: "read",
|
|
169
|
+
feature: "data"
|
|
170
|
+
},
|
|
171
|
+
"data.listRows": {
|
|
172
|
+
mode: "read",
|
|
173
|
+
feature: "data"
|
|
174
|
+
},
|
|
175
|
+
"data.readRow": {
|
|
176
|
+
mode: "read",
|
|
177
|
+
feature: "data"
|
|
178
|
+
},
|
|
179
|
+
"data.facets": {
|
|
180
|
+
mode: "read",
|
|
181
|
+
feature: "data"
|
|
182
|
+
},
|
|
183
|
+
"data.cascadePreview": {
|
|
184
|
+
mode: "read",
|
|
185
|
+
feature: "data"
|
|
186
|
+
},
|
|
187
|
+
"data.writeRow": {
|
|
188
|
+
mode: "write",
|
|
189
|
+
feature: "data",
|
|
190
|
+
gate: "dataEditable"
|
|
191
|
+
},
|
|
192
|
+
"data.deleteRows": {
|
|
193
|
+
mode: "write",
|
|
194
|
+
feature: "data",
|
|
195
|
+
gate: "dataEditable",
|
|
196
|
+
destructive: true
|
|
197
|
+
},
|
|
198
|
+
"data.clearTable": {
|
|
199
|
+
mode: "write",
|
|
200
|
+
feature: "data",
|
|
201
|
+
gate: "dataEditable",
|
|
202
|
+
destructive: true
|
|
203
|
+
},
|
|
204
|
+
"data.generateRows": {
|
|
205
|
+
mode: "write",
|
|
206
|
+
feature: "data",
|
|
207
|
+
gate: "dataEditable"
|
|
208
|
+
},
|
|
209
|
+
"timeTravel.capabilities": {
|
|
210
|
+
mode: "read",
|
|
211
|
+
feature: "timeTravel"
|
|
212
|
+
},
|
|
213
|
+
"timeTravel.currentMark": {
|
|
214
|
+
mode: "read",
|
|
215
|
+
feature: "timeTravel"
|
|
216
|
+
},
|
|
217
|
+
"timeTravel.markForTime": {
|
|
218
|
+
mode: "read",
|
|
219
|
+
feature: "timeTravel"
|
|
220
|
+
},
|
|
221
|
+
"timeTravel.listMarks": {
|
|
222
|
+
mode: "read",
|
|
223
|
+
feature: "timeTravel"
|
|
224
|
+
},
|
|
225
|
+
"timeTravel.preview": {
|
|
226
|
+
mode: "read",
|
|
227
|
+
feature: "timeTravel"
|
|
228
|
+
},
|
|
229
|
+
"timeTravel.armRestore": {
|
|
230
|
+
mode: "write",
|
|
231
|
+
feature: "timeTravel",
|
|
232
|
+
gate: "timeTravelRestore",
|
|
233
|
+
destructive: true
|
|
234
|
+
},
|
|
235
|
+
"timeTravel.undo": {
|
|
236
|
+
mode: "write",
|
|
237
|
+
feature: "timeTravel",
|
|
238
|
+
gate: "timeTravelRestore",
|
|
239
|
+
destructive: true
|
|
240
|
+
},
|
|
241
|
+
"timeTravel.createSnapshot": {
|
|
242
|
+
mode: "write",
|
|
243
|
+
feature: "timeTravel"
|
|
244
|
+
},
|
|
245
|
+
"timeTravel.prune": {
|
|
246
|
+
mode: "write",
|
|
247
|
+
feature: "timeTravel",
|
|
248
|
+
gate: "timeTravelRestore",
|
|
249
|
+
destructive: true
|
|
250
|
+
},
|
|
251
|
+
"transfer.export": {
|
|
252
|
+
mode: "read",
|
|
253
|
+
feature: "transfer"
|
|
254
|
+
},
|
|
255
|
+
"transfer.import": {
|
|
256
|
+
mode: "write",
|
|
257
|
+
feature: "transfer",
|
|
258
|
+
gate: "transferImport",
|
|
259
|
+
destructive: true
|
|
260
|
+
},
|
|
261
|
+
"auth.users": {
|
|
262
|
+
mode: "read",
|
|
263
|
+
feature: "auth"
|
|
264
|
+
},
|
|
265
|
+
"auth.userDetail": {
|
|
266
|
+
mode: "read",
|
|
267
|
+
feature: "auth"
|
|
268
|
+
},
|
|
269
|
+
"auth.sessions": {
|
|
270
|
+
mode: "read",
|
|
271
|
+
feature: "auth"
|
|
272
|
+
},
|
|
273
|
+
"auth.revokeSession": {
|
|
274
|
+
mode: "write",
|
|
275
|
+
feature: "auth",
|
|
276
|
+
gate: "opsEditable"
|
|
277
|
+
},
|
|
278
|
+
"auth.organizations": {
|
|
279
|
+
mode: "read",
|
|
280
|
+
feature: "authOrganizations"
|
|
281
|
+
},
|
|
282
|
+
"queue.list": {
|
|
283
|
+
mode: "read",
|
|
284
|
+
feature: "queue"
|
|
285
|
+
},
|
|
286
|
+
"queue.depths": {
|
|
287
|
+
mode: "read",
|
|
288
|
+
feature: "queue"
|
|
289
|
+
},
|
|
290
|
+
"queue.dlq": {
|
|
291
|
+
mode: "read",
|
|
292
|
+
feature: "queue"
|
|
293
|
+
},
|
|
294
|
+
"queue.send": {
|
|
295
|
+
mode: "write",
|
|
296
|
+
feature: "queue",
|
|
297
|
+
gate: "opsEditable"
|
|
298
|
+
},
|
|
299
|
+
"queue.replay": {
|
|
300
|
+
mode: "write",
|
|
301
|
+
feature: "queue",
|
|
302
|
+
gate: "opsEditable"
|
|
303
|
+
},
|
|
304
|
+
"schedule.jobs": {
|
|
305
|
+
mode: "read",
|
|
306
|
+
feature: "schedule"
|
|
307
|
+
},
|
|
308
|
+
"schedule.triggers": {
|
|
309
|
+
mode: "read",
|
|
310
|
+
feature: "schedule"
|
|
311
|
+
},
|
|
312
|
+
"schedule.runNow": {
|
|
313
|
+
mode: "write",
|
|
314
|
+
feature: "schedule",
|
|
315
|
+
gate: "opsEditable"
|
|
316
|
+
},
|
|
317
|
+
"flags.list": {
|
|
318
|
+
mode: "read",
|
|
319
|
+
feature: "flags"
|
|
320
|
+
},
|
|
321
|
+
"flags.evaluate": {
|
|
322
|
+
mode: "read",
|
|
323
|
+
feature: "flags"
|
|
324
|
+
},
|
|
325
|
+
"logs.tail": {
|
|
326
|
+
mode: "read",
|
|
327
|
+
feature: "logs"
|
|
328
|
+
},
|
|
329
|
+
"live.subscriptions": {
|
|
330
|
+
mode: "read",
|
|
331
|
+
feature: "live"
|
|
332
|
+
},
|
|
333
|
+
"presence.rooms": {
|
|
334
|
+
mode: "read",
|
|
335
|
+
feature: "presence"
|
|
336
|
+
},
|
|
337
|
+
"audit.tail": {
|
|
338
|
+
mode: "read",
|
|
339
|
+
feature: "audit"
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
/**
|
|
343
|
+
* Every op name as a runtime const array. Kept as a literal tuple (via
|
|
344
|
+
* `as const satisfies`) so the exhaustiveness drift guard can compare its
|
|
345
|
+
* element union against `keyof StudioRpcMap` in both directions.
|
|
346
|
+
*/
|
|
347
|
+
const STUDIO_OPS = [
|
|
348
|
+
"studio.capabilities",
|
|
349
|
+
"app.routes",
|
|
350
|
+
"app.modules",
|
|
351
|
+
"app.entrypoints",
|
|
352
|
+
"app.openapi",
|
|
353
|
+
"api.authorizeTryIt",
|
|
354
|
+
"data.listModels",
|
|
355
|
+
"data.describeModel",
|
|
356
|
+
"data.listRows",
|
|
357
|
+
"data.readRow",
|
|
358
|
+
"data.facets",
|
|
359
|
+
"data.cascadePreview",
|
|
360
|
+
"data.writeRow",
|
|
361
|
+
"data.deleteRows",
|
|
362
|
+
"data.clearTable",
|
|
363
|
+
"data.generateRows",
|
|
364
|
+
"timeTravel.capabilities",
|
|
365
|
+
"timeTravel.currentMark",
|
|
366
|
+
"timeTravel.markForTime",
|
|
367
|
+
"timeTravel.listMarks",
|
|
368
|
+
"timeTravel.preview",
|
|
369
|
+
"timeTravel.armRestore",
|
|
370
|
+
"timeTravel.undo",
|
|
371
|
+
"timeTravel.createSnapshot",
|
|
372
|
+
"timeTravel.prune",
|
|
373
|
+
"transfer.export",
|
|
374
|
+
"transfer.import",
|
|
375
|
+
"auth.users",
|
|
376
|
+
"auth.userDetail",
|
|
377
|
+
"auth.sessions",
|
|
378
|
+
"auth.revokeSession",
|
|
379
|
+
"auth.organizations",
|
|
380
|
+
"queue.list",
|
|
381
|
+
"queue.depths",
|
|
382
|
+
"queue.dlq",
|
|
383
|
+
"queue.send",
|
|
384
|
+
"queue.replay",
|
|
385
|
+
"schedule.jobs",
|
|
386
|
+
"schedule.triggers",
|
|
387
|
+
"schedule.runNow",
|
|
388
|
+
"flags.list",
|
|
389
|
+
"flags.evaluate",
|
|
390
|
+
"logs.tail",
|
|
391
|
+
"live.subscriptions",
|
|
392
|
+
"presence.rooms",
|
|
393
|
+
"audit.tail"
|
|
394
|
+
];
|
|
395
|
+
//#endregion
|
|
396
|
+
//#region src/http.ts
|
|
397
|
+
/**
|
|
398
|
+
* Transport constants shared by the server module, the UI, and the dev host.
|
|
399
|
+
* String literals + one integer version marker; zero runtime dependencies.
|
|
400
|
+
*/
|
|
401
|
+
/** Default reserved prefix the admin surface mounts under. */
|
|
402
|
+
const STUDIO_DEFAULT_PATH = "/_vela/admin";
|
|
403
|
+
/** Unauthenticated health probe suffix (`GET {prefix}/health`). */
|
|
404
|
+
const STUDIO_HEALTH_SUFFIX = "/health";
|
|
405
|
+
/** RPC dispatch suffix (`POST {prefix}/rpc/:op`). */
|
|
406
|
+
const STUDIO_RPC_SUFFIX = "/rpc/";
|
|
407
|
+
/** Ephemeral WS sub-token mint suffix (`POST {prefix}/ws-token`). */
|
|
408
|
+
const STUDIO_WS_TOKEN_SUFFIX = "/ws-token";
|
|
409
|
+
/** Snapshot/transfer export suffix (`GET {prefix}/export`). */
|
|
410
|
+
const STUDIO_EXPORT_SUFFIX = "/export";
|
|
411
|
+
/** The header carrying the master bearer token (`Authorization: Bearer <token>`). */
|
|
412
|
+
const STUDIO_TOKEN_HEADER = "authorization";
|
|
413
|
+
/** The wire protocol version. Bumped only on a breaking envelope change. */
|
|
414
|
+
const STUDIO_PROTOCOL_VERSION = 2;
|
|
415
|
+
//#endregion
|
|
416
|
+
//#region src/responses.ts
|
|
417
|
+
const row = z.record(z.string(), z.unknown());
|
|
418
|
+
const strings = z.array(z.string());
|
|
419
|
+
const timeTravelCapabilities = z.object({
|
|
420
|
+
markByTime: z.boolean(),
|
|
421
|
+
list: z.boolean(),
|
|
422
|
+
undo: z.boolean(),
|
|
423
|
+
inPlace: z.boolean(),
|
|
424
|
+
restartRequired: z.boolean(),
|
|
425
|
+
portableExport: z.boolean(),
|
|
426
|
+
createOnDemand: z.boolean(),
|
|
427
|
+
granularity: z.enum([
|
|
428
|
+
"bookmark",
|
|
429
|
+
"snapshot",
|
|
430
|
+
"snapshot+cdc"
|
|
431
|
+
]),
|
|
432
|
+
scopeNote: z.string()
|
|
433
|
+
});
|
|
434
|
+
const mark = z.object({
|
|
435
|
+
id: z.string(),
|
|
436
|
+
kind: z.enum(["bookmark", "snapshot"]),
|
|
437
|
+
time: z.number().optional(),
|
|
438
|
+
label: z.string().optional(),
|
|
439
|
+
schemaHash: z.string().optional(),
|
|
440
|
+
sizeBytes: z.number().optional(),
|
|
441
|
+
tables: strings.optional()
|
|
442
|
+
});
|
|
443
|
+
const restoreOutcome = z.object({
|
|
444
|
+
restoredTo: z.string(),
|
|
445
|
+
undoMark: mark.optional(),
|
|
446
|
+
applied: z.boolean(),
|
|
447
|
+
restartRequested: z.boolean()
|
|
448
|
+
});
|
|
449
|
+
const user = z.object({
|
|
450
|
+
id: z.string(),
|
|
451
|
+
email: z.string(),
|
|
452
|
+
name: z.string().optional(),
|
|
453
|
+
emailVerified: z.boolean(),
|
|
454
|
+
image: z.string().optional(),
|
|
455
|
+
role: z.string().optional(),
|
|
456
|
+
banned: z.boolean().optional(),
|
|
457
|
+
createdAt: z.number()
|
|
458
|
+
});
|
|
459
|
+
const session = z.object({
|
|
460
|
+
id: z.string(),
|
|
461
|
+
userId: z.string(),
|
|
462
|
+
createdAt: z.number(),
|
|
463
|
+
expiresAt: z.number(),
|
|
464
|
+
ipAddress: z.string().optional(),
|
|
465
|
+
userAgent: z.string().optional()
|
|
466
|
+
});
|
|
467
|
+
const organization = z.object({
|
|
468
|
+
id: z.string(),
|
|
469
|
+
name: z.string(),
|
|
470
|
+
slug: z.string().optional(),
|
|
471
|
+
memberCount: z.number().optional(),
|
|
472
|
+
createdAt: z.number()
|
|
473
|
+
});
|
|
474
|
+
const flagValue = z.union([
|
|
475
|
+
z.boolean(),
|
|
476
|
+
z.string(),
|
|
477
|
+
z.number(),
|
|
478
|
+
row,
|
|
479
|
+
z.array(z.unknown())
|
|
480
|
+
]);
|
|
481
|
+
const deleted = z.object({ deleted: z.number() });
|
|
482
|
+
const acknowledged = z.object({ ok: z.literal(true) });
|
|
483
|
+
/** Every operation must supply a concrete validator for its declared output. */
|
|
484
|
+
const STUDIO_RESPONSE_PARSERS = {
|
|
485
|
+
"studio.capabilities": z.object({
|
|
486
|
+
operations: z.array(z.enum(STUDIO_OPS)),
|
|
487
|
+
features: z.record(z.enum(STUDIO_FEATURE_KEYS), z.boolean()),
|
|
488
|
+
writes: z.object({
|
|
489
|
+
dataEditable: z.boolean(),
|
|
490
|
+
schemaEditable: z.boolean(),
|
|
491
|
+
opsEditable: z.boolean(),
|
|
492
|
+
runAsIdentity: z.boolean(),
|
|
493
|
+
timeTravelRestore: z.boolean(),
|
|
494
|
+
transferImport: z.boolean()
|
|
495
|
+
}),
|
|
496
|
+
timeTravel: timeTravelCapabilities.nullable()
|
|
497
|
+
}).parse,
|
|
498
|
+
"app.routes": z.array(z.object({
|
|
499
|
+
method: z.string(),
|
|
500
|
+
path: z.string(),
|
|
501
|
+
handler: z.string(),
|
|
502
|
+
source: z.enum(["controller", "mounted"])
|
|
503
|
+
})).parse,
|
|
504
|
+
"app.modules": z.array(z.object({
|
|
505
|
+
moduleId: z.string(),
|
|
506
|
+
imports: strings,
|
|
507
|
+
isGlobal: z.boolean(),
|
|
508
|
+
lazy: z.boolean(),
|
|
509
|
+
providers: strings,
|
|
510
|
+
exports: strings
|
|
511
|
+
})).parse,
|
|
512
|
+
"app.entrypoints": z.array(z.object({
|
|
513
|
+
kind: z.string(),
|
|
514
|
+
target: z.string(),
|
|
515
|
+
meta: z.unknown().optional()
|
|
516
|
+
})).parse,
|
|
517
|
+
"app.openapi": z.unknown().parse,
|
|
518
|
+
"api.authorizeTryIt": z.object({ authorized: z.literal(true) }).parse,
|
|
519
|
+
"data.listModels": z.array(z.object({
|
|
520
|
+
name: z.string(),
|
|
521
|
+
table: z.string(),
|
|
522
|
+
label: z.string(),
|
|
523
|
+
capabilities: strings
|
|
524
|
+
})).parse,
|
|
525
|
+
"data.describeModel": z.object({
|
|
526
|
+
name: z.string(),
|
|
527
|
+
table: z.string(),
|
|
528
|
+
primaryKeys: strings,
|
|
529
|
+
columns: z.array(z.object({
|
|
530
|
+
name: z.string(),
|
|
531
|
+
type: z.enum([
|
|
532
|
+
"string",
|
|
533
|
+
"number",
|
|
534
|
+
"boolean",
|
|
535
|
+
"date",
|
|
536
|
+
"json",
|
|
537
|
+
"unknown"
|
|
538
|
+
]),
|
|
539
|
+
pk: z.boolean(),
|
|
540
|
+
nullable: z.boolean(),
|
|
541
|
+
unique: z.boolean(),
|
|
542
|
+
managed: z.boolean(),
|
|
543
|
+
fk: z.object({
|
|
544
|
+
table: z.string(),
|
|
545
|
+
relation: z.string()
|
|
546
|
+
}).optional()
|
|
547
|
+
})),
|
|
548
|
+
relations: z.array(z.object({
|
|
549
|
+
name: z.string(),
|
|
550
|
+
type: z.enum([
|
|
551
|
+
"hasOne",
|
|
552
|
+
"hasMany",
|
|
553
|
+
"belongsTo"
|
|
554
|
+
]),
|
|
555
|
+
target: z.string(),
|
|
556
|
+
foreignKey: z.string(),
|
|
557
|
+
cascade: z.string().optional()
|
|
558
|
+
})),
|
|
559
|
+
flags: z.object({
|
|
560
|
+
softDelete: z.boolean(),
|
|
561
|
+
multiTenant: z.boolean(),
|
|
562
|
+
versioning: z.boolean(),
|
|
563
|
+
audit: z.boolean()
|
|
564
|
+
}),
|
|
565
|
+
supports: z.object({
|
|
566
|
+
bulkWrites: z.boolean(),
|
|
567
|
+
facets: z.boolean(),
|
|
568
|
+
search: z.boolean(),
|
|
569
|
+
cascade: z.boolean()
|
|
570
|
+
})
|
|
571
|
+
}).parse,
|
|
572
|
+
"data.listRows": z.object({
|
|
573
|
+
rows: z.array(row),
|
|
574
|
+
info: z.object({
|
|
575
|
+
page: z.number(),
|
|
576
|
+
per_page: z.number(),
|
|
577
|
+
total_count: z.number().optional(),
|
|
578
|
+
total_pages: z.number().optional(),
|
|
579
|
+
has_next_page: z.boolean(),
|
|
580
|
+
has_prev_page: z.boolean(),
|
|
581
|
+
next_cursor: z.string().optional()
|
|
582
|
+
})
|
|
583
|
+
}).parse,
|
|
584
|
+
"data.readRow": row.nullable().parse,
|
|
585
|
+
"data.facets": z.object({ buckets: z.array(z.object({
|
|
586
|
+
value: z.unknown(),
|
|
587
|
+
count: z.number()
|
|
588
|
+
})) }).parse,
|
|
589
|
+
"data.cascadePreview": z.object({ relations: z.array(z.object({
|
|
590
|
+
relation: z.string(),
|
|
591
|
+
target: z.string(),
|
|
592
|
+
action: z.string(),
|
|
593
|
+
affected: z.number()
|
|
594
|
+
})) }).parse,
|
|
595
|
+
"data.writeRow": row.parse,
|
|
596
|
+
"data.deleteRows": deleted.parse,
|
|
597
|
+
"data.clearTable": deleted.parse,
|
|
598
|
+
"data.generateRows": z.object({ inserted: z.number() }).parse,
|
|
599
|
+
"timeTravel.capabilities": timeTravelCapabilities.parse,
|
|
600
|
+
"timeTravel.currentMark": mark.parse,
|
|
601
|
+
"timeTravel.markForTime": mark.nullable().parse,
|
|
602
|
+
"timeTravel.listMarks": z.object({
|
|
603
|
+
marks: z.array(mark),
|
|
604
|
+
nextCursor: z.string().optional()
|
|
605
|
+
}).parse,
|
|
606
|
+
"timeTravel.preview": z.object({
|
|
607
|
+
target: mark,
|
|
608
|
+
affectedTables: z.array(z.object({
|
|
609
|
+
table: z.string(),
|
|
610
|
+
approxRows: z.number().optional()
|
|
611
|
+
})),
|
|
612
|
+
schemaCompatible: z.boolean(),
|
|
613
|
+
incompatibleTables: strings,
|
|
614
|
+
undoAvailable: z.boolean(),
|
|
615
|
+
restartRequired: z.boolean(),
|
|
616
|
+
confirmToken: z.string(),
|
|
617
|
+
expiresAt: z.number()
|
|
618
|
+
}).parse,
|
|
619
|
+
"timeTravel.armRestore": restoreOutcome.parse,
|
|
620
|
+
"timeTravel.undo": restoreOutcome.parse,
|
|
621
|
+
"timeTravel.createSnapshot": mark.parse,
|
|
622
|
+
"timeTravel.prune": z.object({ pruned: z.number() }).parse,
|
|
623
|
+
"transfer.export": z.object({ exportUrl: z.string() }).parse,
|
|
624
|
+
"transfer.import": z.object({
|
|
625
|
+
imported: z.number(),
|
|
626
|
+
errors: z.array(z.object({
|
|
627
|
+
line: z.number(),
|
|
628
|
+
message: z.string()
|
|
629
|
+
}))
|
|
630
|
+
}).parse,
|
|
631
|
+
"auth.users": z.object({
|
|
632
|
+
rows: z.array(user),
|
|
633
|
+
nextCursor: z.string().optional()
|
|
634
|
+
}).parse,
|
|
635
|
+
"auth.userDetail": z.object({
|
|
636
|
+
user,
|
|
637
|
+
sessions: z.array(session),
|
|
638
|
+
organizations: z.array(organization)
|
|
639
|
+
}).parse,
|
|
640
|
+
"auth.sessions": z.array(session).parse,
|
|
641
|
+
"auth.revokeSession": acknowledged.parse,
|
|
642
|
+
"auth.organizations": z.array(organization).parse,
|
|
643
|
+
"queue.list": z.array(z.object({
|
|
644
|
+
name: z.string(),
|
|
645
|
+
kind: z.string(),
|
|
646
|
+
depth: z.number().optional()
|
|
647
|
+
})).parse,
|
|
648
|
+
"queue.depths": z.array(z.object({
|
|
649
|
+
name: z.string(),
|
|
650
|
+
depth: z.number(),
|
|
651
|
+
inFlight: z.number().optional()
|
|
652
|
+
})).parse,
|
|
653
|
+
"queue.dlq": z.array(z.object({
|
|
654
|
+
id: z.string(),
|
|
655
|
+
queue: z.string(),
|
|
656
|
+
failedAt: z.number(),
|
|
657
|
+
attempts: z.number(),
|
|
658
|
+
error: z.string().optional(),
|
|
659
|
+
payload: z.unknown().optional()
|
|
660
|
+
})).parse,
|
|
661
|
+
"queue.send": z.object({ id: z.string() }).parse,
|
|
662
|
+
"queue.replay": z.object({ replayed: z.number() }).parse,
|
|
663
|
+
"schedule.jobs": z.array(z.object({
|
|
664
|
+
name: z.string(),
|
|
665
|
+
kind: z.enum(["cron", "interval"]),
|
|
666
|
+
expression: z.string().optional(),
|
|
667
|
+
ms: z.number().optional(),
|
|
668
|
+
lastRun: z.number().optional(),
|
|
669
|
+
nextRun: z.number().optional()
|
|
670
|
+
})).parse,
|
|
671
|
+
"schedule.triggers": z.array(z.object({
|
|
672
|
+
name: z.string(),
|
|
673
|
+
cron: z.string(),
|
|
674
|
+
nextRun: z.number().optional()
|
|
675
|
+
})).parse,
|
|
676
|
+
"schedule.runNow": acknowledged.parse,
|
|
677
|
+
"flags.list": z.array(z.object({
|
|
678
|
+
key: z.string(),
|
|
679
|
+
value: flagValue
|
|
680
|
+
})).parse,
|
|
681
|
+
"flags.evaluate": z.object({
|
|
682
|
+
flagKey: z.string(),
|
|
683
|
+
value: flagValue,
|
|
684
|
+
reason: z.enum([
|
|
685
|
+
"STATIC",
|
|
686
|
+
"DEFAULT",
|
|
687
|
+
"ERROR"
|
|
688
|
+
]),
|
|
689
|
+
errorMessage: z.string().optional()
|
|
690
|
+
}).parse,
|
|
691
|
+
"logs.tail": z.array(z.object({
|
|
692
|
+
ts: z.number(),
|
|
693
|
+
level: z.enum([
|
|
694
|
+
"debug",
|
|
695
|
+
"info",
|
|
696
|
+
"warn",
|
|
697
|
+
"error"
|
|
698
|
+
]),
|
|
699
|
+
msg: z.string(),
|
|
700
|
+
source: z.string().optional(),
|
|
701
|
+
fields: row.optional()
|
|
702
|
+
})).parse,
|
|
703
|
+
"live.subscriptions": z.array(z.object({
|
|
704
|
+
id: z.string(),
|
|
705
|
+
room: z.string(),
|
|
706
|
+
tags: strings,
|
|
707
|
+
connectedAt: z.number(),
|
|
708
|
+
clientId: z.string().optional()
|
|
709
|
+
})).parse,
|
|
710
|
+
"presence.rooms": z.array(z.object({
|
|
711
|
+
room: z.string(),
|
|
712
|
+
count: z.number(),
|
|
713
|
+
members: strings.optional()
|
|
714
|
+
})).parse,
|
|
715
|
+
"audit.tail": z.array(z.object({
|
|
716
|
+
ts: z.number(),
|
|
717
|
+
op: z.string(),
|
|
718
|
+
mode: z.enum(["read", "write"]),
|
|
719
|
+
subject: z.string(),
|
|
720
|
+
status: z.number(),
|
|
721
|
+
ms: z.number(),
|
|
722
|
+
ip: z.string().nullable(),
|
|
723
|
+
detail: z.object({
|
|
724
|
+
target: z.string().optional(),
|
|
725
|
+
summary: z.string().optional(),
|
|
726
|
+
extra: row.optional()
|
|
727
|
+
}).optional()
|
|
728
|
+
})).parse
|
|
729
|
+
};
|
|
730
|
+
/** The result type comes only from the selected operation and its validator. */
|
|
731
|
+
function parseStudioResponse(op, value) {
|
|
732
|
+
return STUDIO_RESPONSE_PARSERS[op](value);
|
|
733
|
+
}
|
|
734
|
+
const errorStatus = z.number().int().min(400).max(599);
|
|
735
|
+
const envelope = z.discriminatedUnion("ok", [z.object({
|
|
736
|
+
ok: z.literal(true),
|
|
737
|
+
op: z.string(),
|
|
738
|
+
data: z.unknown(),
|
|
739
|
+
meta: z.object({
|
|
740
|
+
ms: z.number(),
|
|
741
|
+
op: z.string(),
|
|
742
|
+
mode: z.enum(["read", "write"])
|
|
743
|
+
})
|
|
744
|
+
}), z.object({
|
|
745
|
+
ok: z.literal(false),
|
|
746
|
+
op: z.string(),
|
|
747
|
+
status: errorStatus,
|
|
748
|
+
error: z.object({
|
|
749
|
+
code: z.string(),
|
|
750
|
+
title: z.string(),
|
|
751
|
+
status: errorStatus,
|
|
752
|
+
message: z.string(),
|
|
753
|
+
hint: z.string().optional(),
|
|
754
|
+
docsUrl: z.string().optional(),
|
|
755
|
+
details: z.unknown().optional()
|
|
756
|
+
})
|
|
757
|
+
})]);
|
|
758
|
+
/** Validate the envelope, operation identity, and operation-specific payload. */
|
|
759
|
+
function parseStudioRpcResponse(op, value) {
|
|
760
|
+
const parsed = envelope.parse(value);
|
|
761
|
+
if (parsed.op !== op) throw new Error("Studio response operation does not match the request.");
|
|
762
|
+
if (!parsed.ok) {
|
|
763
|
+
if (parsed.status !== parsed.error.status) throw new Error("Inconsistent Studio error status.");
|
|
764
|
+
return parsed;
|
|
765
|
+
}
|
|
766
|
+
if (parsed.meta.op !== op || parsed.meta.mode !== STUDIO_OP_META[op].mode) throw new Error("Studio response metadata does not match the request.");
|
|
767
|
+
return {
|
|
768
|
+
...parsed,
|
|
769
|
+
data: parseStudioResponse(op, parsed.data)
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
//#endregion
|
|
773
|
+
export { STUDIO_DEFAULT_PATH, STUDIO_ERROR_CODES, STUDIO_EXPORT_SUFFIX, STUDIO_FEATURE_KEYS, STUDIO_FILTER_OPERATORS, STUDIO_HEALTH_SUFFIX, STUDIO_OPS, STUDIO_OP_META, STUDIO_PROTOCOL_VERSION, STUDIO_RESPONSE_PARSERS, STUDIO_RPC_SUFFIX, STUDIO_TOKEN_HEADER, STUDIO_WS_TOKEN_SUFFIX, isRecord, parseStudioConnection, parseStudioResponse, parseStudioRpcResponse, parseTryItRequest, parseTryItResponse };
|
|
774
|
+
|
|
775
|
+
//# sourceMappingURL=index.js.map
|