@objectstack/plugin-approvals 17.0.0-rc.0 → 17.0.0-rc.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +439 -0
  2. package/dist/index.d.mts +2304 -3491
  3. package/dist/index.d.ts +2304 -3491
  4. package/dist/index.js +116 -43
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +114 -41
  7. package/dist/index.mjs.map +1 -1
  8. package/package.json +16 -9
  9. package/.turbo/turbo-build.log +0 -22
  10. package/scripts/i18n-extract.config.ts +0 -38
  11. package/src/action-link-pages.ts +0 -102
  12. package/src/approval-actor-impersonation.test.ts +0 -330
  13. package/src/approval-node.test.ts +0 -356
  14. package/src/approval-node.ts +0 -196
  15. package/src/approval-revise.test.ts +0 -418
  16. package/src/approval-service.test.ts +0 -2858
  17. package/src/approval-service.ts +0 -3617
  18. package/src/approvals-plugin.ts +0 -294
  19. package/src/approver-cross-org.integration.test.ts +0 -206
  20. package/src/approver-org-scope.test.ts +0 -201
  21. package/src/approver-org-scope.ts +0 -261
  22. package/src/index.ts +0 -42
  23. package/src/lifecycle-hooks.ts +0 -201
  24. package/src/nav-contribution.test.ts +0 -50
  25. package/src/record-lock-schedule-run.integration.test.ts +0 -206
  26. package/src/status-mirror-cascade.integration.test.ts +0 -224
  27. package/src/sys-approval-action.object.ts +0 -149
  28. package/src/sys-approval-approver.object.ts +0 -85
  29. package/src/sys-approval-delegation.object.test.ts +0 -42
  30. package/src/sys-approval-delegation.object.ts +0 -142
  31. package/src/sys-approval-request.object.test.ts +0 -116
  32. package/src/sys-approval-request.object.ts +0 -413
  33. package/src/sys-approval-token.object.ts +0 -101
  34. package/src/translations/bundle-ownership.test.ts +0 -48
  35. package/src/translations/en.objects.generated.ts +0 -311
  36. package/src/translations/es-ES.objects.generated.ts +0 -311
  37. package/src/translations/index.ts +0 -23
  38. package/src/translations/ja-JP.objects.generated.ts +0 -311
  39. package/src/translations/zh-CN.objects.generated.ts +0 -311
  40. package/tsconfig.json +0 -10
package/dist/index.js CHANGED
@@ -257,6 +257,14 @@ var init_en_objects_generated = __esm({
257
257
  comment: {
258
258
  label: "Comment"
259
259
  },
260
+ reassign_from: {
261
+ label: "Reassigned From",
262
+ help: "User whose pending-approver slot was handed over (reassign actions only)"
263
+ },
264
+ reassign_to: {
265
+ label: "Reassigned To",
266
+ help: "User who received the pending-approver slot (reassign actions only)"
267
+ },
260
268
  attachments: {
261
269
  label: "Attachments",
262
270
  help: "Files supporting this action \u2014 e.g. a signed contract or evidence (#3266)."
@@ -566,6 +574,14 @@ var init_zh_CN_objects_generated = __esm({
566
574
  comment: {
567
575
  label: "\u8BC4\u8BBA"
568
576
  },
577
+ reassign_from: {
578
+ label: "\u8F6C\u51FA\u4EBA",
579
+ help: "\u88AB\u79FB\u4EA4\u5F85\u5BA1\u6279\u69FD\u4F4D\u7684\u7528\u6237\uFF08\u4EC5\u8F6C\u7B7E\u64CD\u4F5C\uFF09"
580
+ },
581
+ reassign_to: {
582
+ label: "\u8F6C\u5165\u4EBA",
583
+ help: "\u63A5\u6536\u5F85\u5BA1\u6279\u69FD\u4F4D\u7684\u7528\u6237\uFF08\u4EC5\u8F6C\u7B7E\u64CD\u4F5C\uFF09"
584
+ },
569
585
  attachments: {
570
586
  label: "\u9644\u4EF6",
571
587
  help: "\u652F\u6301\u8BE5\u64CD\u4F5C\u7684\u6587\u4EF6\u2014\u2014\u4F8B\u5982\u5DF2\u7B7E\u7F72\u7684\u5408\u540C\u6216\u8BC1\u660E\u6750\u6599\uFF08#3266\uFF09\u3002"
@@ -875,6 +891,14 @@ var init_ja_JP_objects_generated = __esm({
875
891
  comment: {
876
892
  label: "\u30B3\u30E1\u30F3\u30C8"
877
893
  },
894
+ reassign_from: {
895
+ label: "\u5F15\u304D\u7D99\u304E\u5143",
896
+ help: "\u627F\u8A8D\u5F85\u3061\u30B9\u30ED\u30C3\u30C8\u3092\u5F15\u304D\u6E21\u3057\u305F\u30E6\u30FC\u30B6\u30FC\uFF08\u5F15\u304D\u7D99\u304E\u64CD\u4F5C\u306E\u307F\uFF09"
897
+ },
898
+ reassign_to: {
899
+ label: "\u5F15\u304D\u7D99\u304E\u5148",
900
+ help: "\u627F\u8A8D\u5F85\u3061\u30B9\u30ED\u30C3\u30C8\u3092\u53D7\u3051\u53D6\u3063\u305F\u30E6\u30FC\u30B6\u30FC\uFF08\u5F15\u304D\u7D99\u304E\u64CD\u4F5C\u306E\u307F\uFF09"
901
+ },
878
902
  attachments: {
879
903
  label: "\u6DFB\u4ED8\u30D5\u30A1\u30A4\u30EB",
880
904
  help: "\u3053\u306E\u64CD\u4F5C\u3092\u88CF\u4ED8\u3051\u308B\u30D5\u30A1\u30A4\u30EB\u2014\u2014\u7F72\u540D\u6E08\u307F\u5951\u7D04\u66F8\u3084\u8A3C\u6191\u306A\u3069\uFF08#3266\uFF09\u3002"
@@ -1184,6 +1208,14 @@ var init_es_ES_objects_generated = __esm({
1184
1208
  comment: {
1185
1209
  label: "Comentario"
1186
1210
  },
1211
+ reassign_from: {
1212
+ label: "Reasignado de",
1213
+ help: "Usuario cuyo turno de aprobaci\xF3n pendiente fue traspasado (solo acciones de reasignaci\xF3n)"
1214
+ },
1215
+ reassign_to: {
1216
+ label: "Reasignado a",
1217
+ help: "Usuario que recibi\xF3 el turno de aprobaci\xF3n pendiente (solo acciones de reasignaci\xF3n)"
1218
+ },
1187
1219
  attachments: {
1188
1220
  label: "Adjuntos",
1189
1221
  help: "Archivos que respaldan esta acci\xF3n, p. ej. un contrato firmado o pruebas (#3266)."
@@ -1298,6 +1330,7 @@ module.exports = __toCommonJS(index_exports);
1298
1330
 
1299
1331
  // src/sys-approval-request.object.ts
1300
1332
  var import_data = require("@objectstack/spec/data");
1333
+ var import_contracts = require("@objectstack/spec/contracts");
1301
1334
  var SysApprovalRequest = import_data.ObjectSchema.create({
1302
1335
  name: "sys_approval_request",
1303
1336
  label: "Approval Request",
@@ -1396,9 +1429,10 @@ var SysApprovalRequest = import_data.ObjectSchema.create({
1396
1429
  group: "Target"
1397
1430
  }),
1398
1431
  status: import_data.Field.select(
1399
- // Keep in sync with `ApprovalStatus` (spec/contracts). `returned` =
1400
- // sent back for revision (ADR-0044) terminal for this round.
1401
- ["pending", "approved", "rejected", "recalled", "returned"],
1432
+ // Spread from the contract, not re-typed (#3786). `APPROVAL_STATUSES` is
1433
+ // where the list and the reason for each entry live; `ApprovalStatus` is
1434
+ // derived from it, so this column and the contract cannot disagree.
1435
+ [...import_contracts.APPROVAL_STATUSES],
1402
1436
  {
1403
1437
  label: "Status",
1404
1438
  required: true,
@@ -1667,6 +1701,7 @@ var SysApprovalRequest = import_data.ObjectSchema.create({
1667
1701
 
1668
1702
  // src/sys-approval-action.object.ts
1669
1703
  var import_data2 = require("@objectstack/spec/data");
1704
+ var import_contracts2 = require("@objectstack/spec/contracts");
1670
1705
  var SysApprovalAction = import_data2.ObjectSchema.create({
1671
1706
  name: "sys_approval_action",
1672
1707
  label: "Approval Action",
@@ -1745,12 +1780,11 @@ var SysApprovalAction = import_data2.ObjectSchema.create({
1745
1780
  group: "Target"
1746
1781
  }),
1747
1782
  action: import_data2.Field.select(
1748
- // Keep in sync with `ApprovalActionKind` (spec/contracts). reassign /
1749
- // remind / request_info / comment are thread interactions they never
1750
- // move the flow. revise / resubmit (ADR-0044) DO move it: send back for
1751
- // revision and the later resubmission. ooo_substitute (#1322 M1) is a
1752
- // system-recorded reroute of an out-of-office approver — no flow movement.
1753
- ["submit", "approve", "reject", "recall", "escalate", "reassign", "remind", "request_info", "comment", "revise", "resubmit", "ooo_substitute"],
1783
+ // Spread from the contract, not re-typed (#3786). `APPROVAL_ACTION_KINDS`
1784
+ // is where the list and the per-kind notes live (which kinds move the flow
1785
+ // and which are thread-only); `ApprovalActionKind` is derived from it, so
1786
+ // this column and the contract cannot disagree.
1787
+ [...import_contracts2.APPROVAL_ACTION_KINDS],
1754
1788
  {
1755
1789
  label: "Action",
1756
1790
  required: true,
@@ -1763,6 +1797,23 @@ var SysApprovalAction = import_data2.ObjectSchema.create({
1763
1797
  group: "Action"
1764
1798
  }),
1765
1799
  comment: import_data2.Field.textarea({ label: "Comment", required: false, group: "Action" }),
1800
+ // Structured hand-off parties for `action: 'reassign'` (#4365). Before
1801
+ // these existed the pair lived only inside a default free-text comment
1802
+ // ("<from_id> → <to_id>"), which no client could parse or render readably.
1803
+ // `comment` is pure user input again; timelines render "from A to B" from
1804
+ // these fields.
1805
+ reassign_from: import_data2.Field.lookup("sys_user", {
1806
+ label: "Reassigned From",
1807
+ required: false,
1808
+ group: "Action",
1809
+ description: "User whose pending-approver slot was handed over (reassign actions only)"
1810
+ }),
1811
+ reassign_to: import_data2.Field.lookup("sys_user", {
1812
+ label: "Reassigned To",
1813
+ required: false,
1814
+ group: "Action",
1815
+ description: "User who received the pending-approver slot (reassign actions only)"
1816
+ }),
1766
1817
  attachments: import_data2.Field.file({
1767
1818
  label: "Attachments",
1768
1819
  required: false,
@@ -1949,8 +2000,9 @@ var SysApprovalDelegation = import_data4.ObjectSchema.create({
1949
2000
  var import_node_crypto = require("crypto");
1950
2001
  var import_automation = require("@objectstack/spec/automation");
1951
2002
  var import_formula = require("@objectstack/formula");
2003
+ var import_types = require("@objectstack/types");
1952
2004
  var import_identity = require("@objectstack/spec/identity");
1953
- var import_contracts = require("@objectstack/spec/contracts");
2005
+ var import_contracts3 = require("@objectstack/spec/contracts");
1954
2006
  var import_data5 = require("@objectstack/spec/data");
1955
2007
  var import_core = require("@objectstack/core");
1956
2008
 
@@ -1963,7 +2015,7 @@ function fail(message) {
1963
2015
  async function findOrg(engine, where) {
1964
2016
  try {
1965
2017
  const rows = await engine.find("sys_organization", {
1966
- filter: where,
2018
+ where,
1967
2019
  fields: ["id", "slug", "parent_organization_id"],
1968
2020
  limit: 1,
1969
2021
  context: SYSTEM_CTX
@@ -2050,7 +2102,7 @@ async function filterApproversWhoCanRead(deps, userIds, requestOrgId, context) {
2050
2102
  let members = [];
2051
2103
  try {
2052
2104
  members = await deps.engine.find("sys_member", {
2053
- filter: { organization_id: requestOrg, user_id: { $in: userIds } },
2105
+ where: { organization_id: requestOrg, user_id: { $in: userIds } },
2054
2106
  fields: ["user_id"],
2055
2107
  limit: 1e4,
2056
2108
  context: SYSTEM_CTX
@@ -2120,6 +2172,12 @@ function csvSplit(raw) {
2120
2172
  if (Array.isArray(raw)) return raw.map(String).filter(Boolean);
2121
2173
  return String(raw).split(",").map((s) => s.trim()).filter(Boolean);
2122
2174
  }
2175
+ function isBlankDecisionOutput(value) {
2176
+ if (value === void 0 || value === null) return true;
2177
+ if (typeof value === "string") return value.trim() === "";
2178
+ if (Array.isArray(value)) return value.filter((v) => v !== null && v !== void 0 && String(v).trim() !== "").length === 0;
2179
+ return false;
2180
+ }
2123
2181
  function prettifyMachineName(raw) {
2124
2182
  if (!raw) return void 0;
2125
2183
  const base = String(raw).replace(/^flow:/, "").trim();
@@ -2212,6 +2270,9 @@ function rowFromAction(row) {
2212
2270
  action: row.action,
2213
2271
  actor_id: row.actor_id ?? void 0,
2214
2272
  comment: row.comment ?? void 0,
2273
+ // Structured reassign hand-off parties (#4365).
2274
+ reassign_from: row.reassign_from ?? void 0,
2275
+ reassign_to: row.reassign_to ?? void 0,
2215
2276
  // Decision attachments (#3266): rich descriptors carrying the display name +
2216
2277
  // download URL, so consumers label/open them without reading `sys_file`.
2217
2278
  attachments: attachments.length ? attachments : void 0,
@@ -2351,9 +2412,10 @@ var _ApprovalService = class _ApprovalService {
2351
2412
  * (having also put them on the context). Those are the only two callers that
2352
2413
  * hold a trustworthy actor with no session behind them.
2353
2414
  *
2354
- * A caller with NO identity at all cannot act. That case is reachable: the
2355
- * REST anonymous-deny only fires when `api.requireAuth` is set, so without it
2356
- * an anonymous request previously decided approvals outright by naming one.
2415
+ * A caller with NO identity at all cannot act. Belt-and-suspenders: the REST
2416
+ * anonymous-deny now denies every anonymous request (#3963), but this service
2417
+ * must not rely on a caller upstream an anonymous actor could otherwise
2418
+ * decide approvals outright by naming one.
2357
2419
  */
2358
2420
  async resolveActor(actorId, context) {
2359
2421
  if (context?.isSystem) {
@@ -2639,7 +2701,7 @@ var _ApprovalService = class _ApprovalService {
2639
2701
  let rows = [];
2640
2702
  try {
2641
2703
  rows = await this.engine.find("sys_team_member", {
2642
- filter: { team_id: teamId },
2704
+ where: { team_id: teamId },
2643
2705
  fields: ["user_id"],
2644
2706
  limit: 1e4,
2645
2707
  context: SYSTEM_CTX2
@@ -2678,7 +2740,7 @@ var _ApprovalService = class _ApprovalService {
2678
2740
  if (!businessUnitId) return [];
2679
2741
  try {
2680
2742
  const seed = await this.engine.find("sys_business_unit", {
2681
- filter: this.businessUnitOrgScope({ id: businessUnitId }, organizationId),
2743
+ where: this.businessUnitOrgScope({ id: businessUnitId }, organizationId),
2682
2744
  fields: ["id", "active"],
2683
2745
  limit: 1,
2684
2746
  context: SYSTEM_CTX2
@@ -2713,7 +2775,7 @@ var _ApprovalService = class _ApprovalService {
2713
2775
  let rows = [];
2714
2776
  try {
2715
2777
  rows = await this.engine.find("sys_business_unit_member", {
2716
- filter: { business_unit_id: { $in: Array.from(seen) } },
2778
+ where: { business_unit_id: { $in: Array.from(seen) } },
2717
2779
  fields: ["user_id"],
2718
2780
  limit: 1e4,
2719
2781
  context: SYSTEM_CTX2
@@ -2773,7 +2835,7 @@ var _ApprovalService = class _ApprovalService {
2773
2835
  async lookupManager(userId) {
2774
2836
  try {
2775
2837
  const rows = await this.engine.find("sys_user", {
2776
- filter: { id: userId },
2838
+ where: { id: userId },
2777
2839
  fields: ["id", "manager_id"],
2778
2840
  limit: 1,
2779
2841
  context: SYSTEM_CTX2
@@ -2823,7 +2885,7 @@ var _ApprovalService = class _ApprovalService {
2823
2885
  let rows = [];
2824
2886
  try {
2825
2887
  rows = await this.engine.find("sys_approval_delegation", {
2826
- filter: { delegator_id: delegatorId },
2888
+ where: { delegator_id: delegatorId },
2827
2889
  fields: ["id", "delegator_id", "delegate_id", "valid_from", "valid_until", "reason", "organization_id"],
2828
2890
  limit: 50,
2829
2891
  context: SYSTEM_CTX2
@@ -3149,8 +3211,9 @@ var _ApprovalService = class _ApprovalService {
3149
3211
  const now = this.clock.now().toISOString();
3150
3212
  const outputKeys = input.outputs ? Object.keys(input.outputs) : [];
3151
3213
  let acceptedOutputs;
3214
+ const declaredDefs = (0, import_automation.normalizeDecisionOutputs)(config.decisionOutputs);
3152
3215
  if (outputKeys.length) {
3153
- const declared = (0, import_automation.normalizeDecisionOutputs)(config.decisionOutputs).map((d) => d.key);
3216
+ const declared = declaredDefs.map((d) => d.key);
3154
3217
  if (!declared.length) {
3155
3218
  throw new Error(
3156
3219
  `VALIDATION_FAILED: this approval node declares no decisionOutputs \u2014 outputs are not accepted. Declare the keys on the node config (decisionOutputs: [${outputKeys.map((k) => `'${k}'`).join(", ")}]) to let approvers hand them to the flow.`
@@ -3170,6 +3233,14 @@ var _ApprovalService = class _ApprovalService {
3170
3233
  }
3171
3234
  acceptedOutputs = { ...input.outputs };
3172
3235
  }
3236
+ if (input.decision === "approve") {
3237
+ const missing = declaredDefs.filter((d) => d.required === true && isBlankDecisionOutput(input.outputs?.[d.key])).map((d) => d.key);
3238
+ if (missing.length) {
3239
+ throw new Error(
3240
+ `VALIDATION_FAILED: decision output(s) \`${missing.join("`, `")}\` are required to approve this request \u2014 open the approval and fill them in before approving.`
3241
+ );
3242
+ }
3243
+ }
3173
3244
  await this.engine.insert("sys_approval_action", {
3174
3245
  id: uid("aact"),
3175
3246
  request_id: requestId,
@@ -3267,7 +3338,7 @@ var _ApprovalService = class _ApprovalService {
3267
3338
  * try/catch, because what a failed resume means differs per path.
3268
3339
  */
3269
3340
  async serviceResume(runId, signal) {
3270
- await this.automation.resume(runId, { ...signal, [import_contracts.RESUME_AUTHORITY_SERVICE]: true });
3341
+ await this.automation.resume(runId, { ...signal, [import_contracts3.RESUME_AUTHORITY_SERVICE]: true });
3271
3342
  }
3272
3343
  /**
3273
3344
  * Public contract entrypoint (ADR-0019). Records a decision on a node-driven
@@ -3704,8 +3775,13 @@ var _ApprovalService = class _ApprovalService {
3704
3775
  step_name: raw.flow_node_id ?? raw.current_step ?? null,
3705
3776
  step_index: 0,
3706
3777
  action: "reassign",
3778
+ // The hand-off parties are STRUCTURED fields (#4365) — the old default
3779
+ // comment (`"<from> → <to>"`) baked raw user ids into user-facing text.
3780
+ // `comment` is pure user input: absent unless the actor wrote one.
3707
3781
  actor_id: actorId,
3708
- comment: input.comment ?? `${from} \u2192 ${to}`,
3782
+ reassign_from: from,
3783
+ reassign_to: to,
3784
+ comment: input.comment ?? null,
3709
3785
  created_at: now
3710
3786
  }, { context: SYSTEM_CTX2 });
3711
3787
  let configPatch = {};
@@ -4513,35 +4589,28 @@ var _ApprovalService = class _ApprovalService {
4513
4589
  async rebuildApproverIndex() {
4514
4590
  const desired = /* @__PURE__ */ new Map();
4515
4591
  const PAGE = 500;
4516
- for (let offset = 0; ; offset += PAGE) {
4517
- const batch = await this.engine.find("sys_approval_request", {
4518
- where: { status: "pending" },
4592
+ const requests = (0, import_types.keysetWalk)(
4593
+ (q) => this.engine.find("sys_approval_request", {
4594
+ ...q,
4519
4595
  fields: ["id", "pending_approvers", "organization_id"],
4520
- limit: PAGE,
4521
- offset,
4522
4596
  context: SYSTEM_CTX2
4523
- });
4524
- const rows = Array.isArray(batch) ? batch : [];
4597
+ }),
4598
+ { where: { status: "pending" }, pageSize: PAGE }
4599
+ );
4600
+ for await (const rows of requests.pages()) {
4525
4601
  for (const r of rows) {
4526
4602
  desired.set(String(r.id), {
4527
4603
  approvers: new Set(csvSplit(r.pending_approvers)),
4528
4604
  org: r.organization_id ?? null
4529
4605
  });
4530
4606
  }
4531
- if (rows.length < PAGE) break;
4532
4607
  }
4533
4608
  const indexRows = [];
4534
- for (let offset = 0; ; offset += PAGE) {
4535
- const batch = await this.engine.find("sys_approval_approver", {
4536
- orderBy: [{ field: "created_at", order: "asc" }],
4537
- limit: PAGE,
4538
- offset,
4539
- context: SYSTEM_CTX2
4540
- });
4541
- const rows = Array.isArray(batch) ? batch : [];
4542
- indexRows.push(...rows);
4543
- if (rows.length < PAGE) break;
4544
- }
4609
+ const index = (0, import_types.keysetWalk)(
4610
+ (q) => this.engine.find("sys_approval_approver", { ...q, context: SYSTEM_CTX2 }),
4611
+ { pageSize: PAGE }
4612
+ );
4613
+ for await (const rows of index.pages()) indexRows.push(...rows);
4545
4614
  let inserted = 0;
4546
4615
  let deleted = 0;
4547
4616
  const seen = /* @__PURE__ */ new Map();
@@ -4925,11 +4994,15 @@ var _ApprovalService = class _ApprovalService {
4925
4994
  });
4926
4995
  const actions = Array.isArray(rows) ? rows.map(rowFromAction) : [];
4927
4996
  const names = await this.resolveUserNames(
4928
- actions.map((a) => a.actor_id).filter((id) => id && !id.includes(":"))
4997
+ actions.flatMap((a) => [a.actor_id, a.reassign_from, a.reassign_to]).filter((id) => id && !id.includes(":"))
4929
4998
  );
4930
4999
  for (const a of actions) {
4931
5000
  const n = a.actor_id ? names.get(String(a.actor_id)) : void 0;
4932
5001
  if (n) a.actor_name = n;
5002
+ const fromName = a.reassign_from ? names.get(String(a.reassign_from)) : void 0;
5003
+ if (fromName) a.reassign_from_name = fromName;
5004
+ const toName = a.reassign_to ? names.get(String(a.reassign_to)) : void 0;
5005
+ if (toName) a.reassign_to_name = toName;
4933
5006
  }
4934
5007
  return actions;
4935
5008
  }