@aotter/mantle-admin 0.1.0-alpha.9 → 0.1.2-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/mountMantleAdmin.d.ts +5 -0
- package/dist/mountMantleAdmin.d.ts.map +1 -1
- package/dist/mountMantleAdmin.js +437 -53
- package/dist/mountMantleAdmin.js.map +1 -1
- package/dist/mountMantleOAuth.d.ts +36 -0
- package/dist/mountMantleOAuth.d.ts.map +1 -0
- package/dist/mountMantleOAuth.js +150 -0
- package/dist/mountMantleOAuth.js.map +1 -0
- package/dist/oauthFallbackHtml.d.ts +7 -0
- package/dist/oauthFallbackHtml.d.ts.map +1 -0
- package/dist/oauthFallbackHtml.js +104 -0
- package/dist/oauthFallbackHtml.js.map +1 -0
- package/dist/rejectCrossOriginMutation.d.ts +2 -0
- package/dist/rejectCrossOriginMutation.d.ts.map +1 -0
- package/dist/rejectCrossOriginMutation.js +15 -0
- package/dist/rejectCrossOriginMutation.js.map +1 -0
- package/dist/staffMcp.d.ts +11 -0
- package/dist/staffMcp.d.ts.map +1 -0
- package/dist/staffMcp.js +47 -0
- package/dist/staffMcp.js.map +1 -0
- package/package.json +5 -5
package/dist/mountMantleAdmin.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { bodyLimit } from "hono/body-limit";
|
|
2
|
+
import { staffMcp } from "./staffMcp.js";
|
|
3
|
+
import { rejectCrossOriginMutation } from "./rejectCrossOriginMutation.js";
|
|
4
|
+
import { DiagnosticError, HTTP_STATUS_BY_CODE, MANTLE_REF_KEYWORD, MCP_HINT_KEYWORD, VIEW_PARAMS_RESERVED, isMediaMcpHint, httpStatusFor, meetsRole, redactForWire, resolveLocalizedText, resolveLifecycle, runtimeDiagnostic, checkSchemaAdminUi, checkViewAdminUi, schemaSortableFields, STAFF_ROLES, } from "@aotter/mantle-spec";
|
|
5
|
+
import { ViewParamCoercionError, MAX_JSON_BODY_BYTES, coerceViewParams, evaluateAuthAll, projectCallableCapabilities, } from "@aotter/mantle-runtime";
|
|
3
6
|
const STAFF_ROLE_SET = new Set(STAFF_ROLES);
|
|
4
7
|
const [PAGE_PARAM, SHOW_PARAM] = VIEW_PARAMS_RESERVED;
|
|
5
8
|
const MEMBER_CURSOR_PREFIX = "m:";
|
|
@@ -27,8 +30,19 @@ export function mountMantleAdmin(app, ref) {
|
|
|
27
30
|
const authBasePath = auth.basePath;
|
|
28
31
|
const spa = async (c) => {
|
|
29
32
|
const asset = await ref.assets.fetch(new Request(new URL("/_mantle/admin/index.html", c.req.url)));
|
|
30
|
-
|
|
33
|
+
if (asset) {
|
|
34
|
+
const headers = new Headers(asset.headers);
|
|
35
|
+
// Append a separate policy so an asset's existing CSP remains effective.
|
|
36
|
+
headers.append("content-security-policy", "frame-ancestors 'none'");
|
|
37
|
+
headers.set("x-frame-options", "DENY");
|
|
38
|
+
headers.set("cache-control", "private, no-store");
|
|
39
|
+
return new Response(asset.body, { status: asset.status, headers });
|
|
40
|
+
}
|
|
41
|
+
return new Response("Mantle Admin assets are missing; run `mantle generate` and configure the ASSETS binding.", { status: 503, headers: { "cache-control": "private, no-store" } });
|
|
31
42
|
};
|
|
43
|
+
const limitBody = bodyLimit({ maxSize: MAX_JSON_BODY_BYTES });
|
|
44
|
+
app.use("/admin/api/*", limitBody);
|
|
45
|
+
app.use(`${authBasePath}/*`, limitBody);
|
|
32
46
|
// Public read-only manifest of registered sign-in methods. The admin
|
|
33
47
|
// SPA hits this on sign-in-page mount so it can render per-method
|
|
34
48
|
// sections without baking the method list into its build. No secrets
|
|
@@ -64,10 +78,13 @@ export function mountMantleAdmin(app, ref) {
|
|
|
64
78
|
"/admin/c/:collection/:id",
|
|
65
79
|
"/admin/media",
|
|
66
80
|
"/admin/preferences",
|
|
81
|
+
"/admin/connected-apps",
|
|
67
82
|
"/admin/settings",
|
|
68
83
|
"/admin/staff",
|
|
69
84
|
"/admin/members",
|
|
70
85
|
"/admin/ops",
|
|
86
|
+
"/admin/dev",
|
|
87
|
+
"/admin/dev/*",
|
|
71
88
|
"/admin/views/:name",
|
|
72
89
|
]) {
|
|
73
90
|
app.get(path, spa);
|
|
@@ -89,8 +106,8 @@ export function mountMantleAdmin(app, ref) {
|
|
|
89
106
|
message: `Schema '${schema.metadata.name}' is read-only on generic authoring surfaces; use its declared Procedures.`,
|
|
90
107
|
}));
|
|
91
108
|
};
|
|
92
|
-
const readMutableEntry = async (runtime, id, path) => {
|
|
93
|
-
const entry = await runtime.getEntry.execute({ id });
|
|
109
|
+
const readMutableEntry = async (runtime, collection, id, path) => {
|
|
110
|
+
const entry = await runtime.getEntry.execute({ id, collection });
|
|
94
111
|
assertMutableSchema(path, schemasByName.get(entry.collection));
|
|
95
112
|
return entry;
|
|
96
113
|
};
|
|
@@ -114,29 +131,53 @@ export function mountMantleAdmin(app, ref) {
|
|
|
114
131
|
// `/admin/api/collections` precedent exactly (same shape of
|
|
115
132
|
// "precompute at mount from ref.plan, list on GET") and needs
|
|
116
133
|
// no changes to the `SiteInfo` type or its query key.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
// path and must not appear in the admin report sidebar — listing
|
|
120
|
-
// them was noise + broke on param-driven storefront Views (see #433).
|
|
121
|
-
const staffViews = Object.values(ref.plan.views)
|
|
122
|
-
.map(({ manifest }) => manifest)
|
|
123
|
-
.filter((view) => view.spec.surface === "staff");
|
|
124
|
-
const viewsManifest = staffViews.map((v) => ({
|
|
134
|
+
const views = Object.values(ref.plan.views).map(({ manifest }) => manifest);
|
|
135
|
+
const viewsManifest = views.map((v) => ({
|
|
125
136
|
name: v.metadata.name,
|
|
126
137
|
title: v.spec.title ?? null,
|
|
138
|
+
surface: v.spec.surface,
|
|
127
139
|
from: v.spec.from ?? null,
|
|
128
140
|
params: v.spec.params ?? null,
|
|
129
141
|
fields: v.spec.fields ?? null,
|
|
130
142
|
list: checkViewAdminUi(v).list,
|
|
131
143
|
}));
|
|
144
|
+
const developerConsole = projectDeveloperConsole(ref.plan);
|
|
132
145
|
const guarded = (method, path, body) => {
|
|
133
146
|
app.on(method.toUpperCase(), path, async (c) => {
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
147
|
+
const trace = path === "/admin/api/bootstrap" || path === "/admin/api/entries"
|
|
148
|
+
? {
|
|
149
|
+
kind: path === "/admin/api/bootstrap" ? "mantle-admin-bootstrap-v1" : "mantle-admin-entries-v1",
|
|
150
|
+
requestId: crypto.randomUUID(), ray: c.req.header("cf-ray") ?? null, startedAt: performance.now(),
|
|
151
|
+
}
|
|
152
|
+
: undefined;
|
|
153
|
+
let status = null;
|
|
154
|
+
try {
|
|
155
|
+
const rejected = rejectCrossOriginMutation(c.req.raw);
|
|
156
|
+
if (rejected)
|
|
157
|
+
return rejected;
|
|
158
|
+
const sessionStartedAt = performance.now();
|
|
159
|
+
const gate = await readStaffGate(c, auth);
|
|
160
|
+
if (trace)
|
|
161
|
+
trace.sessionMs = elapsed(sessionStartedAt);
|
|
162
|
+
if (gate.kind === "unauth")
|
|
163
|
+
return adminUnauthenticated(c, path);
|
|
164
|
+
if (gate.kind === "forbidden")
|
|
165
|
+
return adminNotStaff(c, path, gate.login);
|
|
166
|
+
const response = await body(c, gate, trace);
|
|
167
|
+
status = response.status;
|
|
168
|
+
return response;
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
if (trace)
|
|
172
|
+
console.info(JSON.stringify({
|
|
173
|
+
kind: trace.kind, requestId: trace.requestId, ray: trace.ray, status,
|
|
174
|
+
totalMs: elapsed(trace.startedAt), sessionMs: trace.sessionMs ?? null,
|
|
175
|
+
runtimeMs: trace.runtimeMs ?? null, siteMs: trace.siteMs ?? null,
|
|
176
|
+
catalogMs: trace.catalogMs ?? null, entriesMs: trace.entriesMs ?? null,
|
|
177
|
+
listMs: trace.listMs ?? null, translationsMs: trace.translationsMs ?? null,
|
|
178
|
+
serializeMs: trace.serializeMs ?? null, rows: trace.rows ?? null,
|
|
179
|
+
}));
|
|
180
|
+
}
|
|
140
181
|
});
|
|
141
182
|
};
|
|
142
183
|
const roleGuarded = (method, path, minimumRole, body) => {
|
|
@@ -147,6 +188,14 @@ export function mountMantleAdmin(app, ref) {
|
|
|
147
188
|
return body(c, gate);
|
|
148
189
|
});
|
|
149
190
|
};
|
|
191
|
+
guarded("get", "/admin/api/webmcp", async () => {
|
|
192
|
+
const { tools, routes } = await staffMcp(await ref.get(), ref.plan);
|
|
193
|
+
return Response.json({ tools, routes }, { headers: { "cache-control": "private, no-store" } });
|
|
194
|
+
});
|
|
195
|
+
guarded("post", "/admin/api/mcp", async (c, gate) => {
|
|
196
|
+
const { dispatcher } = await staffMcp(await ref.get(), ref.plan);
|
|
197
|
+
return dispatcher.dispatch(c.req.raw, adminHandlerContext(c, gate, ref));
|
|
198
|
+
});
|
|
150
199
|
guarded("get", "/admin/api/me", (_c, gate) => Response.json({ login: gate.login, role: gate.role, userId: gate.userId, image: gate.image }));
|
|
151
200
|
roleGuarded("get", "/admin/api/staff", "owner", async () => Response.json({ users: await auth.listUsers() }));
|
|
152
201
|
roleGuarded("get", "/admin/api/members", "editor", async (c) => {
|
|
@@ -257,7 +306,8 @@ export function mountMantleAdmin(app, ref) {
|
|
|
257
306
|
}
|
|
258
307
|
await auth.setUserRole(result.id, role);
|
|
259
308
|
}
|
|
260
|
-
|
|
309
|
+
await auth.sendStaffInvitation?.(email, role);
|
|
310
|
+
return Response.json({ ok: true, userId: result.id, emailSent: Boolean(auth.sendStaffInvitation) });
|
|
261
311
|
});
|
|
262
312
|
roleGuarded("delete", "/admin/api/staff/invitations/:id", "owner", async (c) => {
|
|
263
313
|
const revoked = await auth.revokeInvite(c.req.param("id") ?? "");
|
|
@@ -276,13 +326,33 @@ export function mountMantleAdmin(app, ref) {
|
|
|
276
326
|
return Response.json({ ok: true });
|
|
277
327
|
});
|
|
278
328
|
guarded("get", "/admin/api/collections", () => Response.json({ collections }));
|
|
329
|
+
guarded("get", "/admin/api/collections/:name/statistics", async (c) => {
|
|
330
|
+
const collection = collections.find((item) => item.name === c.req.param("name") &&
|
|
331
|
+
(!item.parent || item.nav?.standalone === true));
|
|
332
|
+
if (!collection)
|
|
333
|
+
return Response.json({ error: "Collection not found" }, { status: 404 });
|
|
334
|
+
const ranges = {
|
|
335
|
+
"1h": [3_600_000, 300_000], "24h": [86_400_000, 3_600_000],
|
|
336
|
+
"7d": [7 * 86_400_000, 6 * 3_600_000], "20d": [20 * 86_400_000, 86_400_000],
|
|
337
|
+
};
|
|
338
|
+
const range = c.req.query("range") ?? "7d";
|
|
339
|
+
if (!Object.hasOwn(ranges, range))
|
|
340
|
+
return Response.json({ error: "Invalid statistics range" }, { status: 400 });
|
|
341
|
+
const [duration, bucketMs] = ranges[range];
|
|
342
|
+
const runtime = await ref.get();
|
|
343
|
+
if (!runtime.entries.readCreationStatistics) {
|
|
344
|
+
return Response.json({ error: "Statistics unavailable for this storage adapter" }, { status: 501 });
|
|
345
|
+
}
|
|
346
|
+
const to = Date.now();
|
|
347
|
+
const from = to - duration;
|
|
348
|
+
const statistics = await runtime.entries.readCreationStatistics({ collection: collection.name, from, to, bucketMs });
|
|
349
|
+
return Response.json({ ...statistics, from, to, bucketMs }, { headers: { "cache-control": "private, no-store" } });
|
|
350
|
+
});
|
|
351
|
+
roleGuarded("get", "/admin/api/developer-console", "owner", () => Response.json(developerConsole));
|
|
279
352
|
guarded("get", "/admin/api/views-manifest", () => Response.json({ views: viewsManifest }));
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
|
|
283
|
-
// exact same `handleViewRequest` logic and response shape as the
|
|
284
|
-
// public surface; only the gate + path differ.
|
|
285
|
-
for (const v of staffViews) {
|
|
353
|
+
// Admin inspection is staff-gated for both surfaces. Public Views keep
|
|
354
|
+
// their separate public `/api/views/<name>` contract.
|
|
355
|
+
for (const v of views) {
|
|
286
356
|
const viewName = v.metadata.name;
|
|
287
357
|
guarded("get", `/admin/api/views/${viewName}`, async (c, gate) => {
|
|
288
358
|
const runtime = await ref.get();
|
|
@@ -293,7 +363,7 @@ export function mountMantleAdmin(app, ref) {
|
|
|
293
363
|
return handleViewRequest(c.req.raw, runtime, v, adminHandlerContext(c, gate, ref), true);
|
|
294
364
|
});
|
|
295
365
|
}
|
|
296
|
-
|
|
366
|
+
const operationsPayload = (c, gate) => ({
|
|
297
367
|
operations: operations.filter((op) => evaluateAuthAll(op.procedure.spec.requires, adminHandlerContext(c, gate, ref), `GET /admin/api/operations/${op.name}`, "runtime") === null).map((op) => ({
|
|
298
368
|
name: op.name,
|
|
299
369
|
title: op.title,
|
|
@@ -302,8 +372,12 @@ export function mountMantleAdmin(app, ref) {
|
|
|
302
372
|
uiSchema: op.uiSchema,
|
|
303
373
|
triggers: op.triggers,
|
|
304
374
|
rowBindings: op.rowBindings,
|
|
375
|
+
targetCollection: op.procedure.spec.handler.kind === "builtin"
|
|
376
|
+
? op.procedure.spec.handler.schema
|
|
377
|
+
: null,
|
|
305
378
|
})),
|
|
306
|
-
})
|
|
379
|
+
});
|
|
380
|
+
guarded("get", "/admin/api/operations", (c, gate) => Response.json(operationsPayload(c, gate)));
|
|
307
381
|
guarded("post", "/admin/api/operations/:name", async (c, gate) => {
|
|
308
382
|
const name = c.req.param("name") ?? "";
|
|
309
383
|
const op = operationsByName.get(name);
|
|
@@ -340,16 +414,17 @@ export function mountMantleAdmin(app, ref) {
|
|
|
340
414
|
return { ok: true, output: result.data };
|
|
341
415
|
});
|
|
342
416
|
});
|
|
343
|
-
|
|
344
|
-
const
|
|
345
|
-
const { origin, ...site } = await
|
|
417
|
+
const sitePayload = async (c, runtime) => {
|
|
418
|
+
const resolvedRuntime = runtime ?? await ref.get();
|
|
419
|
+
const { origin, ...site } = await resolvedRuntime.siteConfig.load();
|
|
346
420
|
const publicUrl = origin || new URL(c.req.url).origin;
|
|
347
|
-
return
|
|
421
|
+
return {
|
|
348
422
|
...site,
|
|
349
423
|
publicUrl,
|
|
350
424
|
mcpUrl: `${publicUrl}/mcp/staff`,
|
|
351
|
-
}
|
|
352
|
-
}
|
|
425
|
+
};
|
|
426
|
+
};
|
|
427
|
+
guarded("get", "/admin/api/site", async (c) => Response.json(await sitePayload(c)));
|
|
353
428
|
roleGuarded("get", "/admin/api/site-settings", "owner", async () => runMantleUseCase("GET /admin/api/site-settings", async () => {
|
|
354
429
|
const runtime = await ref.get();
|
|
355
430
|
return adminSiteSettings(await runtime.siteConfig.load());
|
|
@@ -366,7 +441,7 @@ export function mountMantleAdmin(app, ref) {
|
|
|
366
441
|
});
|
|
367
442
|
return adminSiteSettings(site);
|
|
368
443
|
}));
|
|
369
|
-
|
|
444
|
+
const entriesPayload = async (c, runtime, trace) => {
|
|
370
445
|
const collection = c.req.query("collection");
|
|
371
446
|
if (!collection) {
|
|
372
447
|
return Response.json({
|
|
@@ -380,7 +455,10 @@ export function mountMantleAdmin(app, ref) {
|
|
|
380
455
|
}),
|
|
381
456
|
}, { status: 400 });
|
|
382
457
|
}
|
|
383
|
-
const
|
|
458
|
+
const runtimeStartedAt = performance.now();
|
|
459
|
+
const resolvedRuntime = runtime ?? await ref.get();
|
|
460
|
+
if (trace)
|
|
461
|
+
trace.runtimeMs = elapsed(runtimeStartedAt);
|
|
384
462
|
const rawLimit = c.req.query("limit");
|
|
385
463
|
const parsedLimit = rawLimit ? Number.parseInt(rawLimit, 10) : NaN;
|
|
386
464
|
const statusQuery = c.req.query("status");
|
|
@@ -399,10 +477,25 @@ export function mountMantleAdmin(app, ref) {
|
|
|
399
477
|
}),
|
|
400
478
|
}, { status: 400 });
|
|
401
479
|
}
|
|
480
|
+
const scopeField = c.req.query("scope_field");
|
|
481
|
+
const scopeValue = c.req.query("scope_value");
|
|
482
|
+
if (Boolean(scopeField) !== Boolean(scopeValue)) {
|
|
483
|
+
return Response.json({
|
|
484
|
+
ok: false,
|
|
485
|
+
diagnostic: runtimeDiagnostic({
|
|
486
|
+
code: "INPUT_VALIDATION_FAILED",
|
|
487
|
+
severity: "error",
|
|
488
|
+
path: "GET /admin/api/entries",
|
|
489
|
+
expected: "scope_field and scope_value together",
|
|
490
|
+
message: "List parent scope requires both `scope_field` and `scope_value`.",
|
|
491
|
+
}),
|
|
492
|
+
}, { status: 400 });
|
|
493
|
+
}
|
|
402
494
|
// Admin pagination needs the cursored shape — `executePage` returns
|
|
403
495
|
// `{ rows, nextCursor? }`. `execute()` is the flat-array variant
|
|
404
496
|
// for app code.
|
|
405
|
-
const
|
|
497
|
+
const listStartedAt = performance.now();
|
|
498
|
+
const result = await resolvedRuntime.listEntries.executePage({
|
|
406
499
|
collection,
|
|
407
500
|
status: statusQuery && statusQuery !== "all" ? statusQuery : undefined,
|
|
408
501
|
limit: Number.isFinite(parsedLimit) ? parsedLimit : 99,
|
|
@@ -412,11 +505,19 @@ export function mountMantleAdmin(app, ref) {
|
|
|
412
505
|
filter: filterField && filterValue
|
|
413
506
|
? { field: filterField, value: filterValue }
|
|
414
507
|
: undefined,
|
|
508
|
+
scope: scopeField && scopeValue
|
|
509
|
+
? { field: scopeField, value: scopeValue }
|
|
510
|
+
: undefined,
|
|
415
511
|
sort: {
|
|
416
512
|
field: c.req.query("sort") || "updatedAt",
|
|
417
513
|
direction: sortDirection,
|
|
418
514
|
},
|
|
419
515
|
});
|
|
516
|
+
if (trace) {
|
|
517
|
+
trace.listMs = elapsed(listStartedAt);
|
|
518
|
+
trace.rows = result.rows.length;
|
|
519
|
+
}
|
|
520
|
+
const translationsStartedAt = performance.now();
|
|
420
521
|
const translationLocales = new Map();
|
|
421
522
|
const translationSchemas = schemas.filter((schema) => schema.spec.translates?.parent === collection);
|
|
422
523
|
for (const schema of translationSchemas) {
|
|
@@ -427,7 +528,7 @@ export function mountMantleAdmin(app, ref) {
|
|
|
427
528
|
if (value !== null)
|
|
428
529
|
parentValues.set(joinValueKey(value), value);
|
|
429
530
|
}
|
|
430
|
-
const translations = await
|
|
531
|
+
const translations = await resolvedRuntime.entries.readByDataFieldIn({
|
|
431
532
|
collection: schema.metadata.name,
|
|
432
533
|
field,
|
|
433
534
|
values: [...parentValues.values()],
|
|
@@ -452,12 +553,61 @@ export function mountMantleAdmin(app, ref) {
|
|
|
452
553
|
translationLocales.set(row.id, locales);
|
|
453
554
|
}
|
|
454
555
|
}
|
|
556
|
+
if (trace)
|
|
557
|
+
trace.translationsMs = elapsed(translationsStartedAt);
|
|
455
558
|
const items = result.rows.map((row) => adminListItem(row, schemasByName, [...(translationLocales.get(row.id) ?? [])]));
|
|
456
|
-
return
|
|
559
|
+
return {
|
|
457
560
|
items,
|
|
458
561
|
previous_cursor: result.previousCursor ?? null,
|
|
459
562
|
next_cursor: result.nextCursor ?? null,
|
|
460
|
-
}
|
|
563
|
+
};
|
|
564
|
+
};
|
|
565
|
+
guarded("get", "/admin/api/entries", async (c, _gate, trace) => {
|
|
566
|
+
const payload = await entriesPayload(c, undefined, trace);
|
|
567
|
+
if (payload instanceof Response)
|
|
568
|
+
return payload;
|
|
569
|
+
const serializeStartedAt = performance.now();
|
|
570
|
+
const response = Response.json(payload);
|
|
571
|
+
if (trace)
|
|
572
|
+
trace.serializeMs = elapsed(serializeStartedAt);
|
|
573
|
+
return response;
|
|
574
|
+
});
|
|
575
|
+
guarded("get", "/admin/api/bootstrap", async (c, gate, trace) => {
|
|
576
|
+
const runtimeStartedAt = performance.now();
|
|
577
|
+
const runtime = await ref.get();
|
|
578
|
+
if (trace)
|
|
579
|
+
trace.runtimeMs = elapsed(runtimeStartedAt);
|
|
580
|
+
const siteStartedAt = performance.now();
|
|
581
|
+
const catalogStartedAt = performance.now();
|
|
582
|
+
const entriesStartedAt = performance.now();
|
|
583
|
+
const [site, catalog, entries] = await Promise.all([
|
|
584
|
+
sitePayload(c, runtime).finally(() => { if (trace)
|
|
585
|
+
trace.siteMs = elapsed(siteStartedAt); }),
|
|
586
|
+
staffMcp(runtime, ref.plan).finally(() => { if (trace)
|
|
587
|
+
trace.catalogMs = elapsed(catalogStartedAt); }),
|
|
588
|
+
entriesPayload(c, runtime, trace).finally(() => { if (trace)
|
|
589
|
+
trace.entriesMs = elapsed(entriesStartedAt); }),
|
|
590
|
+
]);
|
|
591
|
+
if (entries instanceof Response)
|
|
592
|
+
return entries;
|
|
593
|
+
const serializeStartedAt = performance.now();
|
|
594
|
+
const response = Response.json({
|
|
595
|
+
me: {
|
|
596
|
+
login: gate.login,
|
|
597
|
+
role: gate.role,
|
|
598
|
+
userId: gate.userId,
|
|
599
|
+
image: gate.image,
|
|
600
|
+
},
|
|
601
|
+
collections,
|
|
602
|
+
site,
|
|
603
|
+
operations: operationsPayload(c, gate).operations,
|
|
604
|
+
views: viewsManifest,
|
|
605
|
+
webmcp: { tools: catalog.tools, routes: catalog.routes },
|
|
606
|
+
entries,
|
|
607
|
+
}, { headers: { "cache-control": "private, no-store" } });
|
|
608
|
+
if (trace)
|
|
609
|
+
trace.serializeMs = elapsed(serializeStartedAt);
|
|
610
|
+
return response;
|
|
461
611
|
});
|
|
462
612
|
guarded("get", "/admin/api/entries/export", async (c) => {
|
|
463
613
|
const collection = c.req.query("collection");
|
|
@@ -492,11 +642,26 @@ export function mountMantleAdmin(app, ref) {
|
|
|
492
642
|
}),
|
|
493
643
|
}, { status: 400 });
|
|
494
644
|
}
|
|
645
|
+
const scopeField = c.req.query("scope_field");
|
|
646
|
+
const scopeValue = c.req.query("scope_value");
|
|
647
|
+
if (Boolean(scopeField) !== Boolean(scopeValue)) {
|
|
648
|
+
return Response.json({
|
|
649
|
+
ok: false,
|
|
650
|
+
diagnostic: runtimeDiagnostic({
|
|
651
|
+
code: "INPUT_VALIDATION_FAILED",
|
|
652
|
+
severity: "error",
|
|
653
|
+
path: "GET /admin/api/entries/export",
|
|
654
|
+
expected: "scope_field and scope_value together",
|
|
655
|
+
message: "List parent scope requires both `scope_field` and `scope_value`.",
|
|
656
|
+
}),
|
|
657
|
+
}, { status: 400 });
|
|
658
|
+
}
|
|
495
659
|
const listOptions = {
|
|
496
660
|
collection,
|
|
497
661
|
status: statusQuery && statusQuery !== "all" ? statusQuery : undefined,
|
|
498
662
|
search: c.req.query("search") || undefined,
|
|
499
663
|
filter: filterField && filterValue ? { field: filterField, value: filterValue } : undefined,
|
|
664
|
+
scope: scopeField && scopeValue ? { field: scopeField, value: scopeValue } : undefined,
|
|
500
665
|
sort: {
|
|
501
666
|
field: c.req.query("sort") || "updatedAt",
|
|
502
667
|
direction: c.req.query("direction") === "asc" ? "asc" : "desc",
|
|
@@ -524,7 +689,8 @@ export function mountMantleAdmin(app, ref) {
|
|
|
524
689
|
guarded("get", "/admin/api/entries/:id", async (c) => runMantleUseCase(`GET /admin/api/entries/${c.req.param("id")}`, async () => {
|
|
525
690
|
const runtime = await ref.get();
|
|
526
691
|
const id = c.req.param("id");
|
|
527
|
-
const
|
|
692
|
+
const collection = requiredCollection(c.req.query("collection"), schemasByName, `GET /admin/api/entries/${id}`);
|
|
693
|
+
const row = await runtime.getEntry.execute({ id, collection });
|
|
528
694
|
return entryEditorPayload(runtime, row, schemas);
|
|
529
695
|
}));
|
|
530
696
|
guarded("post", "/admin/api/entries", async (c, gate) => runMantleUseCase("POST /admin/api/entries", async () => {
|
|
@@ -556,7 +722,8 @@ export function mountMantleAdmin(app, ref) {
|
|
|
556
722
|
guarded("patch", "/admin/api/entries/:id", async (c, gate) => runMantleUseCase(`PATCH /admin/api/entries/${c.req.param("id")}`, async () => {
|
|
557
723
|
const runtime = await ref.get();
|
|
558
724
|
const id = c.req.param("id");
|
|
559
|
-
const
|
|
725
|
+
const collection = requiredCollection(c.req.query("collection"), schemasByName, `PATCH /admin/api/entries/${id}`);
|
|
726
|
+
const current = await readMutableEntry(runtime, collection, id, `PATCH /admin/api/entries/${id}`);
|
|
560
727
|
if (gate.role === "contributor") {
|
|
561
728
|
const lifecycle = schemasByName.get(current.collection)?.spec.lifecycle ?? "publishing";
|
|
562
729
|
if (lifecycle === "operational" || current.status !== "draft") {
|
|
@@ -566,6 +733,7 @@ export function mountMantleAdmin(app, ref) {
|
|
|
566
733
|
const body = (await c.req.raw.json().catch(() => ({})));
|
|
567
734
|
const updated = await runtime.updateDraft.execute({
|
|
568
735
|
id,
|
|
736
|
+
collection,
|
|
569
737
|
expectedVersion: expectedVersionField(body.expectedVersion, `PATCH /admin/api/entries/${id}#/expectedVersion`),
|
|
570
738
|
data: objectField(body.data),
|
|
571
739
|
ctx: adminHandlerContext(c, gate, ref),
|
|
@@ -576,10 +744,12 @@ export function mountMantleAdmin(app, ref) {
|
|
|
576
744
|
roleGuarded("post", "/admin/api/entries/:id/publish", "editor", async (c, gate) => runMantleUseCase(`POST /admin/api/entries/${c.req.param("id")}/publish`, async () => {
|
|
577
745
|
const runtime = await ref.get();
|
|
578
746
|
const id = c.req.param("id");
|
|
579
|
-
|
|
747
|
+
const collection = requiredCollection(c.req.query("collection"), schemasByName, `POST /admin/api/entries/${id}/publish`);
|
|
748
|
+
await readMutableEntry(runtime, collection, id, `POST /admin/api/entries/${id}/publish`);
|
|
580
749
|
const body = await c.req.raw.json().catch(() => ({}));
|
|
581
750
|
const row = await runtime.requestPublish.execute({
|
|
582
751
|
id,
|
|
752
|
+
collection,
|
|
583
753
|
ctx: adminHandlerContext(c, gate, ref),
|
|
584
754
|
originalInput: body,
|
|
585
755
|
});
|
|
@@ -588,10 +758,12 @@ export function mountMantleAdmin(app, ref) {
|
|
|
588
758
|
roleGuarded("post", "/admin/api/entries/:id/unpublish", "editor", async (c, gate) => runMantleUseCase(`POST /admin/api/entries/${c.req.param("id")}/unpublish`, async () => {
|
|
589
759
|
const runtime = await ref.get();
|
|
590
760
|
const id = c.req.param("id");
|
|
591
|
-
|
|
761
|
+
const collection = requiredCollection(c.req.query("collection"), schemasByName, `POST /admin/api/entries/${id}/unpublish`);
|
|
762
|
+
await readMutableEntry(runtime, collection, id, `POST /admin/api/entries/${id}/unpublish`);
|
|
592
763
|
const body = await c.req.raw.json().catch(() => ({}));
|
|
593
764
|
const row = await runtime.unpublish.execute({
|
|
594
765
|
id,
|
|
766
|
+
collection,
|
|
595
767
|
ctx: adminHandlerContext(c, gate, ref),
|
|
596
768
|
originalInput: body,
|
|
597
769
|
});
|
|
@@ -600,10 +772,12 @@ export function mountMantleAdmin(app, ref) {
|
|
|
600
772
|
roleGuarded("delete", "/admin/api/entries/:id", "editor", async (c, gate) => runMantleUseCase(`DELETE /admin/api/entries/${c.req.param("id")}`, async () => {
|
|
601
773
|
const runtime = await ref.get();
|
|
602
774
|
const id = c.req.param("id");
|
|
603
|
-
|
|
775
|
+
const collection = requiredCollection(c.req.query("collection"), schemasByName, `DELETE /admin/api/entries/${id}`);
|
|
776
|
+
await readMutableEntry(runtime, collection, id, `DELETE /admin/api/entries/${id}`);
|
|
604
777
|
const body = await c.req.raw.json().catch(() => ({}));
|
|
605
778
|
return runtime.deleteEntry.execute({
|
|
606
779
|
id,
|
|
780
|
+
collection,
|
|
607
781
|
ctx: adminHandlerContext(c, gate, ref),
|
|
608
782
|
originalInput: body,
|
|
609
783
|
});
|
|
@@ -912,7 +1086,14 @@ function hasCtxStaffPredicate(procedure) {
|
|
|
912
1086
|
const predicates = procedure.spec.requires?.auth?.all ?? [];
|
|
913
1087
|
return predicates.some((pred) => typeof pred === "object" && pred !== null && "ctx.staff" in pred);
|
|
914
1088
|
}
|
|
915
|
-
function adminEntryTitle(data, schema) {
|
|
1089
|
+
function adminEntryTitle(data, schema, primaryField) {
|
|
1090
|
+
if (primaryField) {
|
|
1091
|
+
const value = data[primaryField];
|
|
1092
|
+
if (typeof value === "string" && value.trim())
|
|
1093
|
+
return value;
|
|
1094
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
1095
|
+
return value;
|
|
1096
|
+
}
|
|
916
1097
|
const key = titleFieldKey(data, schema);
|
|
917
1098
|
return key ? data[key] : null;
|
|
918
1099
|
}
|
|
@@ -1057,10 +1238,12 @@ async function entryEditorPayload(runtime, row, schemas) {
|
|
|
1057
1238
|
}));
|
|
1058
1239
|
}
|
|
1059
1240
|
const related = await relatedEntrySections(runtime, schema, row, schemas);
|
|
1241
|
+
const parent = await parentEntry(runtime, schema, row, schemas);
|
|
1060
1242
|
return {
|
|
1061
1243
|
collection: adminEditorCollection(schema, schemas),
|
|
1062
1244
|
entry: adminEditorEntry(row),
|
|
1063
|
-
parentEntryId:
|
|
1245
|
+
parentEntryId: parent?.id ?? null,
|
|
1246
|
+
parentEntryTitle: parent ? parentEntryTitle(parent, schemas) : null,
|
|
1064
1247
|
related,
|
|
1065
1248
|
};
|
|
1066
1249
|
}
|
|
@@ -1081,6 +1264,7 @@ function adminEditorCollection(schema, schemas) {
|
|
|
1081
1264
|
sortableFields: schemaSortableFields(schema),
|
|
1082
1265
|
filter: adminUi.filter,
|
|
1083
1266
|
list: adminUi.list,
|
|
1267
|
+
nav: adminUi.nav,
|
|
1084
1268
|
};
|
|
1085
1269
|
}
|
|
1086
1270
|
function adminEditorEntry(row) {
|
|
@@ -1184,16 +1368,25 @@ function collectionParentFor(childSchema, schemas) {
|
|
|
1184
1368
|
}
|
|
1185
1369
|
return null;
|
|
1186
1370
|
}
|
|
1187
|
-
async function
|
|
1371
|
+
async function parentEntry(runtime, childSchema, childRow, schemas) {
|
|
1188
1372
|
const parent = collectionParentFor(childSchema, schemas);
|
|
1189
1373
|
if (!parent)
|
|
1190
1374
|
return null;
|
|
1191
1375
|
const value = primitiveJoinValue(childRow.data[parent.childField]);
|
|
1192
1376
|
if (value === null)
|
|
1193
1377
|
return null;
|
|
1194
|
-
if (parent.parentField === "id" && typeof value === "string")
|
|
1195
|
-
return value;
|
|
1196
|
-
|
|
1378
|
+
if (parent.parentField === "id" && typeof value === "string") {
|
|
1379
|
+
return runtime.getEntry.execute({ id: value, collection: parent.collection });
|
|
1380
|
+
}
|
|
1381
|
+
return (await entriesByDataValue(runtime, parent.collection, parent.parentField, value))[0] ?? null;
|
|
1382
|
+
}
|
|
1383
|
+
function parentEntryTitle(entry, schemas) {
|
|
1384
|
+
const schema = schemas.find((candidate) => candidate.metadata.name === entry.collection);
|
|
1385
|
+
const primaryField = schema ? checkSchemaAdminUi(schema).list.primaryField : null;
|
|
1386
|
+
const value = adminEntryTitle(entry.data, schema?.spec.schema, primaryField);
|
|
1387
|
+
return typeof value === "string" || typeof value === "number" || typeof value === "boolean"
|
|
1388
|
+
? String(value)
|
|
1389
|
+
: entry.id;
|
|
1197
1390
|
}
|
|
1198
1391
|
function primitiveJoinValue(value) {
|
|
1199
1392
|
if (typeof value === "string" && value !== "")
|
|
@@ -1238,11 +1431,27 @@ function expectedVersionField(value, path) {
|
|
|
1238
1431
|
function stringField(value) {
|
|
1239
1432
|
return typeof value === "string" ? value : undefined;
|
|
1240
1433
|
}
|
|
1434
|
+
function requiredCollection(value, schemas, path) {
|
|
1435
|
+
if (value && schemas.has(value))
|
|
1436
|
+
return value;
|
|
1437
|
+
throw new DiagnosticError(runtimeDiagnostic({
|
|
1438
|
+
code: "INPUT_VALIDATION_FAILED",
|
|
1439
|
+
severity: "error",
|
|
1440
|
+
path: `${path}#/collection`,
|
|
1441
|
+
value,
|
|
1442
|
+
expected: "the name of a declared Schema",
|
|
1443
|
+
candidates: [...schemas.keys()],
|
|
1444
|
+
message: "A valid `collection` query parameter is required.",
|
|
1445
|
+
}));
|
|
1446
|
+
}
|
|
1241
1447
|
function objectField(value) {
|
|
1242
1448
|
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
1243
1449
|
? value
|
|
1244
1450
|
: {};
|
|
1245
1451
|
}
|
|
1452
|
+
function elapsed(startedAt) {
|
|
1453
|
+
return Math.round((performance.now() - startedAt) * 100) / 100;
|
|
1454
|
+
}
|
|
1246
1455
|
function adminHandlerContext(c, gate, ref) {
|
|
1247
1456
|
const request = ref.requestContext?.(c);
|
|
1248
1457
|
return {
|
|
@@ -1262,8 +1471,11 @@ async function readStaffGate(c, auth) {
|
|
|
1262
1471
|
const session = await auth.getSession(c.req.raw);
|
|
1263
1472
|
if (!session)
|
|
1264
1473
|
return { kind: "unauth" };
|
|
1265
|
-
const role =
|
|
1266
|
-
|
|
1474
|
+
const role = session.user.roleCurrent
|
|
1475
|
+
? session.user.role ?? null
|
|
1476
|
+
: await auth.getUserRole(session.user.id);
|
|
1477
|
+
const login = [session.user.githubLogin, session.user.name, session.user.email]
|
|
1478
|
+
.find((value) => value?.trim()) ?? null;
|
|
1267
1479
|
if (!role || !STAFF_ROLE_SET.has(role)) {
|
|
1268
1480
|
return { kind: "forbidden", login };
|
|
1269
1481
|
}
|
|
@@ -1417,6 +1629,178 @@ function mediaFieldsForSchema(schema) {
|
|
|
1417
1629
|
}
|
|
1418
1630
|
return out;
|
|
1419
1631
|
}
|
|
1632
|
+
function developerRelation(id, kind, sourceId, targetId, pointer, value) {
|
|
1633
|
+
return { id, kind, sourceId, targetId, pointer, value };
|
|
1634
|
+
}
|
|
1635
|
+
function jsonPointerSegment(value) {
|
|
1636
|
+
return value.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
1637
|
+
}
|
|
1638
|
+
function developerAudience(predicates) {
|
|
1639
|
+
if (predicates.some((predicate) => typeof predicate === "object" && "ctx.staff" in predicate))
|
|
1640
|
+
return "staff";
|
|
1641
|
+
if (predicates.includes("ctx.user"))
|
|
1642
|
+
return "members";
|
|
1643
|
+
if (predicates.some((predicate) => predicate === "ctx.auth" || (typeof predicate === "object" && "ctx.auth.scope" in predicate)))
|
|
1644
|
+
return "api-clients";
|
|
1645
|
+
return null;
|
|
1646
|
+
}
|
|
1647
|
+
function projectDeveloperConsole(plan) {
|
|
1648
|
+
const schemasByName = new Map(Object.values(plan.schemas).map(({ name, manifest }) => [name, manifest]));
|
|
1649
|
+
const procedures = Object.values(plan.procedures).map(({ name, manifest, authorization, guard }) => ({
|
|
1650
|
+
name,
|
|
1651
|
+
title: manifest.spec.title ?? null,
|
|
1652
|
+
description: manifest.spec.description ?? null,
|
|
1653
|
+
audience: developerAudience(authorization?.all ?? []) ?? "public",
|
|
1654
|
+
input: manifest.spec.input,
|
|
1655
|
+
output: manifest.spec.output,
|
|
1656
|
+
authorization: authorization?.all ?? [],
|
|
1657
|
+
guard: guard ?? null,
|
|
1658
|
+
handler: manifest.spec.handler,
|
|
1659
|
+
manifest,
|
|
1660
|
+
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
1661
|
+
const triggers = Object.values(plan.triggers).map(({ name, manifest, target }) => {
|
|
1662
|
+
const source = manifest.spec.source;
|
|
1663
|
+
const targetAudience = developerAudience(plan.procedures[target]?.authorization?.all ?? []);
|
|
1664
|
+
return {
|
|
1665
|
+
name,
|
|
1666
|
+
target,
|
|
1667
|
+
audience: source.kind === "lifecycle" ? "system" : source.kind === "mcp" && source.surface === "staff" ? "staff" : targetAudience ?? "public",
|
|
1668
|
+
source,
|
|
1669
|
+
manifest,
|
|
1670
|
+
};
|
|
1671
|
+
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
1672
|
+
const graph = {
|
|
1673
|
+
atoms: [
|
|
1674
|
+
...Object.values(plan.schemas).map(({ name, manifest }) => ({ id: `Schema:${name}`, kind: "Schema", name, title: manifest.spec.title })),
|
|
1675
|
+
...Object.values(plan.views).map(({ name, manifest, authorization }) => ({
|
|
1676
|
+
id: `View:${name}`,
|
|
1677
|
+
kind: "View",
|
|
1678
|
+
name,
|
|
1679
|
+
title: manifest.spec.title ?? null,
|
|
1680
|
+
audience: manifest.spec.surface === "staff" ? "staff" : developerAudience(authorization?.all ?? []) ?? "public",
|
|
1681
|
+
})),
|
|
1682
|
+
...procedures.map(({ name, title, description, audience, handler }) => ({
|
|
1683
|
+
id: `Procedure:${name}`,
|
|
1684
|
+
kind: "Procedure",
|
|
1685
|
+
name,
|
|
1686
|
+
title,
|
|
1687
|
+
description,
|
|
1688
|
+
audience,
|
|
1689
|
+
handler,
|
|
1690
|
+
})),
|
|
1691
|
+
...triggers.map(({ name, source, audience }) => ({
|
|
1692
|
+
id: `Trigger:${name}`,
|
|
1693
|
+
kind: "Trigger",
|
|
1694
|
+
name,
|
|
1695
|
+
title: null,
|
|
1696
|
+
audience,
|
|
1697
|
+
transport: source.kind,
|
|
1698
|
+
})),
|
|
1699
|
+
].sort((a, b) => a.id.localeCompare(b.id)),
|
|
1700
|
+
relations: [
|
|
1701
|
+
...Object.values(plan.schemas).flatMap(({ name, manifest, translationParent }) => [
|
|
1702
|
+
...(translationParent ? [developerRelation(`Schema:${name}:translates:${translationParent}`, "translation-parent", `Schema:${name}`, `Schema:${translationParent}`, "/spec/translates/parent", translationParent)] : []),
|
|
1703
|
+
...Object.entries(manifest.spec.schema.properties ?? {}).flatMap(([field, property]) => {
|
|
1704
|
+
const target = property[MANTLE_REF_KEYWORD];
|
|
1705
|
+
if (typeof target !== "string" || !schemasByName.has(target))
|
|
1706
|
+
return [];
|
|
1707
|
+
return [developerRelation(`Schema:${name}:field:${field}:${target}`, "schema-reference", `Schema:${name}`, `Schema:${target}`, `/spec/schema/properties/${jsonPointerSegment(field)}/x-mantle-ref`, target)];
|
|
1708
|
+
}),
|
|
1709
|
+
]),
|
|
1710
|
+
...Object.values(plan.views).flatMap(({ name, query, guard }) => [
|
|
1711
|
+
...(query.kind === "declarative" ? [developerRelation(`View:${name}:from:${query.from}`, "view-source", `View:${name}`, `Schema:${query.from}`, "/spec/from", query.from)] : []),
|
|
1712
|
+
...(guard ? [developerRelation(`View:${name}:guard:${guard}`, "authorization-guard", `View:${name}`, `Procedure:${guard}`, "/spec/requires/guard/procedure", guard)] : []),
|
|
1713
|
+
]),
|
|
1714
|
+
...Object.values(plan.procedures).flatMap(({ name, manifest, builtinSchema, collectionActionSchema, guard }) => [
|
|
1715
|
+
...(builtinSchema ? [developerRelation(`Procedure:${name}:builtin:${builtinSchema}`, "procedure-schema", `Procedure:${name}`, `Schema:${builtinSchema}`, "/spec/handler/schema", builtinSchema)] : []),
|
|
1716
|
+
...(collectionActionSchema ? [developerRelation(`Procedure:${name}:collectionAction:${collectionActionSchema}`, "collection-action", `Procedure:${name}`, `Schema:${collectionActionSchema}`, "/spec/uiSchema/collectionAction", collectionActionSchema)] : []),
|
|
1717
|
+
...discoverRowBindings(manifest, schemasByName).map(({ collection, inputField }) => developerRelation(`Procedure:${name}:input:${inputField}:${collection}`, "input-reference", `Procedure:${name}`, `Schema:${collection}`, `/spec/input/properties/${jsonPointerSegment(inputField)}/x-mantle-ref`, collection)),
|
|
1718
|
+
...(guard ? [developerRelation(`Procedure:${name}:guard:${guard}`, "authorization-guard", `Procedure:${name}`, `Procedure:${guard}`, "/spec/requires/guard/procedure", guard)] : []),
|
|
1719
|
+
]),
|
|
1720
|
+
...Object.values(plan.triggers).flatMap(({ name, target, lifecycleSchema }) => [
|
|
1721
|
+
developerRelation(`Trigger:${name}:target:${target}`, "trigger-target", `Trigger:${name}`, `Procedure:${target}`, "/spec/target/procedure", target),
|
|
1722
|
+
...(lifecycleSchema ? [developerRelation(`Trigger:${name}:source:${lifecycleSchema}`, "lifecycle-source", `Trigger:${name}`, `Schema:${lifecycleSchema}`, "/spec/source/schema", lifecycleSchema)] : []),
|
|
1723
|
+
]),
|
|
1724
|
+
].sort((a, b) => a.id.localeCompare(b.id)),
|
|
1725
|
+
};
|
|
1726
|
+
return {
|
|
1727
|
+
dataModel: {
|
|
1728
|
+
schemas: Object.values(plan.schemas).map(({ name, manifest }) => ({
|
|
1729
|
+
name,
|
|
1730
|
+
title: manifest.spec.title,
|
|
1731
|
+
lifecycle: resolveLifecycle(manifest),
|
|
1732
|
+
localized: manifest.spec.localized ?? false,
|
|
1733
|
+
translates: manifest.spec.translates ?? null,
|
|
1734
|
+
schema: manifest.spec.schema,
|
|
1735
|
+
uniqueIndexes: manifest.spec.uniqueIndexes ?? [],
|
|
1736
|
+
indexes: manifest.spec.indexes ?? [],
|
|
1737
|
+
searchableFields: manifest.spec.searchableFields ?? [],
|
|
1738
|
+
manifest,
|
|
1739
|
+
})).sort((a, b) => a.name.localeCompare(b.name)),
|
|
1740
|
+
views: Object.values(plan.views).map(({ name, manifest, query, authorization, guard }) => ({
|
|
1741
|
+
name,
|
|
1742
|
+
title: manifest.spec.title ?? null,
|
|
1743
|
+
surface: manifest.spec.surface,
|
|
1744
|
+
query,
|
|
1745
|
+
authorization: authorization?.all ?? [],
|
|
1746
|
+
guard: guard ?? null,
|
|
1747
|
+
manifest,
|
|
1748
|
+
})).sort((a, b) => a.name.localeCompare(b.name)),
|
|
1749
|
+
},
|
|
1750
|
+
logic: { triggers, procedures },
|
|
1751
|
+
interfaces: projectDeveloperInterfaces(plan),
|
|
1752
|
+
graph,
|
|
1753
|
+
};
|
|
1754
|
+
}
|
|
1755
|
+
function projectDeveloperInterfaces(plan) {
|
|
1756
|
+
const callable = projectCallableCapabilities(plan).map((capability) => ({
|
|
1757
|
+
kind: capability.kind,
|
|
1758
|
+
name: capability.name,
|
|
1759
|
+
target: capability.ownerName,
|
|
1760
|
+
surface: capability.surface,
|
|
1761
|
+
audience: developerAudience((capability.kind === "view"
|
|
1762
|
+
? plan.views[capability.ownerName]?.authorization?.all
|
|
1763
|
+
: plan.procedures[capability.ownerName]?.authorization?.all) ?? []) ?? "public",
|
|
1764
|
+
title: capability.title ?? null,
|
|
1765
|
+
description: capability.description,
|
|
1766
|
+
input: capability.inputSchema,
|
|
1767
|
+
output: capability.kind === "procedure" ? capability.outputSchema : null,
|
|
1768
|
+
trigger: capability.kind === "procedure" ? capability.trigger : null,
|
|
1769
|
+
}));
|
|
1770
|
+
const http = [
|
|
1771
|
+
...plan.httpRoutes.map((route) => {
|
|
1772
|
+
const procedure = plan.procedures[route.procedure];
|
|
1773
|
+
return {
|
|
1774
|
+
kind: "procedure",
|
|
1775
|
+
name: route.trigger,
|
|
1776
|
+
target: route.procedure,
|
|
1777
|
+
method: route.method,
|
|
1778
|
+
path: route.path,
|
|
1779
|
+
audience: developerAudience(procedure.authorization?.all ?? []) ?? "public",
|
|
1780
|
+
title: resolveLocalizedText(procedure.manifest.spec.title, "en") ?? null,
|
|
1781
|
+
description: resolveLocalizedText(procedure.manifest.spec.description, "en") ?? `Invoke Procedure '${route.procedure}'.`,
|
|
1782
|
+
input: procedure.manifest.spec.input,
|
|
1783
|
+
output: procedure.manifest.spec.output,
|
|
1784
|
+
};
|
|
1785
|
+
}),
|
|
1786
|
+
...callable.filter((capability) => capability.kind === "view" && capability.surface === "public").map((capability) => {
|
|
1787
|
+
const view = plan.views[capability.target];
|
|
1788
|
+
return {
|
|
1789
|
+
kind: "view",
|
|
1790
|
+
name: capability.name,
|
|
1791
|
+
target: capability.target,
|
|
1792
|
+
method: "GET",
|
|
1793
|
+
path: `/api/views/${capability.target}`,
|
|
1794
|
+
audience: developerAudience(view.authorization?.all ?? []) ?? "public",
|
|
1795
|
+
title: capability.title,
|
|
1796
|
+
description: capability.description,
|
|
1797
|
+
input: capability.input,
|
|
1798
|
+
output: null,
|
|
1799
|
+
};
|
|
1800
|
+
}),
|
|
1801
|
+
].sort((a, b) => `${a.path}\0${a.method}`.localeCompare(`${b.path}\0${b.method}`));
|
|
1802
|
+
return { http, callable };
|
|
1803
|
+
}
|
|
1420
1804
|
export async function runMantleUseCase(operation, execute) {
|
|
1421
1805
|
try {
|
|
1422
1806
|
const result = await execute();
|