@objectstack/rest 7.3.0 → 7.4.1

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;
@@ -3303,19 +3316,17 @@ var RestServer = class {
3303
3316
  });
3304
3317
  }
3305
3318
  /**
3306
- * 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.
3307
3324
  *
3308
3325
  * Routes (all under {basePath}/approvals):
3309
- * GET /processes — list approval processes
3310
- * POST /processes — upsert (defineProcess)
3311
- * GET /processes/:id — get by id or name
3312
- * DELETE /processes/:id — delete process
3313
- * POST /requests — submit
3314
3326
  * GET /requests — list (filters: status, object, recordId, approverId, submitterId)
3315
3327
  * GET /requests/:id — get request
3316
- * POST /requests/:id/approve — approve current step
3317
- * POST /requests/:id/reject — reject current step
3318
- * 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)
3319
3330
  * GET /requests/:id/actions — audit trail
3320
3331
  *
3321
3332
  * Returns 501 when `approvalsServiceProvider` is unset so deployments
@@ -3343,8 +3354,6 @@ var RestServer = class {
3343
3354
  [/^DUPLICATE_REQUEST/, 409, "DUPLICATE_REQUEST"],
3344
3355
  [/^INVALID_STATE/, 409, "INVALID_STATE"],
3345
3356
  [/^FORBIDDEN/, 403, "FORBIDDEN"],
3346
- [/^NO_ACTIVE_PROCESS/, 404, "NO_ACTIVE_PROCESS"],
3347
- [/^PROCESS_NOT_FOUND/, 404, "PROCESS_NOT_FOUND"],
3348
3357
  [/^REQUEST_NOT_FOUND/, 404, "REQUEST_NOT_FOUND"]
3349
3358
  ];
3350
3359
  for (const [re, status, code] of mapping) {
@@ -3355,127 +3364,6 @@ var RestServer = class {
3355
3364
  }
3356
3365
  return false;
3357
3366
  };
3358
- this.routeManager.register({
3359
- method: "GET",
3360
- path: `${dataPath}/approvals/processes`,
3361
- handler: async (req, res) => {
3362
- try {
3363
- const environmentId = isScoped ? req.params?.environmentId : void 0;
3364
- const context = await this.resolveExecCtx(environmentId, req);
3365
- if (this.enforceAuth(req, res, context)) return;
3366
- const svc = await resolveService(environmentId);
3367
- if (!svc) return respond501(res);
3368
- const q = req.query ?? {};
3369
- const rows = await svc.listProcesses({
3370
- object: q.object,
3371
- activeOnly: q.activeOnly === "true" || q.activeOnly === true
3372
- }, context ?? {});
3373
- res.json({ data: rows });
3374
- } catch (error) {
3375
- logError("[REST] List approval processes error:", error);
3376
- res.status(500).json({ code: "APPROVAL_PROCESS_LIST_FAILED", error: String(error?.message ?? error).slice(0, 500) });
3377
- }
3378
- },
3379
- metadata: { summary: "List approval processes", tags: ["approvals"] }
3380
- });
3381
- this.routeManager.register({
3382
- method: "POST",
3383
- path: `${dataPath}/approvals/processes`,
3384
- handler: async (req, res) => {
3385
- try {
3386
- const environmentId = isScoped ? req.params?.environmentId : void 0;
3387
- const context = await this.resolveExecCtx(environmentId, req);
3388
- if (this.enforceAuth(req, res, context)) return;
3389
- const svc = await resolveService(environmentId);
3390
- if (!svc) return respond501(res);
3391
- try {
3392
- const row = await svc.defineProcess(req.body ?? {}, context ?? {});
3393
- res.status(201).json(row);
3394
- } catch (err) {
3395
- if (handleApprovalError(res, err)) return;
3396
- throw err;
3397
- }
3398
- } catch (error) {
3399
- logError("[REST] Define approval process error:", error);
3400
- res.status(500).json({ code: "APPROVAL_PROCESS_DEFINE_FAILED", error: String(error?.message ?? error).slice(0, 500) });
3401
- }
3402
- },
3403
- metadata: { summary: "Define (upsert) an approval process", tags: ["approvals"] }
3404
- });
3405
- this.routeManager.register({
3406
- method: "GET",
3407
- path: `${dataPath}/approvals/processes/:id`,
3408
- handler: async (req, res) => {
3409
- try {
3410
- const environmentId = isScoped ? req.params?.environmentId : void 0;
3411
- const context = await this.resolveExecCtx(environmentId, req);
3412
- if (this.enforceAuth(req, res, context)) return;
3413
- const svc = await resolveService(environmentId);
3414
- if (!svc) return respond501(res);
3415
- const row = await svc.getProcess(req.params.id, context ?? {});
3416
- if (!row) {
3417
- res.status(404).json({ code: "PROCESS_NOT_FOUND", error: `Approval process '${req.params.id}' not found` });
3418
- return;
3419
- }
3420
- res.json(row);
3421
- } catch (error) {
3422
- logError("[REST] Get approval process error:", error);
3423
- res.status(500).json({ code: "APPROVAL_PROCESS_GET_FAILED", error: String(error?.message ?? error).slice(0, 500) });
3424
- }
3425
- },
3426
- metadata: { summary: "Get an approval process by id or name", tags: ["approvals"] }
3427
- });
3428
- this.routeManager.register({
3429
- method: "DELETE",
3430
- path: `${dataPath}/approvals/processes/:id`,
3431
- handler: async (req, res) => {
3432
- try {
3433
- const environmentId = isScoped ? req.params?.environmentId : void 0;
3434
- const context = await this.resolveExecCtx(environmentId, req);
3435
- if (this.enforceAuth(req, res, context)) return;
3436
- const svc = await resolveService(environmentId);
3437
- if (!svc) return respond501(res);
3438
- await svc.deleteProcess(req.params.id, context ?? {});
3439
- res.status(204).end();
3440
- } catch (error) {
3441
- logError("[REST] Delete approval process error:", error);
3442
- res.status(500).json({ code: "APPROVAL_PROCESS_DELETE_FAILED", error: String(error?.message ?? error).slice(0, 500) });
3443
- }
3444
- },
3445
- metadata: { summary: "Delete an approval process", tags: ["approvals"] }
3446
- });
3447
- this.routeManager.register({
3448
- method: "POST",
3449
- path: `${dataPath}/approvals/requests`,
3450
- handler: async (req, res) => {
3451
- try {
3452
- const environmentId = isScoped ? req.params?.environmentId : void 0;
3453
- const context = await this.resolveExecCtx(environmentId, req);
3454
- if (this.enforceAuth(req, res, context)) return;
3455
- const svc = await resolveService(environmentId);
3456
- if (!svc) return respond501(res);
3457
- const body = req.body ?? {};
3458
- try {
3459
- const row = await svc.submit({
3460
- object: body.object,
3461
- recordId: body.recordId ?? body.record_id,
3462
- processName: body.processName ?? body.process_name,
3463
- submitterId: body.submitterId ?? body.submitter_id ?? context?.userId,
3464
- comment: body.comment,
3465
- payload: body.payload
3466
- }, context ?? {});
3467
- res.status(201).json(row);
3468
- } catch (err) {
3469
- if (handleApprovalError(res, err)) return;
3470
- throw err;
3471
- }
3472
- } catch (error) {
3473
- logError("[REST] Submit approval error:", error);
3474
- res.status(500).json({ code: "APPROVAL_SUBMIT_FAILED", error: String(error?.message ?? error).slice(0, 500) });
3475
- }
3476
- },
3477
- metadata: { summary: "Submit a record for approval", tags: ["approvals"] }
3478
- });
3479
3367
  this.routeManager.register({
3480
3368
  method: "GET",
3481
3369
  path: `${dataPath}/approvals/requests`,
@@ -3528,10 +3416,10 @@ var RestServer = class {
3528
3416
  },
3529
3417
  metadata: { summary: "Get an approval request by id", tags: ["approvals"] }
3530
3418
  });
3531
- const decisionRoute = (suffix, method) => {
3419
+ const decisionRoute = (decision) => {
3532
3420
  this.routeManager.register({
3533
3421
  method: "POST",
3534
- path: `${dataPath}/approvals/requests/:id/${suffix}`,
3422
+ path: `${dataPath}/approvals/requests/:id/${decision}`,
3535
3423
  handler: async (req, res) => {
3536
3424
  try {
3537
3425
  const environmentId = isScoped ? req.params?.environmentId : void 0;
@@ -3541,7 +3429,8 @@ var RestServer = class {
3541
3429
  if (!svc) return respond501(res);
3542
3430
  const body = req.body ?? {};
3543
3431
  try {
3544
- const out = await svc[method](req.params.id, {
3432
+ const out = await svc.decide(req.params.id, {
3433
+ decision,
3545
3434
  actorId: body.actorId ?? body.actor_id ?? context?.userId,
3546
3435
  comment: body.comment
3547
3436
  }, context ?? {});
@@ -3551,16 +3440,15 @@ var RestServer = class {
3551
3440
  throw err;
3552
3441
  }
3553
3442
  } catch (error) {
3554
- logError(`[REST] ${suffix} approval error:`, error);
3555
- 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) });
3556
3445
  }
3557
3446
  },
3558
- 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"] }
3559
3448
  });
3560
3449
  };
3561
- decisionRoute("approve", "approve");
3562
- decisionRoute("reject", "reject");
3563
- decisionRoute("recall", "recall");
3450
+ decisionRoute("approve");
3451
+ decisionRoute("reject");
3564
3452
  this.routeManager.register({
3565
3453
  method: "GET",
3566
3454
  path: `${dataPath}/approvals/requests/:id/actions`,
@@ -3833,6 +3721,66 @@ function registerPackageRoutes(server, packageService, basePath = "/api/v1", opt
3833
3721
  });
3834
3722
  }
3835
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
+
3836
3784
  // src/rest-api-plugin.ts
3837
3785
  function createRestApiPlugin(config = {}) {
3838
3786
  return {
@@ -3945,14 +3893,14 @@ function createRestApiPlugin(config = {}) {
3945
3893
  ctx.logger.error("Failed to register REST API routes", { error: err.message });
3946
3894
  throw err;
3947
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";
3948
3901
  try {
3949
3902
  const packageService = ctx.getService("package");
3950
3903
  if (packageService) {
3951
- const basePath = config.api?.api?.basePath || "/api";
3952
- const version = config.api?.api?.version || "v1";
3953
- const versionedBase = `${basePath}/${version}`;
3954
- const enableProjectScoping = config.api?.api?.enableProjectScoping ?? false;
3955
- const projectResolution = config.api?.api?.projectResolution ?? "auto";
3956
3904
  if (enableProjectScoping && projectResolution === "required") {
3957
3905
  registerPackageRoutes(server, packageService, `${versionedBase}/environments/:environmentId`, {
3958
3906
  protocol
@@ -3970,6 +3918,12 @@ function createRestApiPlugin(config = {}) {
3970
3918
  } catch (e) {
3971
3919
  ctx.logger.debug("Package service not available, package routes skipped");
3972
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
+ }
3973
3927
  }
3974
3928
  };
3975
3929
  }