@objectstack/rest 7.2.1 → 7.4.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 CHANGED
@@ -250,6 +250,7 @@ var RouteGroupBuilder = class {
250
250
  // src/rest-server.ts
251
251
  var import_meta = {};
252
252
  var logError = (...args) => globalThis.console?.error(...args);
253
+ var TRANSLATABLE_META_TYPES = /* @__PURE__ */ new Set(["view", "action", "object", "app", "dashboard"]);
253
254
  function mapDataError(error, object) {
254
255
  if (error?.code === "CONCURRENT_UPDATE" || error?.name === "ConcurrentUpdateError") {
255
256
  return {
@@ -859,10 +860,10 @@ var RestServer = class {
859
860
  * locale yields a match. Falls through unchanged for unsupported types
860
861
  * or missing translations.
861
862
  */
862
- async translateMetaItem(req, type, environmentId, item) {
863
+ async translateMetaItem(req, type, environmentId, item, i18nService) {
863
864
  if (!item || typeof item !== "object") return item;
864
- if (type !== "view" && type !== "action" && type !== "object") return item;
865
- const i18n = await this.resolveI18nService(environmentId, req);
865
+ if (!TRANSLATABLE_META_TYPES.has(type)) return item;
866
+ const i18n = i18nService !== void 0 ? i18nService : await this.resolveI18nService(environmentId, req);
866
867
  const bundle = this.buildTranslationBundle(i18n);
867
868
  if (!bundle) return item;
868
869
  const locale = this.extractLocale(req, i18n);
@@ -875,7 +876,7 @@ var RestServer = class {
875
876
  */
876
877
  async translateMetaItems(req, type, environmentId, items) {
877
878
  if (!Array.isArray(items)) return items;
878
- if (type !== "view" && type !== "action" && type !== "object") return items;
879
+ if (!TRANSLATABLE_META_TYPES.has(type)) return items;
879
880
  const i18n = await this.resolveI18nService(environmentId, req);
880
881
  const bundle = this.buildTranslationBundle(i18n);
881
882
  if (!bundle) return items;
@@ -1378,6 +1379,15 @@ var RestServer = class {
1378
1379
  }
1379
1380
  }
1380
1381
  }
1382
+ if (req.params.type === "view" && req.query?.object) {
1383
+ const obj = String(req.query.object);
1384
+ const raw = visible;
1385
+ const list = Array.isArray(raw) ? raw : raw && typeof raw === "object" && Array.isArray(raw.items) ? raw.items : null;
1386
+ if (list) {
1387
+ const filtered = list.filter((v) => v && typeof v === "object" && v.viewKind && v.object === obj).sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || String(a.name).localeCompare(String(b.name)));
1388
+ visible = Array.isArray(raw) ? filtered : { ...raw, items: filtered };
1389
+ }
1390
+ }
1381
1391
  const translated = await this.translateMetaItems(req, req.params.type, environmentId, visible);
1382
1392
  res.header("Vary", "Accept-Language");
1383
1393
  res.json(translated);
@@ -1444,10 +1454,13 @@ var RestServer = class {
1444
1454
  ifNoneMatch: req.headers["if-none-match"],
1445
1455
  ifModifiedSince: req.headers["if-modified-since"]
1446
1456
  };
1457
+ const cacheI18n = await this.resolveI18nService(environmentId, req);
1458
+ const cacheLocale = this.extractLocale(req, cacheI18n);
1447
1459
  const result = await p.getMetaItemCached({
1448
1460
  type: req.params.type,
1449
1461
  name: req.params.name,
1450
1462
  cacheRequest,
1463
+ ...cacheLocale ? { locale: cacheLocale } : {},
1451
1464
  ...environmentId ? { environmentId } : {}
1452
1465
  });
1453
1466
  if (result.notModified) {
@@ -1467,7 +1480,7 @@ var RestServer = class {
1467
1480
  res.header("Cache-Control", directives + maxAge);
1468
1481
  }
1469
1482
  res.header("Vary", "Accept-Language");
1470
- res.json(await this.translateMetaItem(req, req.params.type, environmentId, result.data));
1483
+ res.json(await this.translateMetaItem(req, req.params.type, environmentId, result.data, cacheI18n));
1471
1484
  } else {
1472
1485
  const packageId = req.query?.package || void 0;
1473
1486
  const stateParam = typeof req.query?.state === "string" ? req.query.state.toLowerCase() : void 0;
@@ -1527,6 +1540,8 @@ var RestServer = class {
1527
1540
  const actor = typeof actorHeader === "string" ? actorHeader : void 0;
1528
1541
  const forceRaw = req.query?.force;
1529
1542
  const force = typeof forceRaw === "string" ? ["true", "1", "yes", "on"].includes(forceRaw.toLowerCase()) : !!forceRaw;
1543
+ const packageRaw = req.query?.package;
1544
+ const packageId = typeof packageRaw === "string" && packageRaw && packageRaw !== "all" ? packageRaw : void 0;
1530
1545
  const result = await p.saveMetaItem({
1531
1546
  type: req.params.type,
1532
1547
  name: req.params.name,
@@ -1535,6 +1550,7 @@ var RestServer = class {
1535
1550
  ...parentVersion !== void 0 ? { parentVersion } : {},
1536
1551
  ...actor ? { actor } : {},
1537
1552
  ...force ? { force: true } : {},
1553
+ ...packageId ? { packageId } : {},
1538
1554
  ...typeof req.query?.mode === "string" && req.query.mode.toLowerCase() === "draft" ? { mode: "draft" } : {}
1539
1555
  });
1540
1556
  res.json(result);
@@ -1809,13 +1825,16 @@ var RestServer = class {
1809
1825
  const parentVersion = typeof ifMatchHeader === "string" ? ifMatchHeader.replace(/^"|"$/g, "") : void 0;
1810
1826
  const actorHeader = req.headers?.["x-actor"] ?? req.headers?.["X-Actor"] ?? req.user?.id ?? req.userId;
1811
1827
  const actor = typeof actorHeader === "string" ? actorHeader : void 0;
1828
+ const packageRaw = req.query?.package;
1829
+ const packageId = typeof packageRaw === "string" && packageRaw && packageRaw !== "all" ? packageRaw : void 0;
1812
1830
  const result = await p.saveMetaItem({
1813
1831
  type: req.params.type,
1814
1832
  name: compoundName,
1815
1833
  item: req.body,
1816
1834
  ...environmentId ? { environmentId } : {},
1817
1835
  ...parentVersion !== void 0 ? { parentVersion } : {},
1818
- ...actor ? { actor } : {}
1836
+ ...actor ? { actor } : {},
1837
+ ...packageId ? { packageId } : {}
1819
1838
  });
1820
1839
  res.json(result);
1821
1840
  } catch (error) {
@@ -3297,19 +3316,17 @@ var RestServer = class {
3297
3316
  });
3298
3317
  }
3299
3318
  /**
3300
- * Register approval engine endpoints.
3319
+ * Register approval endpoints (ADR-0019: approval as a flow node).
3320
+ *
3321
+ * Approval is no longer a standalone process engine — a flow's Approval
3322
+ * node opens a request and suspends the run; a decision resumes it. There
3323
+ * are no process-authoring or submit routes anymore.
3301
3324
  *
3302
3325
  * Routes (all under {basePath}/approvals):
3303
- * GET /processes — list approval processes
3304
- * POST /processes — upsert (defineProcess)
3305
- * GET /processes/:id — get by id or name
3306
- * DELETE /processes/:id — delete process
3307
- * POST /requests — submit
3308
3326
  * GET /requests — list (filters: status, object, recordId, approverId, submitterId)
3309
3327
  * GET /requests/:id — get request
3310
- * POST /requests/:id/approve — approve current step
3311
- * POST /requests/:id/reject — reject current step
3312
- * POST /requests/:id/recall — recall (submitter only)
3328
+ * POST /requests/:id/approve — record an approve decision (resumes the flow)
3329
+ * POST /requests/:id/reject — record a reject decision (resumes the flow)
3313
3330
  * GET /requests/:id/actions — audit trail
3314
3331
  *
3315
3332
  * Returns 501 when `approvalsServiceProvider` is unset so deployments
@@ -3337,8 +3354,6 @@ var RestServer = class {
3337
3354
  [/^DUPLICATE_REQUEST/, 409, "DUPLICATE_REQUEST"],
3338
3355
  [/^INVALID_STATE/, 409, "INVALID_STATE"],
3339
3356
  [/^FORBIDDEN/, 403, "FORBIDDEN"],
3340
- [/^NO_ACTIVE_PROCESS/, 404, "NO_ACTIVE_PROCESS"],
3341
- [/^PROCESS_NOT_FOUND/, 404, "PROCESS_NOT_FOUND"],
3342
3357
  [/^REQUEST_NOT_FOUND/, 404, "REQUEST_NOT_FOUND"]
3343
3358
  ];
3344
3359
  for (const [re, status, code] of mapping) {
@@ -3349,127 +3364,6 @@ var RestServer = class {
3349
3364
  }
3350
3365
  return false;
3351
3366
  };
3352
- this.routeManager.register({
3353
- method: "GET",
3354
- path: `${dataPath}/approvals/processes`,
3355
- handler: async (req, res) => {
3356
- try {
3357
- const environmentId = isScoped ? req.params?.environmentId : void 0;
3358
- const context = await this.resolveExecCtx(environmentId, req);
3359
- if (this.enforceAuth(req, res, context)) return;
3360
- const svc = await resolveService(environmentId);
3361
- if (!svc) return respond501(res);
3362
- const q = req.query ?? {};
3363
- const rows = await svc.listProcesses({
3364
- object: q.object,
3365
- activeOnly: q.activeOnly === "true" || q.activeOnly === true
3366
- }, context ?? {});
3367
- res.json({ data: rows });
3368
- } catch (error) {
3369
- logError("[REST] List approval processes error:", error);
3370
- res.status(500).json({ code: "APPROVAL_PROCESS_LIST_FAILED", error: String(error?.message ?? error).slice(0, 500) });
3371
- }
3372
- },
3373
- metadata: { summary: "List approval processes", tags: ["approvals"] }
3374
- });
3375
- this.routeManager.register({
3376
- method: "POST",
3377
- path: `${dataPath}/approvals/processes`,
3378
- handler: async (req, res) => {
3379
- try {
3380
- const environmentId = isScoped ? req.params?.environmentId : void 0;
3381
- const context = await this.resolveExecCtx(environmentId, req);
3382
- if (this.enforceAuth(req, res, context)) return;
3383
- const svc = await resolveService(environmentId);
3384
- if (!svc) return respond501(res);
3385
- try {
3386
- const row = await svc.defineProcess(req.body ?? {}, context ?? {});
3387
- res.status(201).json(row);
3388
- } catch (err) {
3389
- if (handleApprovalError(res, err)) return;
3390
- throw err;
3391
- }
3392
- } catch (error) {
3393
- logError("[REST] Define approval process error:", error);
3394
- res.status(500).json({ code: "APPROVAL_PROCESS_DEFINE_FAILED", error: String(error?.message ?? error).slice(0, 500) });
3395
- }
3396
- },
3397
- metadata: { summary: "Define (upsert) an approval process", tags: ["approvals"] }
3398
- });
3399
- this.routeManager.register({
3400
- method: "GET",
3401
- path: `${dataPath}/approvals/processes/:id`,
3402
- handler: async (req, res) => {
3403
- try {
3404
- const environmentId = isScoped ? req.params?.environmentId : void 0;
3405
- const context = await this.resolveExecCtx(environmentId, req);
3406
- if (this.enforceAuth(req, res, context)) return;
3407
- const svc = await resolveService(environmentId);
3408
- if (!svc) return respond501(res);
3409
- const row = await svc.getProcess(req.params.id, context ?? {});
3410
- if (!row) {
3411
- res.status(404).json({ code: "PROCESS_NOT_FOUND", error: `Approval process '${req.params.id}' not found` });
3412
- return;
3413
- }
3414
- res.json(row);
3415
- } catch (error) {
3416
- logError("[REST] Get approval process error:", error);
3417
- res.status(500).json({ code: "APPROVAL_PROCESS_GET_FAILED", error: String(error?.message ?? error).slice(0, 500) });
3418
- }
3419
- },
3420
- metadata: { summary: "Get an approval process by id or name", tags: ["approvals"] }
3421
- });
3422
- this.routeManager.register({
3423
- method: "DELETE",
3424
- path: `${dataPath}/approvals/processes/:id`,
3425
- handler: async (req, res) => {
3426
- try {
3427
- const environmentId = isScoped ? req.params?.environmentId : void 0;
3428
- const context = await this.resolveExecCtx(environmentId, req);
3429
- if (this.enforceAuth(req, res, context)) return;
3430
- const svc = await resolveService(environmentId);
3431
- if (!svc) return respond501(res);
3432
- await svc.deleteProcess(req.params.id, context ?? {});
3433
- res.status(204).end();
3434
- } catch (error) {
3435
- logError("[REST] Delete approval process error:", error);
3436
- res.status(500).json({ code: "APPROVAL_PROCESS_DELETE_FAILED", error: String(error?.message ?? error).slice(0, 500) });
3437
- }
3438
- },
3439
- metadata: { summary: "Delete an approval process", tags: ["approvals"] }
3440
- });
3441
- this.routeManager.register({
3442
- method: "POST",
3443
- path: `${dataPath}/approvals/requests`,
3444
- handler: async (req, res) => {
3445
- try {
3446
- const environmentId = isScoped ? req.params?.environmentId : void 0;
3447
- const context = await this.resolveExecCtx(environmentId, req);
3448
- if (this.enforceAuth(req, res, context)) return;
3449
- const svc = await resolveService(environmentId);
3450
- if (!svc) return respond501(res);
3451
- const body = req.body ?? {};
3452
- try {
3453
- const row = await svc.submit({
3454
- object: body.object,
3455
- recordId: body.recordId ?? body.record_id,
3456
- processName: body.processName ?? body.process_name,
3457
- submitterId: body.submitterId ?? body.submitter_id ?? context?.userId,
3458
- comment: body.comment,
3459
- payload: body.payload
3460
- }, context ?? {});
3461
- res.status(201).json(row);
3462
- } catch (err) {
3463
- if (handleApprovalError(res, err)) return;
3464
- throw err;
3465
- }
3466
- } catch (error) {
3467
- logError("[REST] Submit approval error:", error);
3468
- res.status(500).json({ code: "APPROVAL_SUBMIT_FAILED", error: String(error?.message ?? error).slice(0, 500) });
3469
- }
3470
- },
3471
- metadata: { summary: "Submit a record for approval", tags: ["approvals"] }
3472
- });
3473
3367
  this.routeManager.register({
3474
3368
  method: "GET",
3475
3369
  path: `${dataPath}/approvals/requests`,
@@ -3522,10 +3416,10 @@ var RestServer = class {
3522
3416
  },
3523
3417
  metadata: { summary: "Get an approval request by id", tags: ["approvals"] }
3524
3418
  });
3525
- const decisionRoute = (suffix, method) => {
3419
+ const decisionRoute = (decision) => {
3526
3420
  this.routeManager.register({
3527
3421
  method: "POST",
3528
- path: `${dataPath}/approvals/requests/:id/${suffix}`,
3422
+ path: `${dataPath}/approvals/requests/:id/${decision}`,
3529
3423
  handler: async (req, res) => {
3530
3424
  try {
3531
3425
  const environmentId = isScoped ? req.params?.environmentId : void 0;
@@ -3535,7 +3429,8 @@ var RestServer = class {
3535
3429
  if (!svc) return respond501(res);
3536
3430
  const body = req.body ?? {};
3537
3431
  try {
3538
- const out = await svc[method](req.params.id, {
3432
+ const out = await svc.decide(req.params.id, {
3433
+ decision,
3539
3434
  actorId: body.actorId ?? body.actor_id ?? context?.userId,
3540
3435
  comment: body.comment
3541
3436
  }, context ?? {});
@@ -3545,16 +3440,15 @@ var RestServer = class {
3545
3440
  throw err;
3546
3441
  }
3547
3442
  } catch (error) {
3548
- logError(`[REST] ${suffix} approval error:`, error);
3549
- res.status(500).json({ code: `APPROVAL_${suffix.toUpperCase()}_FAILED`, error: String(error?.message ?? error).slice(0, 500) });
3443
+ logError(`[REST] ${decision} approval error:`, error);
3444
+ res.status(500).json({ code: `APPROVAL_${decision.toUpperCase()}_FAILED`, error: String(error?.message ?? error).slice(0, 500) });
3550
3445
  }
3551
3446
  },
3552
- metadata: { summary: `${suffix[0].toUpperCase()}${suffix.slice(1)} an approval request`, tags: ["approvals"] }
3447
+ metadata: { summary: `${decision[0].toUpperCase()}${decision.slice(1)} an approval request`, tags: ["approvals"] }
3553
3448
  });
3554
3449
  };
3555
- decisionRoute("approve", "approve");
3556
- decisionRoute("reject", "reject");
3557
- decisionRoute("recall", "recall");
3450
+ decisionRoute("approve");
3451
+ decisionRoute("reject");
3558
3452
  this.routeManager.register({
3559
3453
  method: "GET",
3560
3454
  path: `${dataPath}/approvals/requests/:id/actions`,
@@ -3827,6 +3721,66 @@ function registerPackageRoutes(server, packageService, basePath = "/api/v1", opt
3827
3721
  });
3828
3722
  }
3829
3723
 
3724
+ // src/external-datasource-routes.ts
3725
+ function registerExternalDatasourceRoutes(server, ctx, basePath = "/api/v1") {
3726
+ const ext = `${basePath}/datasources/:name/external`;
3727
+ const externalService = () => {
3728
+ try {
3729
+ return ctx.getService("external-datasource");
3730
+ } catch {
3731
+ return void 0;
3732
+ }
3733
+ };
3734
+ const unavailable = (res) => res.status(503).json({ error: "external_service_unavailable" });
3735
+ server.get(`${ext}/tables`, async (req, res) => {
3736
+ const svc = externalService();
3737
+ if (!svc?.listRemoteTables) return unavailable(res);
3738
+ const schema = typeof req.query?.schema === "string" ? req.query.schema : void 0;
3739
+ const tables = await svc.listRemoteTables(req.params.name, { schema });
3740
+ res.json({ tables });
3741
+ });
3742
+ server.post(`${ext}/tables/:remote/draft`, async (req, res) => {
3743
+ const svc = externalService();
3744
+ if (!svc?.generateObjectDraft) return unavailable(res);
3745
+ const draft = await svc.generateObjectDraft(
3746
+ req.params.name,
3747
+ req.params.remote,
3748
+ req.body ?? {}
3749
+ );
3750
+ res.json({ draft });
3751
+ });
3752
+ server.post(`${ext}/tables/:remote/import`, async (req, res) => {
3753
+ const svc = externalService();
3754
+ if (!svc?.importObject) return unavailable(res);
3755
+ try {
3756
+ const result = await svc.importObject(
3757
+ req.params.name,
3758
+ req.params.remote,
3759
+ req.body ?? {}
3760
+ );
3761
+ res.status(201).json({ object: result });
3762
+ } catch (err) {
3763
+ res.status(400).json({
3764
+ error: "external_import_error",
3765
+ message: err instanceof Error ? err.message : String(err)
3766
+ });
3767
+ }
3768
+ });
3769
+ server.post(`${ext}/refresh-catalog`, async (req, res) => {
3770
+ const svc = externalService();
3771
+ if (!svc?.refreshCatalog) return unavailable(res);
3772
+ const catalog = await svc.refreshCatalog(req.params.name);
3773
+ res.json({ catalog });
3774
+ });
3775
+ server.post(`${ext}/validate`, async (req, res) => {
3776
+ const svc = externalService();
3777
+ if (!svc?.validateAll) return unavailable(res);
3778
+ const report = await svc.validateAll();
3779
+ const results = (report.results ?? []).filter((r) => r.datasource === req.params.name);
3780
+ res.json({ ok: results.every((r) => r.ok), results });
3781
+ });
3782
+ }
3783
+
3830
3784
  // src/rest-api-plugin.ts
3831
3785
  function createRestApiPlugin(config = {}) {
3832
3786
  return {
@@ -3939,14 +3893,14 @@ function createRestApiPlugin(config = {}) {
3939
3893
  ctx.logger.error("Failed to register REST API routes", { error: err.message });
3940
3894
  throw err;
3941
3895
  }
3896
+ const basePath = config.api?.api?.basePath || "/api";
3897
+ const version = config.api?.api?.version || "v1";
3898
+ const versionedBase = `${basePath}/${version}`;
3899
+ const enableProjectScoping = config.api?.api?.enableProjectScoping ?? false;
3900
+ const projectResolution = config.api?.api?.projectResolution ?? "auto";
3942
3901
  try {
3943
3902
  const packageService = ctx.getService("package");
3944
3903
  if (packageService) {
3945
- const basePath = config.api?.api?.basePath || "/api";
3946
- const version = config.api?.api?.version || "v1";
3947
- const versionedBase = `${basePath}/${version}`;
3948
- const enableProjectScoping = config.api?.api?.enableProjectScoping ?? false;
3949
- const projectResolution = config.api?.api?.projectResolution ?? "auto";
3950
3904
  if (enableProjectScoping && projectResolution === "required") {
3951
3905
  registerPackageRoutes(server, packageService, `${versionedBase}/environments/:environmentId`, {
3952
3906
  protocol
@@ -3964,6 +3918,12 @@ function createRestApiPlugin(config = {}) {
3964
3918
  } catch (e) {
3965
3919
  ctx.logger.debug("Package service not available, package routes skipped");
3966
3920
  }
3921
+ try {
3922
+ registerExternalDatasourceRoutes(server, ctx, versionedBase);
3923
+ ctx.logger.info("Datasource federation routes registered");
3924
+ } catch (e) {
3925
+ ctx.logger.warn("Datasource federation routes registration failed", { error: e?.message });
3926
+ }
3967
3927
  }
3968
3928
  };
3969
3929
  }