@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.d.cts CHANGED
@@ -521,19 +521,17 @@ declare class RestServer {
521
521
  */
522
522
  private registerReportsEndpoints;
523
523
  /**
524
- * Register approval engine endpoints.
524
+ * Register approval endpoints (ADR-0019: approval as a flow node).
525
+ *
526
+ * Approval is no longer a standalone process engine — a flow's Approval
527
+ * node opens a request and suspends the run; a decision resumes it. There
528
+ * are no process-authoring or submit routes anymore.
525
529
  *
526
530
  * Routes (all under {basePath}/approvals):
527
- * GET /processes — list approval processes
528
- * POST /processes — upsert (defineProcess)
529
- * GET /processes/:id — get by id or name
530
- * DELETE /processes/:id — delete process
531
- * POST /requests — submit
532
531
  * GET /requests — list (filters: status, object, recordId, approverId, submitterId)
533
532
  * GET /requests/:id — get request
534
- * POST /requests/:id/approve — approve current step
535
- * POST /requests/:id/reject — reject current step
536
- * POST /requests/:id/recall — recall (submitter only)
533
+ * POST /requests/:id/approve — record an approve decision (resumes the flow)
534
+ * POST /requests/:id/reject — record a reject decision (resumes the flow)
537
535
  * GET /requests/:id/actions — audit trail
538
536
  *
539
537
  * Returns 501 when `approvalsServiceProvider` is unset so deployments
package/dist/index.d.ts CHANGED
@@ -521,19 +521,17 @@ declare class RestServer {
521
521
  */
522
522
  private registerReportsEndpoints;
523
523
  /**
524
- * Register approval engine endpoints.
524
+ * Register approval endpoints (ADR-0019: approval as a flow node).
525
+ *
526
+ * Approval is no longer a standalone process engine — a flow's Approval
527
+ * node opens a request and suspends the run; a decision resumes it. There
528
+ * are no process-authoring or submit routes anymore.
525
529
  *
526
530
  * Routes (all under {basePath}/approvals):
527
- * GET /processes — list approval processes
528
- * POST /processes — upsert (defineProcess)
529
- * GET /processes/:id — get by id or name
530
- * DELETE /processes/:id — delete process
531
- * POST /requests — submit
532
531
  * GET /requests — list (filters: status, object, recordId, approverId, submitterId)
533
532
  * GET /requests/:id — get request
534
- * POST /requests/:id/approve — approve current step
535
- * POST /requests/:id/reject — reject current step
536
- * POST /requests/:id/recall — recall (submitter only)
533
+ * POST /requests/:id/approve — record an approve decision (resumes the flow)
534
+ * POST /requests/:id/reject — record a reject decision (resumes the flow)
537
535
  * GET /requests/:id/actions — audit trail
538
536
  *
539
537
  * Returns 501 when `approvalsServiceProvider` is unset so deployments
package/dist/index.js CHANGED
@@ -210,6 +210,7 @@ var RouteGroupBuilder = class {
210
210
 
211
211
  // src/rest-server.ts
212
212
  var logError = (...args) => globalThis.console?.error(...args);
213
+ var TRANSLATABLE_META_TYPES = /* @__PURE__ */ new Set(["view", "action", "object", "app", "dashboard"]);
213
214
  function mapDataError(error, object) {
214
215
  if (error?.code === "CONCURRENT_UPDATE" || error?.name === "ConcurrentUpdateError") {
215
216
  return {
@@ -819,10 +820,10 @@ var RestServer = class {
819
820
  * locale yields a match. Falls through unchanged for unsupported types
820
821
  * or missing translations.
821
822
  */
822
- async translateMetaItem(req, type, environmentId, item) {
823
+ async translateMetaItem(req, type, environmentId, item, i18nService) {
823
824
  if (!item || typeof item !== "object") return item;
824
- if (type !== "view" && type !== "action" && type !== "object") return item;
825
- const i18n = await this.resolveI18nService(environmentId, req);
825
+ if (!TRANSLATABLE_META_TYPES.has(type)) return item;
826
+ const i18n = i18nService !== void 0 ? i18nService : await this.resolveI18nService(environmentId, req);
826
827
  const bundle = this.buildTranslationBundle(i18n);
827
828
  if (!bundle) return item;
828
829
  const locale = this.extractLocale(req, i18n);
@@ -835,7 +836,7 @@ var RestServer = class {
835
836
  */
836
837
  async translateMetaItems(req, type, environmentId, items) {
837
838
  if (!Array.isArray(items)) return items;
838
- if (type !== "view" && type !== "action" && type !== "object") return items;
839
+ if (!TRANSLATABLE_META_TYPES.has(type)) return items;
839
840
  const i18n = await this.resolveI18nService(environmentId, req);
840
841
  const bundle = this.buildTranslationBundle(i18n);
841
842
  if (!bundle) return items;
@@ -1338,6 +1339,15 @@ var RestServer = class {
1338
1339
  }
1339
1340
  }
1340
1341
  }
1342
+ if (req.params.type === "view" && req.query?.object) {
1343
+ const obj = String(req.query.object);
1344
+ const raw = visible;
1345
+ const list = Array.isArray(raw) ? raw : raw && typeof raw === "object" && Array.isArray(raw.items) ? raw.items : null;
1346
+ if (list) {
1347
+ 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)));
1348
+ visible = Array.isArray(raw) ? filtered : { ...raw, items: filtered };
1349
+ }
1350
+ }
1341
1351
  const translated = await this.translateMetaItems(req, req.params.type, environmentId, visible);
1342
1352
  res.header("Vary", "Accept-Language");
1343
1353
  res.json(translated);
@@ -1404,10 +1414,13 @@ var RestServer = class {
1404
1414
  ifNoneMatch: req.headers["if-none-match"],
1405
1415
  ifModifiedSince: req.headers["if-modified-since"]
1406
1416
  };
1417
+ const cacheI18n = await this.resolveI18nService(environmentId, req);
1418
+ const cacheLocale = this.extractLocale(req, cacheI18n);
1407
1419
  const result = await p.getMetaItemCached({
1408
1420
  type: req.params.type,
1409
1421
  name: req.params.name,
1410
1422
  cacheRequest,
1423
+ ...cacheLocale ? { locale: cacheLocale } : {},
1411
1424
  ...environmentId ? { environmentId } : {}
1412
1425
  });
1413
1426
  if (result.notModified) {
@@ -1427,7 +1440,7 @@ var RestServer = class {
1427
1440
  res.header("Cache-Control", directives + maxAge);
1428
1441
  }
1429
1442
  res.header("Vary", "Accept-Language");
1430
- res.json(await this.translateMetaItem(req, req.params.type, environmentId, result.data));
1443
+ res.json(await this.translateMetaItem(req, req.params.type, environmentId, result.data, cacheI18n));
1431
1444
  } else {
1432
1445
  const packageId = req.query?.package || void 0;
1433
1446
  const stateParam = typeof req.query?.state === "string" ? req.query.state.toLowerCase() : void 0;
@@ -1487,6 +1500,8 @@ var RestServer = class {
1487
1500
  const actor = typeof actorHeader === "string" ? actorHeader : void 0;
1488
1501
  const forceRaw = req.query?.force;
1489
1502
  const force = typeof forceRaw === "string" ? ["true", "1", "yes", "on"].includes(forceRaw.toLowerCase()) : !!forceRaw;
1503
+ const packageRaw = req.query?.package;
1504
+ const packageId = typeof packageRaw === "string" && packageRaw && packageRaw !== "all" ? packageRaw : void 0;
1490
1505
  const result = await p.saveMetaItem({
1491
1506
  type: req.params.type,
1492
1507
  name: req.params.name,
@@ -1495,6 +1510,7 @@ var RestServer = class {
1495
1510
  ...parentVersion !== void 0 ? { parentVersion } : {},
1496
1511
  ...actor ? { actor } : {},
1497
1512
  ...force ? { force: true } : {},
1513
+ ...packageId ? { packageId } : {},
1498
1514
  ...typeof req.query?.mode === "string" && req.query.mode.toLowerCase() === "draft" ? { mode: "draft" } : {}
1499
1515
  });
1500
1516
  res.json(result);
@@ -1769,13 +1785,16 @@ var RestServer = class {
1769
1785
  const parentVersion = typeof ifMatchHeader === "string" ? ifMatchHeader.replace(/^"|"$/g, "") : void 0;
1770
1786
  const actorHeader = req.headers?.["x-actor"] ?? req.headers?.["X-Actor"] ?? req.user?.id ?? req.userId;
1771
1787
  const actor = typeof actorHeader === "string" ? actorHeader : void 0;
1788
+ const packageRaw = req.query?.package;
1789
+ const packageId = typeof packageRaw === "string" && packageRaw && packageRaw !== "all" ? packageRaw : void 0;
1772
1790
  const result = await p.saveMetaItem({
1773
1791
  type: req.params.type,
1774
1792
  name: compoundName,
1775
1793
  item: req.body,
1776
1794
  ...environmentId ? { environmentId } : {},
1777
1795
  ...parentVersion !== void 0 ? { parentVersion } : {},
1778
- ...actor ? { actor } : {}
1796
+ ...actor ? { actor } : {},
1797
+ ...packageId ? { packageId } : {}
1779
1798
  });
1780
1799
  res.json(result);
1781
1800
  } catch (error) {
@@ -3257,19 +3276,17 @@ var RestServer = class {
3257
3276
  });
3258
3277
  }
3259
3278
  /**
3260
- * Register approval engine endpoints.
3279
+ * Register approval endpoints (ADR-0019: approval as a flow node).
3280
+ *
3281
+ * Approval is no longer a standalone process engine — a flow's Approval
3282
+ * node opens a request and suspends the run; a decision resumes it. There
3283
+ * are no process-authoring or submit routes anymore.
3261
3284
  *
3262
3285
  * Routes (all under {basePath}/approvals):
3263
- * GET /processes — list approval processes
3264
- * POST /processes — upsert (defineProcess)
3265
- * GET /processes/:id — get by id or name
3266
- * DELETE /processes/:id — delete process
3267
- * POST /requests — submit
3268
3286
  * GET /requests — list (filters: status, object, recordId, approverId, submitterId)
3269
3287
  * GET /requests/:id — get request
3270
- * POST /requests/:id/approve — approve current step
3271
- * POST /requests/:id/reject — reject current step
3272
- * POST /requests/:id/recall — recall (submitter only)
3288
+ * POST /requests/:id/approve — record an approve decision (resumes the flow)
3289
+ * POST /requests/:id/reject — record a reject decision (resumes the flow)
3273
3290
  * GET /requests/:id/actions — audit trail
3274
3291
  *
3275
3292
  * Returns 501 when `approvalsServiceProvider` is unset so deployments
@@ -3297,8 +3314,6 @@ var RestServer = class {
3297
3314
  [/^DUPLICATE_REQUEST/, 409, "DUPLICATE_REQUEST"],
3298
3315
  [/^INVALID_STATE/, 409, "INVALID_STATE"],
3299
3316
  [/^FORBIDDEN/, 403, "FORBIDDEN"],
3300
- [/^NO_ACTIVE_PROCESS/, 404, "NO_ACTIVE_PROCESS"],
3301
- [/^PROCESS_NOT_FOUND/, 404, "PROCESS_NOT_FOUND"],
3302
3317
  [/^REQUEST_NOT_FOUND/, 404, "REQUEST_NOT_FOUND"]
3303
3318
  ];
3304
3319
  for (const [re, status, code] of mapping) {
@@ -3309,127 +3324,6 @@ var RestServer = class {
3309
3324
  }
3310
3325
  return false;
3311
3326
  };
3312
- this.routeManager.register({
3313
- method: "GET",
3314
- path: `${dataPath}/approvals/processes`,
3315
- handler: async (req, res) => {
3316
- try {
3317
- const environmentId = isScoped ? req.params?.environmentId : void 0;
3318
- const context = await this.resolveExecCtx(environmentId, req);
3319
- if (this.enforceAuth(req, res, context)) return;
3320
- const svc = await resolveService(environmentId);
3321
- if (!svc) return respond501(res);
3322
- const q = req.query ?? {};
3323
- const rows = await svc.listProcesses({
3324
- object: q.object,
3325
- activeOnly: q.activeOnly === "true" || q.activeOnly === true
3326
- }, context ?? {});
3327
- res.json({ data: rows });
3328
- } catch (error) {
3329
- logError("[REST] List approval processes error:", error);
3330
- res.status(500).json({ code: "APPROVAL_PROCESS_LIST_FAILED", error: String(error?.message ?? error).slice(0, 500) });
3331
- }
3332
- },
3333
- metadata: { summary: "List approval processes", tags: ["approvals"] }
3334
- });
3335
- this.routeManager.register({
3336
- method: "POST",
3337
- path: `${dataPath}/approvals/processes`,
3338
- handler: async (req, res) => {
3339
- try {
3340
- const environmentId = isScoped ? req.params?.environmentId : void 0;
3341
- const context = await this.resolveExecCtx(environmentId, req);
3342
- if (this.enforceAuth(req, res, context)) return;
3343
- const svc = await resolveService(environmentId);
3344
- if (!svc) return respond501(res);
3345
- try {
3346
- const row = await svc.defineProcess(req.body ?? {}, context ?? {});
3347
- res.status(201).json(row);
3348
- } catch (err) {
3349
- if (handleApprovalError(res, err)) return;
3350
- throw err;
3351
- }
3352
- } catch (error) {
3353
- logError("[REST] Define approval process error:", error);
3354
- res.status(500).json({ code: "APPROVAL_PROCESS_DEFINE_FAILED", error: String(error?.message ?? error).slice(0, 500) });
3355
- }
3356
- },
3357
- metadata: { summary: "Define (upsert) an approval process", tags: ["approvals"] }
3358
- });
3359
- this.routeManager.register({
3360
- method: "GET",
3361
- path: `${dataPath}/approvals/processes/:id`,
3362
- handler: async (req, res) => {
3363
- try {
3364
- const environmentId = isScoped ? req.params?.environmentId : void 0;
3365
- const context = await this.resolveExecCtx(environmentId, req);
3366
- if (this.enforceAuth(req, res, context)) return;
3367
- const svc = await resolveService(environmentId);
3368
- if (!svc) return respond501(res);
3369
- const row = await svc.getProcess(req.params.id, context ?? {});
3370
- if (!row) {
3371
- res.status(404).json({ code: "PROCESS_NOT_FOUND", error: `Approval process '${req.params.id}' not found` });
3372
- return;
3373
- }
3374
- res.json(row);
3375
- } catch (error) {
3376
- logError("[REST] Get approval process error:", error);
3377
- res.status(500).json({ code: "APPROVAL_PROCESS_GET_FAILED", error: String(error?.message ?? error).slice(0, 500) });
3378
- }
3379
- },
3380
- metadata: { summary: "Get an approval process by id or name", tags: ["approvals"] }
3381
- });
3382
- this.routeManager.register({
3383
- method: "DELETE",
3384
- path: `${dataPath}/approvals/processes/:id`,
3385
- handler: async (req, res) => {
3386
- try {
3387
- const environmentId = isScoped ? req.params?.environmentId : void 0;
3388
- const context = await this.resolveExecCtx(environmentId, req);
3389
- if (this.enforceAuth(req, res, context)) return;
3390
- const svc = await resolveService(environmentId);
3391
- if (!svc) return respond501(res);
3392
- await svc.deleteProcess(req.params.id, context ?? {});
3393
- res.status(204).end();
3394
- } catch (error) {
3395
- logError("[REST] Delete approval process error:", error);
3396
- res.status(500).json({ code: "APPROVAL_PROCESS_DELETE_FAILED", error: String(error?.message ?? error).slice(0, 500) });
3397
- }
3398
- },
3399
- metadata: { summary: "Delete an approval process", tags: ["approvals"] }
3400
- });
3401
- this.routeManager.register({
3402
- method: "POST",
3403
- path: `${dataPath}/approvals/requests`,
3404
- handler: async (req, res) => {
3405
- try {
3406
- const environmentId = isScoped ? req.params?.environmentId : void 0;
3407
- const context = await this.resolveExecCtx(environmentId, req);
3408
- if (this.enforceAuth(req, res, context)) return;
3409
- const svc = await resolveService(environmentId);
3410
- if (!svc) return respond501(res);
3411
- const body = req.body ?? {};
3412
- try {
3413
- const row = await svc.submit({
3414
- object: body.object,
3415
- recordId: body.recordId ?? body.record_id,
3416
- processName: body.processName ?? body.process_name,
3417
- submitterId: body.submitterId ?? body.submitter_id ?? context?.userId,
3418
- comment: body.comment,
3419
- payload: body.payload
3420
- }, context ?? {});
3421
- res.status(201).json(row);
3422
- } catch (err) {
3423
- if (handleApprovalError(res, err)) return;
3424
- throw err;
3425
- }
3426
- } catch (error) {
3427
- logError("[REST] Submit approval error:", error);
3428
- res.status(500).json({ code: "APPROVAL_SUBMIT_FAILED", error: String(error?.message ?? error).slice(0, 500) });
3429
- }
3430
- },
3431
- metadata: { summary: "Submit a record for approval", tags: ["approvals"] }
3432
- });
3433
3327
  this.routeManager.register({
3434
3328
  method: "GET",
3435
3329
  path: `${dataPath}/approvals/requests`,
@@ -3482,10 +3376,10 @@ var RestServer = class {
3482
3376
  },
3483
3377
  metadata: { summary: "Get an approval request by id", tags: ["approvals"] }
3484
3378
  });
3485
- const decisionRoute = (suffix, method) => {
3379
+ const decisionRoute = (decision) => {
3486
3380
  this.routeManager.register({
3487
3381
  method: "POST",
3488
- path: `${dataPath}/approvals/requests/:id/${suffix}`,
3382
+ path: `${dataPath}/approvals/requests/:id/${decision}`,
3489
3383
  handler: async (req, res) => {
3490
3384
  try {
3491
3385
  const environmentId = isScoped ? req.params?.environmentId : void 0;
@@ -3495,7 +3389,8 @@ var RestServer = class {
3495
3389
  if (!svc) return respond501(res);
3496
3390
  const body = req.body ?? {};
3497
3391
  try {
3498
- const out = await svc[method](req.params.id, {
3392
+ const out = await svc.decide(req.params.id, {
3393
+ decision,
3499
3394
  actorId: body.actorId ?? body.actor_id ?? context?.userId,
3500
3395
  comment: body.comment
3501
3396
  }, context ?? {});
@@ -3505,16 +3400,15 @@ var RestServer = class {
3505
3400
  throw err;
3506
3401
  }
3507
3402
  } catch (error) {
3508
- logError(`[REST] ${suffix} approval error:`, error);
3509
- res.status(500).json({ code: `APPROVAL_${suffix.toUpperCase()}_FAILED`, error: String(error?.message ?? error).slice(0, 500) });
3403
+ logError(`[REST] ${decision} approval error:`, error);
3404
+ res.status(500).json({ code: `APPROVAL_${decision.toUpperCase()}_FAILED`, error: String(error?.message ?? error).slice(0, 500) });
3510
3405
  }
3511
3406
  },
3512
- metadata: { summary: `${suffix[0].toUpperCase()}${suffix.slice(1)} an approval request`, tags: ["approvals"] }
3407
+ metadata: { summary: `${decision[0].toUpperCase()}${decision.slice(1)} an approval request`, tags: ["approvals"] }
3513
3408
  });
3514
3409
  };
3515
- decisionRoute("approve", "approve");
3516
- decisionRoute("reject", "reject");
3517
- decisionRoute("recall", "recall");
3410
+ decisionRoute("approve");
3411
+ decisionRoute("reject");
3518
3412
  this.routeManager.register({
3519
3413
  method: "GET",
3520
3414
  path: `${dataPath}/approvals/requests/:id/actions`,
@@ -3787,6 +3681,66 @@ function registerPackageRoutes(server, packageService, basePath = "/api/v1", opt
3787
3681
  });
3788
3682
  }
3789
3683
 
3684
+ // src/external-datasource-routes.ts
3685
+ function registerExternalDatasourceRoutes(server, ctx, basePath = "/api/v1") {
3686
+ const ext = `${basePath}/datasources/:name/external`;
3687
+ const externalService = () => {
3688
+ try {
3689
+ return ctx.getService("external-datasource");
3690
+ } catch {
3691
+ return void 0;
3692
+ }
3693
+ };
3694
+ const unavailable = (res) => res.status(503).json({ error: "external_service_unavailable" });
3695
+ server.get(`${ext}/tables`, async (req, res) => {
3696
+ const svc = externalService();
3697
+ if (!svc?.listRemoteTables) return unavailable(res);
3698
+ const schema = typeof req.query?.schema === "string" ? req.query.schema : void 0;
3699
+ const tables = await svc.listRemoteTables(req.params.name, { schema });
3700
+ res.json({ tables });
3701
+ });
3702
+ server.post(`${ext}/tables/:remote/draft`, async (req, res) => {
3703
+ const svc = externalService();
3704
+ if (!svc?.generateObjectDraft) return unavailable(res);
3705
+ const draft = await svc.generateObjectDraft(
3706
+ req.params.name,
3707
+ req.params.remote,
3708
+ req.body ?? {}
3709
+ );
3710
+ res.json({ draft });
3711
+ });
3712
+ server.post(`${ext}/tables/:remote/import`, async (req, res) => {
3713
+ const svc = externalService();
3714
+ if (!svc?.importObject) return unavailable(res);
3715
+ try {
3716
+ const result = await svc.importObject(
3717
+ req.params.name,
3718
+ req.params.remote,
3719
+ req.body ?? {}
3720
+ );
3721
+ res.status(201).json({ object: result });
3722
+ } catch (err) {
3723
+ res.status(400).json({
3724
+ error: "external_import_error",
3725
+ message: err instanceof Error ? err.message : String(err)
3726
+ });
3727
+ }
3728
+ });
3729
+ server.post(`${ext}/refresh-catalog`, async (req, res) => {
3730
+ const svc = externalService();
3731
+ if (!svc?.refreshCatalog) return unavailable(res);
3732
+ const catalog = await svc.refreshCatalog(req.params.name);
3733
+ res.json({ catalog });
3734
+ });
3735
+ server.post(`${ext}/validate`, async (req, res) => {
3736
+ const svc = externalService();
3737
+ if (!svc?.validateAll) return unavailable(res);
3738
+ const report = await svc.validateAll();
3739
+ const results = (report.results ?? []).filter((r) => r.datasource === req.params.name);
3740
+ res.json({ ok: results.every((r) => r.ok), results });
3741
+ });
3742
+ }
3743
+
3790
3744
  // src/rest-api-plugin.ts
3791
3745
  function createRestApiPlugin(config = {}) {
3792
3746
  return {
@@ -3899,14 +3853,14 @@ function createRestApiPlugin(config = {}) {
3899
3853
  ctx.logger.error("Failed to register REST API routes", { error: err.message });
3900
3854
  throw err;
3901
3855
  }
3856
+ const basePath = config.api?.api?.basePath || "/api";
3857
+ const version = config.api?.api?.version || "v1";
3858
+ const versionedBase = `${basePath}/${version}`;
3859
+ const enableProjectScoping = config.api?.api?.enableProjectScoping ?? false;
3860
+ const projectResolution = config.api?.api?.projectResolution ?? "auto";
3902
3861
  try {
3903
3862
  const packageService = ctx.getService("package");
3904
3863
  if (packageService) {
3905
- const basePath = config.api?.api?.basePath || "/api";
3906
- const version = config.api?.api?.version || "v1";
3907
- const versionedBase = `${basePath}/${version}`;
3908
- const enableProjectScoping = config.api?.api?.enableProjectScoping ?? false;
3909
- const projectResolution = config.api?.api?.projectResolution ?? "auto";
3910
3864
  if (enableProjectScoping && projectResolution === "required") {
3911
3865
  registerPackageRoutes(server, packageService, `${versionedBase}/environments/:environmentId`, {
3912
3866
  protocol
@@ -3924,6 +3878,12 @@ function createRestApiPlugin(config = {}) {
3924
3878
  } catch (e) {
3925
3879
  ctx.logger.debug("Package service not available, package routes skipped");
3926
3880
  }
3881
+ try {
3882
+ registerExternalDatasourceRoutes(server, ctx, versionedBase);
3883
+ ctx.logger.info("Datasource federation routes registered");
3884
+ } catch (e) {
3885
+ ctx.logger.warn("Datasource federation routes registration failed", { error: e?.message });
3886
+ }
3927
3887
  }
3928
3888
  };
3929
3889
  }