@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.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;
@@ -3263,19 +3276,17 @@ var RestServer = class {
3263
3276
  });
3264
3277
  }
3265
3278
  /**
3266
- * 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.
3267
3284
  *
3268
3285
  * Routes (all under {basePath}/approvals):
3269
- * GET /processes — list approval processes
3270
- * POST /processes — upsert (defineProcess)
3271
- * GET /processes/:id — get by id or name
3272
- * DELETE /processes/:id — delete process
3273
- * POST /requests — submit
3274
3286
  * GET /requests — list (filters: status, object, recordId, approverId, submitterId)
3275
3287
  * GET /requests/:id — get request
3276
- * POST /requests/:id/approve — approve current step
3277
- * POST /requests/:id/reject — reject current step
3278
- * 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)
3279
3290
  * GET /requests/:id/actions — audit trail
3280
3291
  *
3281
3292
  * Returns 501 when `approvalsServiceProvider` is unset so deployments
@@ -3303,8 +3314,6 @@ var RestServer = class {
3303
3314
  [/^DUPLICATE_REQUEST/, 409, "DUPLICATE_REQUEST"],
3304
3315
  [/^INVALID_STATE/, 409, "INVALID_STATE"],
3305
3316
  [/^FORBIDDEN/, 403, "FORBIDDEN"],
3306
- [/^NO_ACTIVE_PROCESS/, 404, "NO_ACTIVE_PROCESS"],
3307
- [/^PROCESS_NOT_FOUND/, 404, "PROCESS_NOT_FOUND"],
3308
3317
  [/^REQUEST_NOT_FOUND/, 404, "REQUEST_NOT_FOUND"]
3309
3318
  ];
3310
3319
  for (const [re, status, code] of mapping) {
@@ -3315,127 +3324,6 @@ var RestServer = class {
3315
3324
  }
3316
3325
  return false;
3317
3326
  };
3318
- this.routeManager.register({
3319
- method: "GET",
3320
- path: `${dataPath}/approvals/processes`,
3321
- handler: async (req, res) => {
3322
- try {
3323
- const environmentId = isScoped ? req.params?.environmentId : void 0;
3324
- const context = await this.resolveExecCtx(environmentId, req);
3325
- if (this.enforceAuth(req, res, context)) return;
3326
- const svc = await resolveService(environmentId);
3327
- if (!svc) return respond501(res);
3328
- const q = req.query ?? {};
3329
- const rows = await svc.listProcesses({
3330
- object: q.object,
3331
- activeOnly: q.activeOnly === "true" || q.activeOnly === true
3332
- }, context ?? {});
3333
- res.json({ data: rows });
3334
- } catch (error) {
3335
- logError("[REST] List approval processes error:", error);
3336
- res.status(500).json({ code: "APPROVAL_PROCESS_LIST_FAILED", error: String(error?.message ?? error).slice(0, 500) });
3337
- }
3338
- },
3339
- metadata: { summary: "List approval processes", tags: ["approvals"] }
3340
- });
3341
- this.routeManager.register({
3342
- method: "POST",
3343
- path: `${dataPath}/approvals/processes`,
3344
- handler: async (req, res) => {
3345
- try {
3346
- const environmentId = isScoped ? req.params?.environmentId : void 0;
3347
- const context = await this.resolveExecCtx(environmentId, req);
3348
- if (this.enforceAuth(req, res, context)) return;
3349
- const svc = await resolveService(environmentId);
3350
- if (!svc) return respond501(res);
3351
- try {
3352
- const row = await svc.defineProcess(req.body ?? {}, context ?? {});
3353
- res.status(201).json(row);
3354
- } catch (err) {
3355
- if (handleApprovalError(res, err)) return;
3356
- throw err;
3357
- }
3358
- } catch (error) {
3359
- logError("[REST] Define approval process error:", error);
3360
- res.status(500).json({ code: "APPROVAL_PROCESS_DEFINE_FAILED", error: String(error?.message ?? error).slice(0, 500) });
3361
- }
3362
- },
3363
- metadata: { summary: "Define (upsert) an approval process", tags: ["approvals"] }
3364
- });
3365
- this.routeManager.register({
3366
- method: "GET",
3367
- path: `${dataPath}/approvals/processes/:id`,
3368
- handler: async (req, res) => {
3369
- try {
3370
- const environmentId = isScoped ? req.params?.environmentId : void 0;
3371
- const context = await this.resolveExecCtx(environmentId, req);
3372
- if (this.enforceAuth(req, res, context)) return;
3373
- const svc = await resolveService(environmentId);
3374
- if (!svc) return respond501(res);
3375
- const row = await svc.getProcess(req.params.id, context ?? {});
3376
- if (!row) {
3377
- res.status(404).json({ code: "PROCESS_NOT_FOUND", error: `Approval process '${req.params.id}' not found` });
3378
- return;
3379
- }
3380
- res.json(row);
3381
- } catch (error) {
3382
- logError("[REST] Get approval process error:", error);
3383
- res.status(500).json({ code: "APPROVAL_PROCESS_GET_FAILED", error: String(error?.message ?? error).slice(0, 500) });
3384
- }
3385
- },
3386
- metadata: { summary: "Get an approval process by id or name", tags: ["approvals"] }
3387
- });
3388
- this.routeManager.register({
3389
- method: "DELETE",
3390
- path: `${dataPath}/approvals/processes/:id`,
3391
- handler: async (req, res) => {
3392
- try {
3393
- const environmentId = isScoped ? req.params?.environmentId : void 0;
3394
- const context = await this.resolveExecCtx(environmentId, req);
3395
- if (this.enforceAuth(req, res, context)) return;
3396
- const svc = await resolveService(environmentId);
3397
- if (!svc) return respond501(res);
3398
- await svc.deleteProcess(req.params.id, context ?? {});
3399
- res.status(204).end();
3400
- } catch (error) {
3401
- logError("[REST] Delete approval process error:", error);
3402
- res.status(500).json({ code: "APPROVAL_PROCESS_DELETE_FAILED", error: String(error?.message ?? error).slice(0, 500) });
3403
- }
3404
- },
3405
- metadata: { summary: "Delete an approval process", tags: ["approvals"] }
3406
- });
3407
- this.routeManager.register({
3408
- method: "POST",
3409
- path: `${dataPath}/approvals/requests`,
3410
- handler: async (req, res) => {
3411
- try {
3412
- const environmentId = isScoped ? req.params?.environmentId : void 0;
3413
- const context = await this.resolveExecCtx(environmentId, req);
3414
- if (this.enforceAuth(req, res, context)) return;
3415
- const svc = await resolveService(environmentId);
3416
- if (!svc) return respond501(res);
3417
- const body = req.body ?? {};
3418
- try {
3419
- const row = await svc.submit({
3420
- object: body.object,
3421
- recordId: body.recordId ?? body.record_id,
3422
- processName: body.processName ?? body.process_name,
3423
- submitterId: body.submitterId ?? body.submitter_id ?? context?.userId,
3424
- comment: body.comment,
3425
- payload: body.payload
3426
- }, context ?? {});
3427
- res.status(201).json(row);
3428
- } catch (err) {
3429
- if (handleApprovalError(res, err)) return;
3430
- throw err;
3431
- }
3432
- } catch (error) {
3433
- logError("[REST] Submit approval error:", error);
3434
- res.status(500).json({ code: "APPROVAL_SUBMIT_FAILED", error: String(error?.message ?? error).slice(0, 500) });
3435
- }
3436
- },
3437
- metadata: { summary: "Submit a record for approval", tags: ["approvals"] }
3438
- });
3439
3327
  this.routeManager.register({
3440
3328
  method: "GET",
3441
3329
  path: `${dataPath}/approvals/requests`,
@@ -3488,10 +3376,10 @@ var RestServer = class {
3488
3376
  },
3489
3377
  metadata: { summary: "Get an approval request by id", tags: ["approvals"] }
3490
3378
  });
3491
- const decisionRoute = (suffix, method) => {
3379
+ const decisionRoute = (decision) => {
3492
3380
  this.routeManager.register({
3493
3381
  method: "POST",
3494
- path: `${dataPath}/approvals/requests/:id/${suffix}`,
3382
+ path: `${dataPath}/approvals/requests/:id/${decision}`,
3495
3383
  handler: async (req, res) => {
3496
3384
  try {
3497
3385
  const environmentId = isScoped ? req.params?.environmentId : void 0;
@@ -3501,7 +3389,8 @@ var RestServer = class {
3501
3389
  if (!svc) return respond501(res);
3502
3390
  const body = req.body ?? {};
3503
3391
  try {
3504
- const out = await svc[method](req.params.id, {
3392
+ const out = await svc.decide(req.params.id, {
3393
+ decision,
3505
3394
  actorId: body.actorId ?? body.actor_id ?? context?.userId,
3506
3395
  comment: body.comment
3507
3396
  }, context ?? {});
@@ -3511,16 +3400,15 @@ var RestServer = class {
3511
3400
  throw err;
3512
3401
  }
3513
3402
  } catch (error) {
3514
- logError(`[REST] ${suffix} approval error:`, error);
3515
- 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) });
3516
3405
  }
3517
3406
  },
3518
- 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"] }
3519
3408
  });
3520
3409
  };
3521
- decisionRoute("approve", "approve");
3522
- decisionRoute("reject", "reject");
3523
- decisionRoute("recall", "recall");
3410
+ decisionRoute("approve");
3411
+ decisionRoute("reject");
3524
3412
  this.routeManager.register({
3525
3413
  method: "GET",
3526
3414
  path: `${dataPath}/approvals/requests/:id/actions`,
@@ -3793,6 +3681,66 @@ function registerPackageRoutes(server, packageService, basePath = "/api/v1", opt
3793
3681
  });
3794
3682
  }
3795
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
+
3796
3744
  // src/rest-api-plugin.ts
3797
3745
  function createRestApiPlugin(config = {}) {
3798
3746
  return {
@@ -3905,14 +3853,14 @@ function createRestApiPlugin(config = {}) {
3905
3853
  ctx.logger.error("Failed to register REST API routes", { error: err.message });
3906
3854
  throw err;
3907
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";
3908
3861
  try {
3909
3862
  const packageService = ctx.getService("package");
3910
3863
  if (packageService) {
3911
- const basePath = config.api?.api?.basePath || "/api";
3912
- const version = config.api?.api?.version || "v1";
3913
- const versionedBase = `${basePath}/${version}`;
3914
- const enableProjectScoping = config.api?.api?.enableProjectScoping ?? false;
3915
- const projectResolution = config.api?.api?.projectResolution ?? "auto";
3916
3864
  if (enableProjectScoping && projectResolution === "required") {
3917
3865
  registerPackageRoutes(server, packageService, `${versionedBase}/environments/:environmentId`, {
3918
3866
  protocol
@@ -3930,6 +3878,12 @@ function createRestApiPlugin(config = {}) {
3930
3878
  } catch (e) {
3931
3879
  ctx.logger.debug("Package service not available, package routes skipped");
3932
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
+ }
3933
3887
  }
3934
3888
  };
3935
3889
  }