@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.mjs CHANGED
@@ -245,6 +245,14 @@ var init_en_objects_generated = __esm({
245
245
  comment: {
246
246
  label: "Comment"
247
247
  },
248
+ reassign_from: {
249
+ label: "Reassigned From",
250
+ help: "User whose pending-approver slot was handed over (reassign actions only)"
251
+ },
252
+ reassign_to: {
253
+ label: "Reassigned To",
254
+ help: "User who received the pending-approver slot (reassign actions only)"
255
+ },
248
256
  attachments: {
249
257
  label: "Attachments",
250
258
  help: "Files supporting this action \u2014 e.g. a signed contract or evidence (#3266)."
@@ -554,6 +562,14 @@ var init_zh_CN_objects_generated = __esm({
554
562
  comment: {
555
563
  label: "\u8BC4\u8BBA"
556
564
  },
565
+ reassign_from: {
566
+ label: "\u8F6C\u51FA\u4EBA",
567
+ help: "\u88AB\u79FB\u4EA4\u5F85\u5BA1\u6279\u69FD\u4F4D\u7684\u7528\u6237\uFF08\u4EC5\u8F6C\u7B7E\u64CD\u4F5C\uFF09"
568
+ },
569
+ reassign_to: {
570
+ label: "\u8F6C\u5165\u4EBA",
571
+ help: "\u63A5\u6536\u5F85\u5BA1\u6279\u69FD\u4F4D\u7684\u7528\u6237\uFF08\u4EC5\u8F6C\u7B7E\u64CD\u4F5C\uFF09"
572
+ },
557
573
  attachments: {
558
574
  label: "\u9644\u4EF6",
559
575
  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"
@@ -863,6 +879,14 @@ var init_ja_JP_objects_generated = __esm({
863
879
  comment: {
864
880
  label: "\u30B3\u30E1\u30F3\u30C8"
865
881
  },
882
+ reassign_from: {
883
+ label: "\u5F15\u304D\u7D99\u304E\u5143",
884
+ 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"
885
+ },
886
+ reassign_to: {
887
+ label: "\u5F15\u304D\u7D99\u304E\u5148",
888
+ 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"
889
+ },
866
890
  attachments: {
867
891
  label: "\u6DFB\u4ED8\u30D5\u30A1\u30A4\u30EB",
868
892
  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"
@@ -1172,6 +1196,14 @@ var init_es_ES_objects_generated = __esm({
1172
1196
  comment: {
1173
1197
  label: "Comentario"
1174
1198
  },
1199
+ reassign_from: {
1200
+ label: "Reasignado de",
1201
+ help: "Usuario cuyo turno de aprobaci\xF3n pendiente fue traspasado (solo acciones de reasignaci\xF3n)"
1202
+ },
1203
+ reassign_to: {
1204
+ label: "Reasignado a",
1205
+ help: "Usuario que recibi\xF3 el turno de aprobaci\xF3n pendiente (solo acciones de reasignaci\xF3n)"
1206
+ },
1175
1207
  attachments: {
1176
1208
  label: "Adjuntos",
1177
1209
  help: "Archivos que respaldan esta acci\xF3n, p. ej. un contrato firmado o pruebas (#3266)."
@@ -1273,6 +1305,7 @@ var init_translations = __esm({
1273
1305
 
1274
1306
  // src/sys-approval-request.object.ts
1275
1307
  import { ObjectSchema, Field } from "@objectstack/spec/data";
1308
+ import { APPROVAL_STATUSES } from "@objectstack/spec/contracts";
1276
1309
  var SysApprovalRequest = ObjectSchema.create({
1277
1310
  name: "sys_approval_request",
1278
1311
  label: "Approval Request",
@@ -1371,9 +1404,10 @@ var SysApprovalRequest = ObjectSchema.create({
1371
1404
  group: "Target"
1372
1405
  }),
1373
1406
  status: Field.select(
1374
- // Keep in sync with `ApprovalStatus` (spec/contracts). `returned` =
1375
- // sent back for revision (ADR-0044) terminal for this round.
1376
- ["pending", "approved", "rejected", "recalled", "returned"],
1407
+ // Spread from the contract, not re-typed (#3786). `APPROVAL_STATUSES` is
1408
+ // where the list and the reason for each entry live; `ApprovalStatus` is
1409
+ // derived from it, so this column and the contract cannot disagree.
1410
+ [...APPROVAL_STATUSES],
1377
1411
  {
1378
1412
  label: "Status",
1379
1413
  required: true,
@@ -1642,6 +1676,7 @@ var SysApprovalRequest = ObjectSchema.create({
1642
1676
 
1643
1677
  // src/sys-approval-action.object.ts
1644
1678
  import { ObjectSchema as ObjectSchema2, Field as Field2 } from "@objectstack/spec/data";
1679
+ import { APPROVAL_ACTION_KINDS } from "@objectstack/spec/contracts";
1645
1680
  var SysApprovalAction = ObjectSchema2.create({
1646
1681
  name: "sys_approval_action",
1647
1682
  label: "Approval Action",
@@ -1720,12 +1755,11 @@ var SysApprovalAction = ObjectSchema2.create({
1720
1755
  group: "Target"
1721
1756
  }),
1722
1757
  action: Field2.select(
1723
- // Keep in sync with `ApprovalActionKind` (spec/contracts). reassign /
1724
- // remind / request_info / comment are thread interactions they never
1725
- // move the flow. revise / resubmit (ADR-0044) DO move it: send back for
1726
- // revision and the later resubmission. ooo_substitute (#1322 M1) is a
1727
- // system-recorded reroute of an out-of-office approver — no flow movement.
1728
- ["submit", "approve", "reject", "recall", "escalate", "reassign", "remind", "request_info", "comment", "revise", "resubmit", "ooo_substitute"],
1758
+ // Spread from the contract, not re-typed (#3786). `APPROVAL_ACTION_KINDS`
1759
+ // is where the list and the per-kind notes live (which kinds move the flow
1760
+ // and which are thread-only); `ApprovalActionKind` is derived from it, so
1761
+ // this column and the contract cannot disagree.
1762
+ [...APPROVAL_ACTION_KINDS],
1729
1763
  {
1730
1764
  label: "Action",
1731
1765
  required: true,
@@ -1738,6 +1772,23 @@ var SysApprovalAction = ObjectSchema2.create({
1738
1772
  group: "Action"
1739
1773
  }),
1740
1774
  comment: Field2.textarea({ label: "Comment", required: false, group: "Action" }),
1775
+ // Structured hand-off parties for `action: 'reassign'` (#4365). Before
1776
+ // these existed the pair lived only inside a default free-text comment
1777
+ // ("<from_id> → <to_id>"), which no client could parse or render readably.
1778
+ // `comment` is pure user input again; timelines render "from A to B" from
1779
+ // these fields.
1780
+ reassign_from: Field2.lookup("sys_user", {
1781
+ label: "Reassigned From",
1782
+ required: false,
1783
+ group: "Action",
1784
+ description: "User whose pending-approver slot was handed over (reassign actions only)"
1785
+ }),
1786
+ reassign_to: Field2.lookup("sys_user", {
1787
+ label: "Reassigned To",
1788
+ required: false,
1789
+ group: "Action",
1790
+ description: "User who received the pending-approver slot (reassign actions only)"
1791
+ }),
1741
1792
  attachments: Field2.file({
1742
1793
  label: "Attachments",
1743
1794
  required: false,
@@ -1929,6 +1980,7 @@ import {
1929
1980
  normalizeDecisionOutputs
1930
1981
  } from "@objectstack/spec/automation";
1931
1982
  import { ExpressionEngine, collectCelRootIdentifiers } from "@objectstack/formula";
1983
+ import { keysetWalk } from "@objectstack/types";
1932
1984
  import {
1933
1985
  ADMIN_FULL_ACCESS,
1934
1986
  ORGANIZATION_ADMIN_GRANTS,
@@ -1949,7 +2001,7 @@ function fail(message) {
1949
2001
  async function findOrg(engine, where) {
1950
2002
  try {
1951
2003
  const rows = await engine.find("sys_organization", {
1952
- filter: where,
2004
+ where,
1953
2005
  fields: ["id", "slug", "parent_organization_id"],
1954
2006
  limit: 1,
1955
2007
  context: SYSTEM_CTX
@@ -2036,7 +2088,7 @@ async function filterApproversWhoCanRead(deps, userIds, requestOrgId, context) {
2036
2088
  let members = [];
2037
2089
  try {
2038
2090
  members = await deps.engine.find("sys_member", {
2039
- filter: { organization_id: requestOrg, user_id: { $in: userIds } },
2091
+ where: { organization_id: requestOrg, user_id: { $in: userIds } },
2040
2092
  fields: ["user_id"],
2041
2093
  limit: 1e4,
2042
2094
  context: SYSTEM_CTX
@@ -2106,6 +2158,12 @@ function csvSplit(raw) {
2106
2158
  if (Array.isArray(raw)) return raw.map(String).filter(Boolean);
2107
2159
  return String(raw).split(",").map((s) => s.trim()).filter(Boolean);
2108
2160
  }
2161
+ function isBlankDecisionOutput(value) {
2162
+ if (value === void 0 || value === null) return true;
2163
+ if (typeof value === "string") return value.trim() === "";
2164
+ if (Array.isArray(value)) return value.filter((v) => v !== null && v !== void 0 && String(v).trim() !== "").length === 0;
2165
+ return false;
2166
+ }
2109
2167
  function prettifyMachineName(raw) {
2110
2168
  if (!raw) return void 0;
2111
2169
  const base = String(raw).replace(/^flow:/, "").trim();
@@ -2198,6 +2256,9 @@ function rowFromAction(row) {
2198
2256
  action: row.action,
2199
2257
  actor_id: row.actor_id ?? void 0,
2200
2258
  comment: row.comment ?? void 0,
2259
+ // Structured reassign hand-off parties (#4365).
2260
+ reassign_from: row.reassign_from ?? void 0,
2261
+ reassign_to: row.reassign_to ?? void 0,
2201
2262
  // Decision attachments (#3266): rich descriptors carrying the display name +
2202
2263
  // download URL, so consumers label/open them without reading `sys_file`.
2203
2264
  attachments: attachments.length ? attachments : void 0,
@@ -2337,9 +2398,10 @@ var _ApprovalService = class _ApprovalService {
2337
2398
  * (having also put them on the context). Those are the only two callers that
2338
2399
  * hold a trustworthy actor with no session behind them.
2339
2400
  *
2340
- * A caller with NO identity at all cannot act. That case is reachable: the
2341
- * REST anonymous-deny only fires when `api.requireAuth` is set, so without it
2342
- * an anonymous request previously decided approvals outright by naming one.
2401
+ * A caller with NO identity at all cannot act. Belt-and-suspenders: the REST
2402
+ * anonymous-deny now denies every anonymous request (#3963), but this service
2403
+ * must not rely on a caller upstream an anonymous actor could otherwise
2404
+ * decide approvals outright by naming one.
2343
2405
  */
2344
2406
  async resolveActor(actorId, context) {
2345
2407
  if (context?.isSystem) {
@@ -2625,7 +2687,7 @@ var _ApprovalService = class _ApprovalService {
2625
2687
  let rows = [];
2626
2688
  try {
2627
2689
  rows = await this.engine.find("sys_team_member", {
2628
- filter: { team_id: teamId },
2690
+ where: { team_id: teamId },
2629
2691
  fields: ["user_id"],
2630
2692
  limit: 1e4,
2631
2693
  context: SYSTEM_CTX2
@@ -2664,7 +2726,7 @@ var _ApprovalService = class _ApprovalService {
2664
2726
  if (!businessUnitId) return [];
2665
2727
  try {
2666
2728
  const seed = await this.engine.find("sys_business_unit", {
2667
- filter: this.businessUnitOrgScope({ id: businessUnitId }, organizationId),
2729
+ where: this.businessUnitOrgScope({ id: businessUnitId }, organizationId),
2668
2730
  fields: ["id", "active"],
2669
2731
  limit: 1,
2670
2732
  context: SYSTEM_CTX2
@@ -2699,7 +2761,7 @@ var _ApprovalService = class _ApprovalService {
2699
2761
  let rows = [];
2700
2762
  try {
2701
2763
  rows = await this.engine.find("sys_business_unit_member", {
2702
- filter: { business_unit_id: { $in: Array.from(seen) } },
2764
+ where: { business_unit_id: { $in: Array.from(seen) } },
2703
2765
  fields: ["user_id"],
2704
2766
  limit: 1e4,
2705
2767
  context: SYSTEM_CTX2
@@ -2759,7 +2821,7 @@ var _ApprovalService = class _ApprovalService {
2759
2821
  async lookupManager(userId) {
2760
2822
  try {
2761
2823
  const rows = await this.engine.find("sys_user", {
2762
- filter: { id: userId },
2824
+ where: { id: userId },
2763
2825
  fields: ["id", "manager_id"],
2764
2826
  limit: 1,
2765
2827
  context: SYSTEM_CTX2
@@ -2809,7 +2871,7 @@ var _ApprovalService = class _ApprovalService {
2809
2871
  let rows = [];
2810
2872
  try {
2811
2873
  rows = await this.engine.find("sys_approval_delegation", {
2812
- filter: { delegator_id: delegatorId },
2874
+ where: { delegator_id: delegatorId },
2813
2875
  fields: ["id", "delegator_id", "delegate_id", "valid_from", "valid_until", "reason", "organization_id"],
2814
2876
  limit: 50,
2815
2877
  context: SYSTEM_CTX2
@@ -3135,8 +3197,9 @@ var _ApprovalService = class _ApprovalService {
3135
3197
  const now = this.clock.now().toISOString();
3136
3198
  const outputKeys = input.outputs ? Object.keys(input.outputs) : [];
3137
3199
  let acceptedOutputs;
3200
+ const declaredDefs = normalizeDecisionOutputs(config.decisionOutputs);
3138
3201
  if (outputKeys.length) {
3139
- const declared = normalizeDecisionOutputs(config.decisionOutputs).map((d) => d.key);
3202
+ const declared = declaredDefs.map((d) => d.key);
3140
3203
  if (!declared.length) {
3141
3204
  throw new Error(
3142
3205
  `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.`
@@ -3156,6 +3219,14 @@ var _ApprovalService = class _ApprovalService {
3156
3219
  }
3157
3220
  acceptedOutputs = { ...input.outputs };
3158
3221
  }
3222
+ if (input.decision === "approve") {
3223
+ const missing = declaredDefs.filter((d) => d.required === true && isBlankDecisionOutput(input.outputs?.[d.key])).map((d) => d.key);
3224
+ if (missing.length) {
3225
+ throw new Error(
3226
+ `VALIDATION_FAILED: decision output(s) \`${missing.join("`, `")}\` are required to approve this request \u2014 open the approval and fill them in before approving.`
3227
+ );
3228
+ }
3229
+ }
3159
3230
  await this.engine.insert("sys_approval_action", {
3160
3231
  id: uid("aact"),
3161
3232
  request_id: requestId,
@@ -3690,8 +3761,13 @@ var _ApprovalService = class _ApprovalService {
3690
3761
  step_name: raw.flow_node_id ?? raw.current_step ?? null,
3691
3762
  step_index: 0,
3692
3763
  action: "reassign",
3764
+ // The hand-off parties are STRUCTURED fields (#4365) — the old default
3765
+ // comment (`"<from> → <to>"`) baked raw user ids into user-facing text.
3766
+ // `comment` is pure user input: absent unless the actor wrote one.
3693
3767
  actor_id: actorId,
3694
- comment: input.comment ?? `${from} \u2192 ${to}`,
3768
+ reassign_from: from,
3769
+ reassign_to: to,
3770
+ comment: input.comment ?? null,
3695
3771
  created_at: now
3696
3772
  }, { context: SYSTEM_CTX2 });
3697
3773
  let configPatch = {};
@@ -4499,35 +4575,28 @@ var _ApprovalService = class _ApprovalService {
4499
4575
  async rebuildApproverIndex() {
4500
4576
  const desired = /* @__PURE__ */ new Map();
4501
4577
  const PAGE = 500;
4502
- for (let offset = 0; ; offset += PAGE) {
4503
- const batch = await this.engine.find("sys_approval_request", {
4504
- where: { status: "pending" },
4578
+ const requests = keysetWalk(
4579
+ (q) => this.engine.find("sys_approval_request", {
4580
+ ...q,
4505
4581
  fields: ["id", "pending_approvers", "organization_id"],
4506
- limit: PAGE,
4507
- offset,
4508
4582
  context: SYSTEM_CTX2
4509
- });
4510
- const rows = Array.isArray(batch) ? batch : [];
4583
+ }),
4584
+ { where: { status: "pending" }, pageSize: PAGE }
4585
+ );
4586
+ for await (const rows of requests.pages()) {
4511
4587
  for (const r of rows) {
4512
4588
  desired.set(String(r.id), {
4513
4589
  approvers: new Set(csvSplit(r.pending_approvers)),
4514
4590
  org: r.organization_id ?? null
4515
4591
  });
4516
4592
  }
4517
- if (rows.length < PAGE) break;
4518
4593
  }
4519
4594
  const indexRows = [];
4520
- for (let offset = 0; ; offset += PAGE) {
4521
- const batch = await this.engine.find("sys_approval_approver", {
4522
- orderBy: [{ field: "created_at", order: "asc" }],
4523
- limit: PAGE,
4524
- offset,
4525
- context: SYSTEM_CTX2
4526
- });
4527
- const rows = Array.isArray(batch) ? batch : [];
4528
- indexRows.push(...rows);
4529
- if (rows.length < PAGE) break;
4530
- }
4595
+ const index = keysetWalk(
4596
+ (q) => this.engine.find("sys_approval_approver", { ...q, context: SYSTEM_CTX2 }),
4597
+ { pageSize: PAGE }
4598
+ );
4599
+ for await (const rows of index.pages()) indexRows.push(...rows);
4531
4600
  let inserted = 0;
4532
4601
  let deleted = 0;
4533
4602
  const seen = /* @__PURE__ */ new Map();
@@ -4911,11 +4980,15 @@ var _ApprovalService = class _ApprovalService {
4911
4980
  });
4912
4981
  const actions = Array.isArray(rows) ? rows.map(rowFromAction) : [];
4913
4982
  const names = await this.resolveUserNames(
4914
- actions.map((a) => a.actor_id).filter((id) => id && !id.includes(":"))
4983
+ actions.flatMap((a) => [a.actor_id, a.reassign_from, a.reassign_to]).filter((id) => id && !id.includes(":"))
4915
4984
  );
4916
4985
  for (const a of actions) {
4917
4986
  const n = a.actor_id ? names.get(String(a.actor_id)) : void 0;
4918
4987
  if (n) a.actor_name = n;
4988
+ const fromName = a.reassign_from ? names.get(String(a.reassign_from)) : void 0;
4989
+ if (fromName) a.reassign_from_name = fromName;
4990
+ const toName = a.reassign_to ? names.get(String(a.reassign_to)) : void 0;
4991
+ if (toName) a.reassign_to_name = toName;
4919
4992
  }
4920
4993
  return actions;
4921
4994
  }