@objectstack/rest 6.9.0 → 7.1.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/dist/index.cjs +290 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +24 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +290 -7
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.d.cts
CHANGED
|
@@ -280,6 +280,20 @@ declare class RestServer {
|
|
|
280
280
|
* to the protocol layer (the SecurityPlugin treats undefined as anon).
|
|
281
281
|
*/
|
|
282
282
|
private resolveExecCtx;
|
|
283
|
+
/**
|
|
284
|
+
* Filter an `App` metadata item by the current user's `systemPermissions`.
|
|
285
|
+
*
|
|
286
|
+
* - Drops the app entirely if its top-level `requiredPermissions` are not
|
|
287
|
+
* a subset of the user's system permissions.
|
|
288
|
+
* - Recursively strips child navigation entries (groups, items) whose
|
|
289
|
+
* `requiredPermissions` are not satisfied. Empty groups collapse so
|
|
290
|
+
* the sidebar doesn't render a label with no children.
|
|
291
|
+
*
|
|
292
|
+
* Returns `null` when the app should be hidden from the user. Returns a
|
|
293
|
+
* shallow copy with a filtered `navigation` tree otherwise — the original
|
|
294
|
+
* is never mutated so cached metadata stays clean.
|
|
295
|
+
*/
|
|
296
|
+
private filterAppForUser;
|
|
283
297
|
/**
|
|
284
298
|
* Build a `TranslationBundle` (`Record<locale, TranslationData>`) from an
|
|
285
299
|
* `II18nService` instance. Returns `undefined` when no locales are
|
|
@@ -304,6 +318,16 @@ declare class RestServer {
|
|
|
304
318
|
* Translate a list of metadata documents using `translateMetaItem`.
|
|
305
319
|
*/
|
|
306
320
|
private translateMetaItems;
|
|
321
|
+
/**
|
|
322
|
+
* Translate the `entries` payload returned by `getMetaTypes()` — applies
|
|
323
|
+
* the active locale to each entry's `label`, `description`, and the
|
|
324
|
+
* nested `form` layout (section labels, field labels, helpText,
|
|
325
|
+
* placeholders) via `metadataForms.<type>` translation namespace.
|
|
326
|
+
*
|
|
327
|
+
* No-ops when no i18n service / locale / matching bundle entry exists,
|
|
328
|
+
* so this is safe to call unconditionally from the `/meta` handler.
|
|
329
|
+
*/
|
|
330
|
+
private translateMetaTypesResponse;
|
|
307
331
|
/**
|
|
308
332
|
* Pull the request hostname (without port) from a Node-style `req` or
|
|
309
333
|
* a Fetch-style request wrapper. Returns undefined when no Host header
|
package/dist/index.d.ts
CHANGED
|
@@ -280,6 +280,20 @@ declare class RestServer {
|
|
|
280
280
|
* to the protocol layer (the SecurityPlugin treats undefined as anon).
|
|
281
281
|
*/
|
|
282
282
|
private resolveExecCtx;
|
|
283
|
+
/**
|
|
284
|
+
* Filter an `App` metadata item by the current user's `systemPermissions`.
|
|
285
|
+
*
|
|
286
|
+
* - Drops the app entirely if its top-level `requiredPermissions` are not
|
|
287
|
+
* a subset of the user's system permissions.
|
|
288
|
+
* - Recursively strips child navigation entries (groups, items) whose
|
|
289
|
+
* `requiredPermissions` are not satisfied. Empty groups collapse so
|
|
290
|
+
* the sidebar doesn't render a label with no children.
|
|
291
|
+
*
|
|
292
|
+
* Returns `null` when the app should be hidden from the user. Returns a
|
|
293
|
+
* shallow copy with a filtered `navigation` tree otherwise — the original
|
|
294
|
+
* is never mutated so cached metadata stays clean.
|
|
295
|
+
*/
|
|
296
|
+
private filterAppForUser;
|
|
283
297
|
/**
|
|
284
298
|
* Build a `TranslationBundle` (`Record<locale, TranslationData>`) from an
|
|
285
299
|
* `II18nService` instance. Returns `undefined` when no locales are
|
|
@@ -304,6 +318,16 @@ declare class RestServer {
|
|
|
304
318
|
* Translate a list of metadata documents using `translateMetaItem`.
|
|
305
319
|
*/
|
|
306
320
|
private translateMetaItems;
|
|
321
|
+
/**
|
|
322
|
+
* Translate the `entries` payload returned by `getMetaTypes()` — applies
|
|
323
|
+
* the active locale to each entry's `label`, `description`, and the
|
|
324
|
+
* nested `form` layout (section labels, field labels, helpText,
|
|
325
|
+
* placeholders) via `metadataForms.<type>` translation namespace.
|
|
326
|
+
*
|
|
327
|
+
* No-ops when no i18n service / locale / matching bundle entry exists,
|
|
328
|
+
* so this is safe to call unconditionally from the `/meta` handler.
|
|
329
|
+
*/
|
|
330
|
+
private translateMetaTypesResponse;
|
|
307
331
|
/**
|
|
308
332
|
* Pull the request hostname (without port) from a Node-style `req` or
|
|
309
333
|
* a Fetch-style request wrapper. Returns undefined when no Host header
|
package/dist/index.js
CHANGED
|
@@ -628,6 +628,7 @@ var RestServer = class {
|
|
|
628
628
|
const userId = session.user.id;
|
|
629
629
|
const tenantId = session.session?.activeOrganizationId ?? void 0;
|
|
630
630
|
const permissions = [];
|
|
631
|
+
const systemPermissions = [];
|
|
631
632
|
const roles = [];
|
|
632
633
|
try {
|
|
633
634
|
let ql;
|
|
@@ -672,6 +673,20 @@ var RestServer = class {
|
|
|
672
673
|
}).catch(() => []);
|
|
673
674
|
for (const ps of psRows ?? []) {
|
|
674
675
|
if (ps.name && !permissions.includes(ps.name)) permissions.push(ps.name);
|
|
676
|
+
const rawSys = typeof ps.system_permissions === "string" ? (() => {
|
|
677
|
+
try {
|
|
678
|
+
return JSON.parse(ps.system_permissions);
|
|
679
|
+
} catch {
|
|
680
|
+
return [];
|
|
681
|
+
}
|
|
682
|
+
})() : ps.system_permissions ?? ps.systemPermissions;
|
|
683
|
+
if (Array.isArray(rawSys)) {
|
|
684
|
+
for (const sp of rawSys) {
|
|
685
|
+
if (typeof sp === "string" && !systemPermissions.includes(sp)) {
|
|
686
|
+
systemPermissions.push(sp);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
675
690
|
}
|
|
676
691
|
}
|
|
677
692
|
}
|
|
@@ -709,6 +724,7 @@ var RestServer = class {
|
|
|
709
724
|
tenantId,
|
|
710
725
|
roles,
|
|
711
726
|
permissions,
|
|
727
|
+
systemPermissions,
|
|
712
728
|
isSystem: false,
|
|
713
729
|
org_user_ids
|
|
714
730
|
};
|
|
@@ -716,6 +732,45 @@ var RestServer = class {
|
|
|
716
732
|
return void 0;
|
|
717
733
|
}
|
|
718
734
|
}
|
|
735
|
+
/**
|
|
736
|
+
* Filter an `App` metadata item by the current user's `systemPermissions`.
|
|
737
|
+
*
|
|
738
|
+
* - Drops the app entirely if its top-level `requiredPermissions` are not
|
|
739
|
+
* a subset of the user's system permissions.
|
|
740
|
+
* - Recursively strips child navigation entries (groups, items) whose
|
|
741
|
+
* `requiredPermissions` are not satisfied. Empty groups collapse so
|
|
742
|
+
* the sidebar doesn't render a label with no children.
|
|
743
|
+
*
|
|
744
|
+
* Returns `null` when the app should be hidden from the user. Returns a
|
|
745
|
+
* shallow copy with a filtered `navigation` tree otherwise — the original
|
|
746
|
+
* is never mutated so cached metadata stays clean.
|
|
747
|
+
*/
|
|
748
|
+
filterAppForUser(item, sysPerms) {
|
|
749
|
+
if (!item || typeof item !== "object") return item;
|
|
750
|
+
const reqApp = Array.isArray(item.requiredPermissions) ? item.requiredPermissions : [];
|
|
751
|
+
if (reqApp.length > 0 && !reqApp.every((p) => sysPerms.has(p))) {
|
|
752
|
+
return null;
|
|
753
|
+
}
|
|
754
|
+
const nav = Array.isArray(item.navigation) ? item.navigation : null;
|
|
755
|
+
if (!nav) return item;
|
|
756
|
+
const filterNav = (entries) => {
|
|
757
|
+
const out = [];
|
|
758
|
+
for (const e of entries) {
|
|
759
|
+
if (!e || typeof e !== "object") continue;
|
|
760
|
+
const req = Array.isArray(e.requiredPermissions) ? e.requiredPermissions : [];
|
|
761
|
+
if (req.length > 0 && !req.every((p) => sysPerms.has(p))) continue;
|
|
762
|
+
if (Array.isArray(e.children) && e.children.length > 0) {
|
|
763
|
+
const kids = filterNav(e.children);
|
|
764
|
+
if (e.type === "group" && kids.length === 0) continue;
|
|
765
|
+
out.push({ ...e, children: kids });
|
|
766
|
+
} else {
|
|
767
|
+
out.push(e);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
return out;
|
|
771
|
+
};
|
|
772
|
+
return { ...item, navigation: filterNav(nav) };
|
|
773
|
+
}
|
|
719
774
|
/**
|
|
720
775
|
* Build a `TranslationBundle` (`Record<locale, TranslationData>`) from an
|
|
721
776
|
* `II18nService` instance. Returns `undefined` when no locales are
|
|
@@ -789,6 +844,41 @@ var RestServer = class {
|
|
|
789
844
|
const { translateMetadataDocument } = await import("@objectstack/spec/system");
|
|
790
845
|
return items.map((item) => translateMetadataDocument(type, item, bundle, { locale }));
|
|
791
846
|
}
|
|
847
|
+
/**
|
|
848
|
+
* Translate the `entries` payload returned by `getMetaTypes()` — applies
|
|
849
|
+
* the active locale to each entry's `label`, `description`, and the
|
|
850
|
+
* nested `form` layout (section labels, field labels, helpText,
|
|
851
|
+
* placeholders) via `metadataForms.<type>` translation namespace.
|
|
852
|
+
*
|
|
853
|
+
* No-ops when no i18n service / locale / matching bundle entry exists,
|
|
854
|
+
* so this is safe to call unconditionally from the `/meta` handler.
|
|
855
|
+
*/
|
|
856
|
+
async translateMetaTypesResponse(req, environmentId, payload) {
|
|
857
|
+
if (!payload || typeof payload !== "object" || !Array.isArray(payload.entries)) return payload;
|
|
858
|
+
const i18n = await this.resolveI18nService(environmentId, req);
|
|
859
|
+
const bundle = this.buildTranslationBundle(i18n);
|
|
860
|
+
if (!bundle) return payload;
|
|
861
|
+
const locale = this.extractLocale(req, i18n);
|
|
862
|
+
if (!locale) return payload;
|
|
863
|
+
const {
|
|
864
|
+
resolveMetadataTypeLabel,
|
|
865
|
+
resolveMetadataTypeDescription,
|
|
866
|
+
resolveMetadataFormLabels
|
|
867
|
+
} = await import("@objectstack/spec/system");
|
|
868
|
+
const opts = { locale };
|
|
869
|
+
const entries = payload.entries.map((entry) => {
|
|
870
|
+
if (!entry || typeof entry !== "object" || typeof entry.type !== "string") return entry;
|
|
871
|
+
const next = { ...entry };
|
|
872
|
+
next.label = resolveMetadataTypeLabel(bundle, entry.type, entry.label ?? entry.type, opts);
|
|
873
|
+
const desc = resolveMetadataTypeDescription(bundle, entry.type, entry.description, opts);
|
|
874
|
+
if (desc !== void 0) next.description = desc;
|
|
875
|
+
if (entry.form) {
|
|
876
|
+
next.form = resolveMetadataFormLabels(entry.form, entry.type, bundle, opts);
|
|
877
|
+
}
|
|
878
|
+
return next;
|
|
879
|
+
});
|
|
880
|
+
return { ...payload, entries };
|
|
881
|
+
}
|
|
792
882
|
/**
|
|
793
883
|
* Pull the request hostname (without port) from a Node-style `req` or
|
|
794
884
|
* a Fetch-style request wrapper. Returns undefined when no Host header
|
|
@@ -1171,7 +1261,9 @@ var RestServer = class {
|
|
|
1171
1261
|
const environmentId = isScoped ? req.params?.environmentId : void 0;
|
|
1172
1262
|
const p = await this.resolveProtocol(environmentId, req);
|
|
1173
1263
|
const types = await p.getMetaTypes();
|
|
1174
|
-
|
|
1264
|
+
const translated = await this.translateMetaTypesResponse(req, environmentId, types);
|
|
1265
|
+
res.header("Vary", "Accept-Language");
|
|
1266
|
+
res.json(translated);
|
|
1175
1267
|
} catch (error) {
|
|
1176
1268
|
logError("[REST] Unhandled error:", error);
|
|
1177
1269
|
sendError(res, error);
|
|
@@ -1183,6 +1275,40 @@ var RestServer = class {
|
|
|
1183
1275
|
}
|
|
1184
1276
|
});
|
|
1185
1277
|
}
|
|
1278
|
+
if (metadata.endpoints.items !== false) {
|
|
1279
|
+
this.routeManager.register({
|
|
1280
|
+
method: "GET",
|
|
1281
|
+
path: `${metaPath}/diagnostics`,
|
|
1282
|
+
handler: async (req, res) => {
|
|
1283
|
+
try {
|
|
1284
|
+
const environmentId = isScoped ? req.params?.environmentId : void 0;
|
|
1285
|
+
const p = await this.resolveProtocol(environmentId, req);
|
|
1286
|
+
if (typeof p.getMetaDiagnostics !== "function") {
|
|
1287
|
+
res.status(501).json({
|
|
1288
|
+
error: "not_implemented",
|
|
1289
|
+
message: "protocol.getMetaDiagnostics() is not available in this kernel"
|
|
1290
|
+
});
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
const severityParam = req.query?.severity ?? "error";
|
|
1294
|
+
const severity = severityParam === "warning" ? "warning" : "error";
|
|
1295
|
+
const result = await p.getMetaDiagnostics({
|
|
1296
|
+
type: req.query?.type || void 0,
|
|
1297
|
+
severity,
|
|
1298
|
+
packageId: req.query?.package || void 0
|
|
1299
|
+
});
|
|
1300
|
+
res.json(result);
|
|
1301
|
+
} catch (error) {
|
|
1302
|
+
logError("[REST] Unhandled error:", error);
|
|
1303
|
+
sendError(res, error);
|
|
1304
|
+
}
|
|
1305
|
+
},
|
|
1306
|
+
metadata: {
|
|
1307
|
+
summary: "List metadata entries that fail spec validation",
|
|
1308
|
+
tags: ["metadata"]
|
|
1309
|
+
}
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1186
1312
|
if (metadata.endpoints.items !== false) {
|
|
1187
1313
|
this.routeManager.register({
|
|
1188
1314
|
method: "GET",
|
|
@@ -1197,7 +1323,22 @@ var RestServer = class {
|
|
|
1197
1323
|
packageId,
|
|
1198
1324
|
...environmentId ? { environmentId } : {}
|
|
1199
1325
|
});
|
|
1200
|
-
|
|
1326
|
+
let visible = items;
|
|
1327
|
+
if (req.params.type === "app") {
|
|
1328
|
+
const raw = items;
|
|
1329
|
+
const list = Array.isArray(raw) ? raw : raw && typeof raw === "object" && Array.isArray(raw.items) ? raw.items : null;
|
|
1330
|
+
if (list) {
|
|
1331
|
+
const ctx = await this.resolveExecCtx(environmentId, req).catch(() => void 0);
|
|
1332
|
+
if (ctx?.userId) {
|
|
1333
|
+
const sysPerms = new Set(
|
|
1334
|
+
Array.isArray(ctx.systemPermissions) ? ctx.systemPermissions : []
|
|
1335
|
+
);
|
|
1336
|
+
const filtered = list.map((it) => this.filterAppForUser(it, sysPerms)).filter((it) => it != null);
|
|
1337
|
+
visible = Array.isArray(raw) ? filtered : { ...raw, items: filtered };
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
const translated = await this.translateMetaItems(req, req.params.type, environmentId, visible);
|
|
1201
1342
|
res.header("Vary", "Accept-Language");
|
|
1202
1343
|
res.json(translated);
|
|
1203
1344
|
} catch (error) {
|
|
@@ -1256,7 +1397,9 @@ var RestServer = class {
|
|
|
1256
1397
|
res.json(layered);
|
|
1257
1398
|
return;
|
|
1258
1399
|
}
|
|
1259
|
-
|
|
1400
|
+
const isAppType = req.params.type === "app";
|
|
1401
|
+
const isDraftRead = typeof req.query?.state === "string" && req.query.state.toLowerCase() === "draft";
|
|
1402
|
+
if (metadata.enableCache && p.getMetaItemCached && !isAppType && !isDraftRead) {
|
|
1260
1403
|
const cacheRequest = {
|
|
1261
1404
|
ifNoneMatch: req.headers["if-none-match"],
|
|
1262
1405
|
ifModifiedSince: req.headers["if-modified-since"]
|
|
@@ -1287,13 +1430,32 @@ var RestServer = class {
|
|
|
1287
1430
|
res.json(await this.translateMetaItem(req, req.params.type, environmentId, result.data));
|
|
1288
1431
|
} else {
|
|
1289
1432
|
const packageId = req.query?.package || void 0;
|
|
1433
|
+
const stateParam = typeof req.query?.state === "string" ? req.query.state.toLowerCase() : void 0;
|
|
1290
1434
|
const item = await p.getMetaItem({
|
|
1291
1435
|
type: req.params.type,
|
|
1292
1436
|
name: req.params.name,
|
|
1293
|
-
packageId
|
|
1437
|
+
packageId,
|
|
1438
|
+
...stateParam === "draft" ? { state: "draft" } : {}
|
|
1294
1439
|
});
|
|
1440
|
+
let visible = item;
|
|
1441
|
+
if (isAppType && item) {
|
|
1442
|
+
const ctx = await this.resolveExecCtx(environmentId, req).catch(() => void 0);
|
|
1443
|
+
if (ctx?.userId) {
|
|
1444
|
+
const sysPerms = new Set(
|
|
1445
|
+
Array.isArray(ctx.systemPermissions) ? ctx.systemPermissions : []
|
|
1446
|
+
);
|
|
1447
|
+
visible = this.filterAppForUser(item, sysPerms);
|
|
1448
|
+
if (visible == null) {
|
|
1449
|
+
res.status(404).json({
|
|
1450
|
+
error: "not_found",
|
|
1451
|
+
message: "Metadata item not found or access denied."
|
|
1452
|
+
});
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1295
1457
|
res.header("Vary", "Accept-Language");
|
|
1296
|
-
res.json(await this.translateMetaItem(req, req.params.type, environmentId,
|
|
1458
|
+
res.json(await this.translateMetaItem(req, req.params.type, environmentId, visible));
|
|
1297
1459
|
}
|
|
1298
1460
|
} catch (error) {
|
|
1299
1461
|
logError("[REST] Unhandled error:", error);
|
|
@@ -1332,7 +1494,8 @@ var RestServer = class {
|
|
|
1332
1494
|
...environmentId ? { environmentId } : {},
|
|
1333
1495
|
...parentVersion !== void 0 ? { parentVersion } : {},
|
|
1334
1496
|
...actor ? { actor } : {},
|
|
1335
|
-
...force ? { force: true } : {}
|
|
1497
|
+
...force ? { force: true } : {},
|
|
1498
|
+
...typeof req.query?.mode === "string" && req.query.mode.toLowerCase() === "draft" ? { mode: "draft" } : {}
|
|
1336
1499
|
});
|
|
1337
1500
|
res.json(result);
|
|
1338
1501
|
} catch (error) {
|
|
@@ -1362,12 +1525,14 @@ var RestServer = class {
|
|
|
1362
1525
|
const parentVersion = typeof ifMatchHeader === "string" ? ifMatchHeader.replace(/^"|"$/g, "") : void 0;
|
|
1363
1526
|
const actorHeader = req.headers?.["x-actor"] ?? req.headers?.["X-Actor"] ?? req.user?.id ?? req.userId;
|
|
1364
1527
|
const actor = typeof actorHeader === "string" ? actorHeader : void 0;
|
|
1528
|
+
const stateParam = typeof req.query?.state === "string" && req.query.state.toLowerCase() === "draft" ? "draft" : void 0;
|
|
1365
1529
|
const result = await p.deleteMetaItem({
|
|
1366
1530
|
type: req.params.type,
|
|
1367
1531
|
name: req.params.name,
|
|
1368
1532
|
...environmentId ? { environmentId } : {},
|
|
1369
1533
|
...parentVersion !== void 0 ? { parentVersion } : {},
|
|
1370
|
-
...actor ? { actor } : {}
|
|
1534
|
+
...actor ? { actor } : {},
|
|
1535
|
+
...stateParam ? { state: stateParam } : {}
|
|
1371
1536
|
});
|
|
1372
1537
|
res.json(result);
|
|
1373
1538
|
} catch (error) {
|
|
@@ -1413,6 +1578,124 @@ var RestServer = class {
|
|
|
1413
1578
|
tags: ["metadata"]
|
|
1414
1579
|
}
|
|
1415
1580
|
});
|
|
1581
|
+
this.routeManager.register({
|
|
1582
|
+
method: "POST",
|
|
1583
|
+
path: `${metaPath}/:type/:name/publish`,
|
|
1584
|
+
handler: async (req, res) => {
|
|
1585
|
+
try {
|
|
1586
|
+
const environmentId = isScoped ? req.params?.environmentId : void 0;
|
|
1587
|
+
const p = await this.resolveProtocol(environmentId, req);
|
|
1588
|
+
if (!p.publishMetaItem) {
|
|
1589
|
+
res.status(501).json({
|
|
1590
|
+
error: "Publish operation not supported by protocol implementation"
|
|
1591
|
+
});
|
|
1592
|
+
return;
|
|
1593
|
+
}
|
|
1594
|
+
const actorHeader = req.headers?.["x-actor"] ?? req.headers?.["X-Actor"] ?? req.user?.id ?? req.userId;
|
|
1595
|
+
const actor = typeof actorHeader === "string" ? actorHeader : void 0;
|
|
1596
|
+
const body = req.body && typeof req.body === "object" ? req.body : {};
|
|
1597
|
+
const message = typeof body.message === "string" ? body.message : void 0;
|
|
1598
|
+
const result = await p.publishMetaItem({
|
|
1599
|
+
type: req.params.type,
|
|
1600
|
+
name: req.params.name,
|
|
1601
|
+
...environmentId ? { environmentId } : {},
|
|
1602
|
+
...actor ? { actor } : {},
|
|
1603
|
+
...message ? { message } : {}
|
|
1604
|
+
});
|
|
1605
|
+
res.json(result);
|
|
1606
|
+
} catch (error) {
|
|
1607
|
+
logError("[REST] Unhandled error:", error);
|
|
1608
|
+
sendError(res, error);
|
|
1609
|
+
}
|
|
1610
|
+
},
|
|
1611
|
+
metadata: {
|
|
1612
|
+
summary: "Publish the pending draft overlay (promotes draft \u2192 active)",
|
|
1613
|
+
tags: ["metadata"]
|
|
1614
|
+
}
|
|
1615
|
+
});
|
|
1616
|
+
this.routeManager.register({
|
|
1617
|
+
method: "POST",
|
|
1618
|
+
path: `${metaPath}/:type/:name/rollback`,
|
|
1619
|
+
handler: async (req, res) => {
|
|
1620
|
+
try {
|
|
1621
|
+
const environmentId = isScoped ? req.params?.environmentId : void 0;
|
|
1622
|
+
const p = await this.resolveProtocol(environmentId, req);
|
|
1623
|
+
if (!p.rollbackMetaItem) {
|
|
1624
|
+
res.status(501).json({
|
|
1625
|
+
error: "Rollback operation not supported by protocol implementation"
|
|
1626
|
+
});
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
const body = req.body && typeof req.body === "object" ? req.body : {};
|
|
1630
|
+
const toVersionRaw = body.toVersion ?? body.version ?? req.query?.toVersion;
|
|
1631
|
+
const toVersion = Number(toVersionRaw);
|
|
1632
|
+
if (!Number.isFinite(toVersion) || toVersion < 1) {
|
|
1633
|
+
res.status(400).json({
|
|
1634
|
+
error: `'toVersion' (positive integer) is required`,
|
|
1635
|
+
code: "invalid_request"
|
|
1636
|
+
});
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
const actorHeader = req.headers?.["x-actor"] ?? req.headers?.["X-Actor"] ?? req.user?.id ?? req.userId;
|
|
1640
|
+
const actor = typeof actorHeader === "string" ? actorHeader : void 0;
|
|
1641
|
+
const message = typeof body.message === "string" ? body.message : void 0;
|
|
1642
|
+
const result = await p.rollbackMetaItem({
|
|
1643
|
+
type: req.params.type,
|
|
1644
|
+
name: req.params.name,
|
|
1645
|
+
toVersion,
|
|
1646
|
+
...environmentId ? { environmentId } : {},
|
|
1647
|
+
...actor ? { actor } : {},
|
|
1648
|
+
...message ? { message } : {}
|
|
1649
|
+
});
|
|
1650
|
+
res.json(result);
|
|
1651
|
+
} catch (error) {
|
|
1652
|
+
logError("[REST] Unhandled error:", error);
|
|
1653
|
+
sendError(res, error);
|
|
1654
|
+
}
|
|
1655
|
+
},
|
|
1656
|
+
metadata: {
|
|
1657
|
+
summary: "Restore the body at the given history version as the new live row",
|
|
1658
|
+
tags: ["metadata"]
|
|
1659
|
+
}
|
|
1660
|
+
});
|
|
1661
|
+
this.routeManager.register({
|
|
1662
|
+
method: "GET",
|
|
1663
|
+
path: `${metaPath}/:type/:name/diff`,
|
|
1664
|
+
handler: async (req, res) => {
|
|
1665
|
+
try {
|
|
1666
|
+
const environmentId = isScoped ? req.params?.environmentId : void 0;
|
|
1667
|
+
const p = await this.resolveProtocol(environmentId, req);
|
|
1668
|
+
if (!p.diffMetaItem) {
|
|
1669
|
+
res.status(501).json({
|
|
1670
|
+
error: "Diff operation not supported by protocol implementation"
|
|
1671
|
+
});
|
|
1672
|
+
return;
|
|
1673
|
+
}
|
|
1674
|
+
const parseV = (raw) => {
|
|
1675
|
+
if (raw === void 0 || raw === null || raw === "") return void 0;
|
|
1676
|
+
const n = Number(raw);
|
|
1677
|
+
return Number.isFinite(n) ? n : void 0;
|
|
1678
|
+
};
|
|
1679
|
+
const fromVersion = parseV(req.query?.from ?? req.query?.fromVersion);
|
|
1680
|
+
const toVersion = parseV(req.query?.to ?? req.query?.toVersion);
|
|
1681
|
+
const result = await p.diffMetaItem({
|
|
1682
|
+
type: req.params.type,
|
|
1683
|
+
name: req.params.name,
|
|
1684
|
+
...environmentId ? { environmentId } : {},
|
|
1685
|
+
...fromVersion !== void 0 ? { fromVersion } : {},
|
|
1686
|
+
...toVersion !== void 0 ? { toVersion } : {}
|
|
1687
|
+
});
|
|
1688
|
+
res.json(result);
|
|
1689
|
+
} catch (error) {
|
|
1690
|
+
logError("[REST] Unhandled error:", error);
|
|
1691
|
+
sendError(res, error);
|
|
1692
|
+
}
|
|
1693
|
+
},
|
|
1694
|
+
metadata: {
|
|
1695
|
+
summary: "Diff two metadata versions (from/to query params; omit for previous-vs-current)",
|
|
1696
|
+
tags: ["metadata"]
|
|
1697
|
+
}
|
|
1698
|
+
});
|
|
1416
1699
|
if (metadata.endpoints.item !== false) {
|
|
1417
1700
|
this.routeManager.register({
|
|
1418
1701
|
method: "GET",
|