@objectstack/plugin-approvals 17.2.0 → 17.3.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/CHANGELOG.md +891 -0
- package/dist/index.d.mts +1206 -579
- package/dist/index.d.ts +1206 -579
- package/dist/index.js +620 -75
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +626 -76
- package/dist/index.mjs.map +1 -1
- package/package.json +18 -15
package/dist/index.js
CHANGED
|
@@ -41,7 +41,7 @@ var init_en_objects_generated = __esm({
|
|
|
41
41
|
},
|
|
42
42
|
organization_id: {
|
|
43
43
|
label: "Organization",
|
|
44
|
-
help: "
|
|
44
|
+
help: "Organization of the record this request is about (falls back to the acting context when the record has none)"
|
|
45
45
|
},
|
|
46
46
|
process_name: {
|
|
47
47
|
label: "Source",
|
|
@@ -67,7 +67,15 @@ var init_en_objects_generated = __esm({
|
|
|
67
67
|
approved: "Approved",
|
|
68
68
|
rejected: "Rejected",
|
|
69
69
|
recalled: "Recalled",
|
|
70
|
-
returned: "Returned"
|
|
70
|
+
returned: "Returned",
|
|
71
|
+
cancelled: "Cancelled"
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
cancel_reason: {
|
|
75
|
+
label: "Cancellation Reason",
|
|
76
|
+
help: "Why the platform voided this request (set only when the status is cancelled)",
|
|
77
|
+
options: {
|
|
78
|
+
record_deleted: "Related record deleted"
|
|
71
79
|
}
|
|
72
80
|
},
|
|
73
81
|
current_step: {
|
|
@@ -248,7 +256,8 @@ var init_en_objects_generated = __esm({
|
|
|
248
256
|
comment: "Comment",
|
|
249
257
|
revise: "Revise",
|
|
250
258
|
resubmit: "Resubmit",
|
|
251
|
-
ooo_substitute: "Out-of-Office Substitution"
|
|
259
|
+
ooo_substitute: "Out-of-Office Substitution",
|
|
260
|
+
cancel: "Cancel"
|
|
252
261
|
}
|
|
253
262
|
},
|
|
254
263
|
actor_id: {
|
|
@@ -362,7 +371,7 @@ var init_zh_CN_objects_generated = __esm({
|
|
|
362
371
|
},
|
|
363
372
|
organization_id: {
|
|
364
373
|
label: "\u7EC4\u7EC7",
|
|
365
|
-
help: "\
|
|
374
|
+
help: "\u8BE5\u8BF7\u6C42\u6240\u6D89\u8BB0\u5F55\u6240\u5C5E\u7684\u7EC4\u7EC7\uFF08\u8BB0\u5F55\u65E0\u7EC4\u7EC7\u65F6\u56DE\u9000\u5230\u64CD\u4F5C\u4E0A\u4E0B\u6587\uFF09"
|
|
366
375
|
},
|
|
367
376
|
process_name: {
|
|
368
377
|
label: "\u6765\u6E90",
|
|
@@ -388,7 +397,15 @@ var init_zh_CN_objects_generated = __esm({
|
|
|
388
397
|
approved: "\u5DF2\u6279\u51C6",
|
|
389
398
|
rejected: "\u5DF2\u62D2\u7EDD",
|
|
390
399
|
recalled: "\u5DF2\u64A4\u56DE",
|
|
391
|
-
returned: "\u5DF2\u9000\u56DE\u4FEE\u6539"
|
|
400
|
+
returned: "\u5DF2\u9000\u56DE\u4FEE\u6539",
|
|
401
|
+
cancelled: "\u5DF2\u4F5C\u5E9F"
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
cancel_reason: {
|
|
405
|
+
label: "\u4F5C\u5E9F\u539F\u56E0",
|
|
406
|
+
help: "\u5E73\u53F0\u4F5C\u5E9F\u8BE5\u5BA1\u6279\u5355\u7684\u539F\u56E0(\u4EC5\u5728\u72B6\u6001\u4E3A\u300C\u5DF2\u4F5C\u5E9F\u300D\u65F6\u5199\u5165)",
|
|
407
|
+
options: {
|
|
408
|
+
record_deleted: "\u5173\u8054\u8BB0\u5F55\u5DF2\u5220\u9664"
|
|
392
409
|
}
|
|
393
410
|
},
|
|
394
411
|
current_step: {
|
|
@@ -569,7 +586,8 @@ var init_zh_CN_objects_generated = __esm({
|
|
|
569
586
|
comment: "\u8BC4\u8BBA",
|
|
570
587
|
revise: "\u9000\u56DE\u4FEE\u6539",
|
|
571
588
|
resubmit: "\u91CD\u65B0\u63D0\u4EA4",
|
|
572
|
-
ooo_substitute: "\u4E0D\u5728\u5C97\u6539\u6D3E"
|
|
589
|
+
ooo_substitute: "\u4E0D\u5728\u5C97\u6539\u6D3E",
|
|
590
|
+
cancel: "\u4F5C\u5E9F"
|
|
573
591
|
}
|
|
574
592
|
},
|
|
575
593
|
actor_id: {
|
|
@@ -683,7 +701,7 @@ var init_ja_JP_objects_generated = __esm({
|
|
|
683
701
|
},
|
|
684
702
|
organization_id: {
|
|
685
703
|
label: "\u7D44\u7E54",
|
|
686
|
-
help: "\u3053\u306E\
|
|
704
|
+
help: "\u3053\u306E\u30EA\u30AF\u30A8\u30B9\u30C8\u306E\u5BFE\u8C61\u30EC\u30B3\u30FC\u30C9\u304C\u5C5E\u3059\u308B\u7D44\u7E54\uFF08\u30EC\u30B3\u30FC\u30C9\u306B\u7D44\u7E54\u304C\u306A\u3044\u5834\u5408\u306F\u64CD\u4F5C\u30B3\u30F3\u30C6\u30AD\u30B9\u30C8\u306B\u30D5\u30A9\u30FC\u30EB\u30D0\u30C3\u30AF\uFF09"
|
|
687
705
|
},
|
|
688
706
|
process_name: {
|
|
689
707
|
label: "\u30BD\u30FC\u30B9",
|
|
@@ -709,7 +727,15 @@ var init_ja_JP_objects_generated = __esm({
|
|
|
709
727
|
approved: "\u627F\u8A8D\u6E08\u307F",
|
|
710
728
|
rejected: "\u5374\u4E0B\u6E08\u307F",
|
|
711
729
|
recalled: "\u53D6\u308A\u6D88\u3057\u6E08\u307F",
|
|
712
|
-
returned: "\u5DEE\u3057\u623B\u3057\u6E08\u307F"
|
|
730
|
+
returned: "\u5DEE\u3057\u623B\u3057\u6E08\u307F",
|
|
731
|
+
cancelled: "\u7121\u52B9\u5316\u6E08\u307F"
|
|
732
|
+
}
|
|
733
|
+
},
|
|
734
|
+
cancel_reason: {
|
|
735
|
+
label: "\u7121\u52B9\u5316\u7406\u7531",
|
|
736
|
+
help: "\u30D7\u30E9\u30C3\u30C8\u30D5\u30A9\u30FC\u30E0\u304C\u3053\u306E\u7533\u8ACB\u3092\u7121\u52B9\u5316\u3057\u305F\u7406\u7531(\u30B9\u30C6\u30FC\u30BF\u30B9\u304C\u7121\u52B9\u5316\u6E08\u307F\u306E\u5834\u5408\u306E\u307F\u8A2D\u5B9A)",
|
|
737
|
+
options: {
|
|
738
|
+
record_deleted: "\u95A2\u9023\u30EC\u30B3\u30FC\u30C9\u304C\u524A\u9664\u3055\u308C\u307E\u3057\u305F"
|
|
713
739
|
}
|
|
714
740
|
},
|
|
715
741
|
current_step: {
|
|
@@ -890,7 +916,8 @@ var init_ja_JP_objects_generated = __esm({
|
|
|
890
916
|
comment: "\u30B3\u30E1\u30F3\u30C8",
|
|
891
917
|
revise: "\u5DEE\u3057\u623B\u3057",
|
|
892
918
|
resubmit: "\u518D\u63D0\u51FA",
|
|
893
|
-
ooo_substitute: "\u4E0D\u5728\u6642\u4EE3\u7406"
|
|
919
|
+
ooo_substitute: "\u4E0D\u5728\u6642\u4EE3\u7406",
|
|
920
|
+
cancel: "\u7121\u52B9\u5316"
|
|
894
921
|
}
|
|
895
922
|
},
|
|
896
923
|
actor_id: {
|
|
@@ -1004,7 +1031,7 @@ var init_es_ES_objects_generated = __esm({
|
|
|
1004
1031
|
},
|
|
1005
1032
|
organization_id: {
|
|
1006
1033
|
label: "Organizaci\xF3n",
|
|
1007
|
-
help: "
|
|
1034
|
+
help: "Organizaci\xF3n del registro al que se refiere esta solicitud (recurre al contexto del actor cuando el registro no tiene ninguna)."
|
|
1008
1035
|
},
|
|
1009
1036
|
process_name: {
|
|
1010
1037
|
label: "Origen",
|
|
@@ -1030,7 +1057,15 @@ var init_es_ES_objects_generated = __esm({
|
|
|
1030
1057
|
approved: "Aprobada",
|
|
1031
1058
|
rejected: "Rechazada",
|
|
1032
1059
|
recalled: "Retirada",
|
|
1033
|
-
returned: "Devuelta para revisi\xF3n"
|
|
1060
|
+
returned: "Devuelta para revisi\xF3n",
|
|
1061
|
+
cancelled: "Anulada"
|
|
1062
|
+
}
|
|
1063
|
+
},
|
|
1064
|
+
cancel_reason: {
|
|
1065
|
+
label: "Motivo de anulaci\xF3n",
|
|
1066
|
+
help: "Por qu\xE9 la plataforma anul\xF3 esta solicitud (solo se establece cuando el estado es Anulada)",
|
|
1067
|
+
options: {
|
|
1068
|
+
record_deleted: "Registro relacionado eliminado"
|
|
1034
1069
|
}
|
|
1035
1070
|
},
|
|
1036
1071
|
current_step: {
|
|
@@ -1211,7 +1246,8 @@ var init_es_ES_objects_generated = __esm({
|
|
|
1211
1246
|
comment: "Comentario",
|
|
1212
1247
|
revise: "Devoluci\xF3n",
|
|
1213
1248
|
resubmit: "Reenv\xEDo",
|
|
1214
|
-
ooo_substitute: "Sustituci\xF3n por ausencia"
|
|
1249
|
+
ooo_substitute: "Sustituci\xF3n por ausencia",
|
|
1250
|
+
cancel: "Anulaci\xF3n"
|
|
1215
1251
|
}
|
|
1216
1252
|
},
|
|
1217
1253
|
actor_id: {
|
|
@@ -1309,24 +1345,120 @@ var init_es_ES_objects_generated = __esm({
|
|
|
1309
1345
|
}
|
|
1310
1346
|
});
|
|
1311
1347
|
|
|
1348
|
+
// src/translations/zh-CN.source-hashes.generated.ts
|
|
1349
|
+
var zhCNGeneratedSourceHashes;
|
|
1350
|
+
var init_zh_CN_source_hashes_generated = __esm({
|
|
1351
|
+
"src/translations/zh-CN.source-hashes.generated.ts"() {
|
|
1352
|
+
"use strict";
|
|
1353
|
+
zhCNGeneratedSourceHashes = {
|
|
1354
|
+
"objects.sys_approval_request.fields.flow_node_id.help": "154aa23b4eee4cae",
|
|
1355
|
+
"objects.sys_approval_request.fields.flow_node_id.label": "052ad568aa41227c",
|
|
1356
|
+
"objects.sys_approval_request.fields.flow_run_id.help": "35c92818f5e11090",
|
|
1357
|
+
"objects.sys_approval_request.fields.flow_run_id.label": "d0124e323da966f9",
|
|
1358
|
+
"objects.sys_approval_request.fields.node_config_json.help": "7ee00ef9b3ca7e80",
|
|
1359
|
+
"objects.sys_approval_request.fields.node_config_json.label": "e64eb4bf7d7ea033"
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
});
|
|
1363
|
+
|
|
1364
|
+
// src/translations/ja-JP.source-hashes.generated.ts
|
|
1365
|
+
var jaJPGeneratedSourceHashes;
|
|
1366
|
+
var init_ja_JP_source_hashes_generated = __esm({
|
|
1367
|
+
"src/translations/ja-JP.source-hashes.generated.ts"() {
|
|
1368
|
+
"use strict";
|
|
1369
|
+
jaJPGeneratedSourceHashes = {
|
|
1370
|
+
"objects.sys_approval_delegation._views.active.emptyState.message": "1095d61c017b6db2",
|
|
1371
|
+
"objects.sys_approval_delegation._views.active.emptyState.title": "799d28e51d88963e",
|
|
1372
|
+
"objects.sys_approval_delegation._views.active.label": "eee107cf4e466779",
|
|
1373
|
+
"objects.sys_approval_delegation.description": "432b0435aa15f6b9",
|
|
1374
|
+
"objects.sys_approval_delegation.fields.created_at.label": "1f02d416befb595b",
|
|
1375
|
+
"objects.sys_approval_delegation.fields.delegate_id.help": "9cfc641f7b0b04f9",
|
|
1376
|
+
"objects.sys_approval_delegation.fields.delegate_id.label": "afd6d8733dc5bc14",
|
|
1377
|
+
"objects.sys_approval_delegation.fields.delegator_id.help": "c4686c5c9f24e0be",
|
|
1378
|
+
"objects.sys_approval_delegation.fields.delegator_id.label": "f76b1f95f2fdabff",
|
|
1379
|
+
"objects.sys_approval_delegation.fields.id.label": "3383564051b4b76d",
|
|
1380
|
+
"objects.sys_approval_delegation.fields.organization_id.help": "f02982e88229d9ca",
|
|
1381
|
+
"objects.sys_approval_delegation.fields.organization_id.label": "3e55836156e1c1de",
|
|
1382
|
+
"objects.sys_approval_delegation.fields.reason.help": "712516f1461dff2b",
|
|
1383
|
+
"objects.sys_approval_delegation.fields.reason.label": "ac399f03d7f2e64d",
|
|
1384
|
+
"objects.sys_approval_delegation.fields.updated_at.label": "aba63dc2a9c79b8d",
|
|
1385
|
+
"objects.sys_approval_delegation.fields.valid_from.help": "01386010c454bfbd",
|
|
1386
|
+
"objects.sys_approval_delegation.fields.valid_from.label": "02eaf201e57c9eda",
|
|
1387
|
+
"objects.sys_approval_delegation.fields.valid_until.help": "a48ea34e65933e9d",
|
|
1388
|
+
"objects.sys_approval_delegation.fields.valid_until.label": "a68db5bd4fc72a13",
|
|
1389
|
+
"objects.sys_approval_delegation.label": "4389c4fe5f5c9746",
|
|
1390
|
+
"objects.sys_approval_delegation.pluralLabel": "f07fec3ffd74e6db",
|
|
1391
|
+
"objects.sys_approval_request.fields.flow_node_id.help": "154aa23b4eee4cae",
|
|
1392
|
+
"objects.sys_approval_request.fields.flow_node_id.label": "052ad568aa41227c",
|
|
1393
|
+
"objects.sys_approval_request.fields.flow_run_id.help": "35c92818f5e11090",
|
|
1394
|
+
"objects.sys_approval_request.fields.flow_run_id.label": "d0124e323da966f9",
|
|
1395
|
+
"objects.sys_approval_request.fields.node_config_json.help": "7ee00ef9b3ca7e80",
|
|
1396
|
+
"objects.sys_approval_request.fields.node_config_json.label": "e64eb4bf7d7ea033"
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
});
|
|
1400
|
+
|
|
1401
|
+
// src/translations/es-ES.source-hashes.generated.ts
|
|
1402
|
+
var esESGeneratedSourceHashes;
|
|
1403
|
+
var init_es_ES_source_hashes_generated = __esm({
|
|
1404
|
+
"src/translations/es-ES.source-hashes.generated.ts"() {
|
|
1405
|
+
"use strict";
|
|
1406
|
+
esESGeneratedSourceHashes = {
|
|
1407
|
+
"objects.sys_approval_action.fields.actor_id.label": "b155813f8a7f06e3",
|
|
1408
|
+
"objects.sys_approval_delegation._views.active.emptyState.message": "1095d61c017b6db2",
|
|
1409
|
+
"objects.sys_approval_delegation._views.active.emptyState.title": "799d28e51d88963e",
|
|
1410
|
+
"objects.sys_approval_delegation._views.active.label": "eee107cf4e466779",
|
|
1411
|
+
"objects.sys_approval_delegation.description": "432b0435aa15f6b9",
|
|
1412
|
+
"objects.sys_approval_delegation.fields.created_at.label": "1f02d416befb595b",
|
|
1413
|
+
"objects.sys_approval_delegation.fields.delegate_id.help": "9cfc641f7b0b04f9",
|
|
1414
|
+
"objects.sys_approval_delegation.fields.delegate_id.label": "afd6d8733dc5bc14",
|
|
1415
|
+
"objects.sys_approval_delegation.fields.delegator_id.help": "c4686c5c9f24e0be",
|
|
1416
|
+
"objects.sys_approval_delegation.fields.delegator_id.label": "f76b1f95f2fdabff",
|
|
1417
|
+
"objects.sys_approval_delegation.fields.id.label": "3383564051b4b76d",
|
|
1418
|
+
"objects.sys_approval_delegation.fields.organization_id.help": "f02982e88229d9ca",
|
|
1419
|
+
"objects.sys_approval_delegation.fields.organization_id.label": "3e55836156e1c1de",
|
|
1420
|
+
"objects.sys_approval_delegation.fields.reason.help": "712516f1461dff2b",
|
|
1421
|
+
"objects.sys_approval_delegation.fields.reason.label": "ac399f03d7f2e64d",
|
|
1422
|
+
"objects.sys_approval_delegation.fields.updated_at.label": "aba63dc2a9c79b8d",
|
|
1423
|
+
"objects.sys_approval_delegation.fields.valid_from.help": "01386010c454bfbd",
|
|
1424
|
+
"objects.sys_approval_delegation.fields.valid_from.label": "02eaf201e57c9eda",
|
|
1425
|
+
"objects.sys_approval_delegation.fields.valid_until.help": "a48ea34e65933e9d",
|
|
1426
|
+
"objects.sys_approval_delegation.fields.valid_until.label": "a68db5bd4fc72a13",
|
|
1427
|
+
"objects.sys_approval_delegation.label": "4389c4fe5f5c9746",
|
|
1428
|
+
"objects.sys_approval_delegation.pluralLabel": "f07fec3ffd74e6db",
|
|
1429
|
+
"objects.sys_approval_request.fields.flow_node_id.help": "154aa23b4eee4cae",
|
|
1430
|
+
"objects.sys_approval_request.fields.flow_node_id.label": "052ad568aa41227c",
|
|
1431
|
+
"objects.sys_approval_request.fields.flow_run_id.help": "35c92818f5e11090",
|
|
1432
|
+
"objects.sys_approval_request.fields.flow_run_id.label": "d0124e323da966f9",
|
|
1433
|
+
"objects.sys_approval_request.fields.node_config_json.help": "7ee00ef9b3ca7e80",
|
|
1434
|
+
"objects.sys_approval_request.fields.node_config_json.label": "e64eb4bf7d7ea033"
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
});
|
|
1438
|
+
|
|
1312
1439
|
// src/translations/index.ts
|
|
1313
1440
|
var translations_exports = {};
|
|
1314
1441
|
__export(translations_exports, {
|
|
1315
1442
|
ApprovalsTranslations: () => ApprovalsTranslations
|
|
1316
1443
|
});
|
|
1317
|
-
var ApprovalsTranslations;
|
|
1444
|
+
var import_apps, enSource, ApprovalsTranslations;
|
|
1318
1445
|
var init_translations = __esm({
|
|
1319
1446
|
"src/translations/index.ts"() {
|
|
1320
1447
|
"use strict";
|
|
1448
|
+
import_apps = require("@objectstack/platform-objects/apps");
|
|
1321
1449
|
init_en_objects_generated();
|
|
1322
1450
|
init_zh_CN_objects_generated();
|
|
1323
1451
|
init_ja_JP_objects_generated();
|
|
1324
1452
|
init_es_ES_objects_generated();
|
|
1453
|
+
init_zh_CN_source_hashes_generated();
|
|
1454
|
+
init_ja_JP_source_hashes_generated();
|
|
1455
|
+
init_es_ES_source_hashes_generated();
|
|
1456
|
+
enSource = { objects: enObjects };
|
|
1325
1457
|
ApprovalsTranslations = {
|
|
1326
|
-
en:
|
|
1327
|
-
"zh-CN": { objects: zhCNObjects },
|
|
1328
|
-
"ja-JP": { objects: jaJPObjects },
|
|
1329
|
-
"es-ES": { objects: esESObjects }
|
|
1458
|
+
en: enSource,
|
|
1459
|
+
"zh-CN": (0, import_apps.withSourceFallback)({ objects: zhCNObjects }, enSource, void 0, zhCNGeneratedSourceHashes),
|
|
1460
|
+
"ja-JP": (0, import_apps.withSourceFallback)({ objects: jaJPObjects }, enSource, void 0, jaJPGeneratedSourceHashes),
|
|
1461
|
+
"es-ES": (0, import_apps.withSourceFallback)({ objects: esESObjects }, enSource, void 0, esESGeneratedSourceHashes)
|
|
1330
1462
|
};
|
|
1331
1463
|
}
|
|
1332
1464
|
});
|
|
@@ -1395,7 +1527,10 @@ var SysApprovalRequest = import_data.ObjectSchema.create({
|
|
|
1395
1527
|
label: "Completed",
|
|
1396
1528
|
data: { provider: "object", object: "sys_approval_request" },
|
|
1397
1529
|
columns: ["process_name", "object_name", "record_id", "status", "submitter_id", "completed_at"],
|
|
1398
|
-
|
|
1530
|
+
// Every terminal state, `cancelled` included (#13568) — a platform-voided
|
|
1531
|
+
// request is kept as audit evidence, and a terminal state absent from the
|
|
1532
|
+
// only curated terminal view is evidence nobody can find.
|
|
1533
|
+
filter: [{ field: "status", operator: "in", value: ["approved", "rejected", "recalled", "cancelled"] }],
|
|
1399
1534
|
sort: [{ field: "completed_at", order: "desc" }],
|
|
1400
1535
|
pagination: { pageSize: 25 }
|
|
1401
1536
|
},
|
|
@@ -1411,46 +1546,40 @@ var SysApprovalRequest = import_data.ObjectSchema.create({
|
|
|
1411
1546
|
},
|
|
1412
1547
|
fields: {
|
|
1413
1548
|
id: import_data.Field.text({ label: "Request ID", required: true, readonly: true, group: "System" }),
|
|
1414
|
-
//
|
|
1549
|
+
// [#10101, the cloud#1395 Option A ruling] The SUBJECT record's
|
|
1550
|
+
// organization, with the acting context as fallback — resolved by the
|
|
1551
|
+
// SHARED platform-row resolver (`resolveRecordOrganizationField`,
|
|
1552
|
+
// `@objectstack/metadata-core`) in `openNodeRequest`, the row's only
|
|
1553
|
+
// writer. The same `requestOrg` stamps `sys_approval_action` and the
|
|
1554
|
+
// `sys_approval_approver` index, so all three move together.
|
|
1415
1555
|
//
|
|
1416
|
-
//
|
|
1417
|
-
//
|
|
1418
|
-
//
|
|
1419
|
-
//
|
|
1420
|
-
//
|
|
1556
|
+
// Why subject-first: an approval request is read through the organization
|
|
1557
|
+
// wall by the approvals inbox, and the acting context is NULL on every
|
|
1558
|
+
// schedule / time-relative / api triggered run (none carries a tenant, by
|
|
1559
|
+
// construction). Stamped from the actor alone — the pre-#10101 behaviour,
|
|
1560
|
+
// measured on cloud#1395 as 27 of 27 rows org-less on a walled HotCRM SaaS
|
|
1561
|
+
// boot — such a request LOCKED the record it was about while being
|
|
1562
|
+
// invisible in every inbox, its owner's included. Subject-first is also
|
|
1563
|
+
// what `sys_audit_log`'s writer already did (#8707 honouring #8287's
|
|
1564
|
+
// ruling), so an approval row and an audit row about the same record now
|
|
1565
|
+
// land behind the same wall instead of two.
|
|
1421
1566
|
//
|
|
1422
|
-
// The
|
|
1423
|
-
// `
|
|
1424
|
-
//
|
|
1425
|
-
//
|
|
1426
|
-
//
|
|
1427
|
-
// `record_id` owned by a specific customer.
|
|
1428
|
-
//
|
|
1429
|
-
// ⛔ Do NOT read that as "platform tables do not carry an organization".
|
|
1430
|
-
// `sys_audit_log` (1669 rows) was correctly attributed on the SAME boot,
|
|
1431
|
-
// because its writer takes the organization from the RECORD the row is
|
|
1432
|
-
// about, with the session only as fallback (plugin-audit
|
|
1433
|
-
// `resolveRecordOrganizationField`, #8707 honouring #8287's ruling). Two
|
|
1434
|
-
// writers read the actor; a third reads the subject. That disagreement is
|
|
1435
|
-
// the defect.
|
|
1436
|
-
//
|
|
1437
|
-
// Which of the two a side-table row should follow is an open contract
|
|
1438
|
-
// question on cloud#1395 — the audit resolver is scope-pinned to audit
|
|
1439
|
-
// stamping by the #8778 ruling, so this writer needs its own. The same
|
|
1440
|
-
// `ctxOrg` also stamps `sys_approval_action` and `sys_approval_approver`,
|
|
1441
|
-
// so all three move together.
|
|
1567
|
+
// The `sys_api_key` divergence is deliberate and preserved: its
|
|
1568
|
+
// `tenancy.organizationField: 'active_organization_id'` (stamp-only,
|
|
1569
|
+
// #8778) wins limb 0 of the shared resolver, while the credential table
|
|
1570
|
+
// itself stays unwalled (`tenancy.enabled: false`) — who a row is ABOUT
|
|
1571
|
+
// and what an object is WALLED by remain different questions.
|
|
1442
1572
|
organization_id: import_data.Field.lookup("sys_organization", {
|
|
1443
1573
|
label: "Organization",
|
|
1444
1574
|
required: false,
|
|
1445
1575
|
group: "System",
|
|
1446
|
-
//
|
|
1447
|
-
//
|
|
1448
|
-
//
|
|
1449
|
-
//
|
|
1450
|
-
//
|
|
1451
|
-
//
|
|
1452
|
-
|
|
1453
|
-
description: "Tenant that owns this approval request (propagated from submitter context)"
|
|
1576
|
+
// Reworded with the #10101 write-side fix (was "Tenant that owns this
|
|
1577
|
+
// approval request (propagated from submitter context)" — it claimed a
|
|
1578
|
+
// propagation that measurably did not happen, and said "Tenant" where
|
|
1579
|
+
// ADR-0120 §Terminology requires "organization"). Extracted into the
|
|
1580
|
+
// generated i18n bundles as `help`; the four locales regenerate in the
|
|
1581
|
+
// same pass.
|
|
1582
|
+
description: "Organization of the record this request is about (falls back to the acting context when the record has none)"
|
|
1454
1583
|
}),
|
|
1455
1584
|
process_name: import_data.Field.text({
|
|
1456
1585
|
label: "Source",
|
|
@@ -1469,7 +1598,38 @@ var SysApprovalRequest = import_data.ObjectSchema.create({
|
|
|
1469
1598
|
label: "Record ID",
|
|
1470
1599
|
required: true,
|
|
1471
1600
|
maxLength: 100,
|
|
1472
|
-
|
|
1601
|
+
// [#11386] The id half of this object's pointer pair (ADR-0052 §5),
|
|
1602
|
+
// adopting the #11339 carrier. VERIFIED for THIS object: the pair is not
|
|
1603
|
+
// decoration but the key the approval machinery QUERIES ON —
|
|
1604
|
+
// `approval-service.ts` finds a record's pending request with
|
|
1605
|
+
// `where: { object_name, record_id, status: 'pending' }`, and
|
|
1606
|
+
// `lifecycle-hooks.ts` holds the record LOCK on the same pair
|
|
1607
|
+
// (single-record and `$in` batch forms). `submit()` writes it from
|
|
1608
|
+
// `input.object` / `input.recordId`, so a stored value is always a
|
|
1609
|
+
// record id of the object the sibling names.
|
|
1610
|
+
//
|
|
1611
|
+
// Consequence of declaring, sharper here than elsewhere: a seeded
|
|
1612
|
+
// request whose `record_id` stayed a verbatim natural key locked
|
|
1613
|
+
// NOTHING and appeared under no record — it looked like a pending
|
|
1614
|
+
// approval while being invisible to both queries that give the row its
|
|
1615
|
+
// meaning. That is now a loud seed-time refusal. Both halves are
|
|
1616
|
+
// `required: true`, so the un-addressable case (id half authored, type
|
|
1617
|
+
// half empty) is already unreachable on this object.
|
|
1618
|
+
group: "Target",
|
|
1619
|
+
//
|
|
1620
|
+
// ⚠️ ORDERING CONSTRAINT — this id half is `required: true`, and that
|
|
1621
|
+
// makes it ORDER-DEPENDENT in seeds even though a pointer pair
|
|
1622
|
+
// contributes no static ordering edge (#11674, measured against the real
|
|
1623
|
+
// engine in `packages/objectql/src/engine-seed-required-deferral.test.ts`):
|
|
1624
|
+
// the seed loader defers an unresolvable reference by DELETING the column
|
|
1625
|
+
// from the pass-1 insert, required-validation rejects that row, and pass 2
|
|
1626
|
+
// is then left with no row to back-fill. So the pass-2 healing that makes
|
|
1627
|
+
// an OPTIONAL id half order-independent (`sys_audit_log`) does not reach
|
|
1628
|
+
// this one. ⇒ SEED THE TARGET DATASET FIRST. The failure if you do not is
|
|
1629
|
+
// loud in three places — a write error naming this column, a
|
|
1630
|
+
// dropped-deferral error, and `success: false` — and since #11674 the
|
|
1631
|
+
// loader also WARNS at load time, before the engine rejects the row.
|
|
1632
|
+
referenceVia: "object_name"
|
|
1473
1633
|
}),
|
|
1474
1634
|
submitter_id: import_data.Field.lookup("sys_user", {
|
|
1475
1635
|
label: "Submitter",
|
|
@@ -1497,6 +1657,29 @@ var SysApprovalRequest = import_data.ObjectSchema.create({
|
|
|
1497
1657
|
group: "State"
|
|
1498
1658
|
}
|
|
1499
1659
|
),
|
|
1660
|
+
// [#13568] Why a `cancelled` request was cancelled — the machine-readable
|
|
1661
|
+
// half of the maintainer's 2026-08-31 ruling ("状态转 cancelled + 机器可读
|
|
1662
|
+
// 原因"). Derived from `APPROVAL_CANCEL_REASONS`, never re-typed, on the
|
|
1663
|
+
// same #3786 rule the `status` column above follows.
|
|
1664
|
+
//
|
|
1665
|
+
// On the ROW rather than on the `sys_approval_action` audit entry, because
|
|
1666
|
+
// the readers are the inbox and the tombstone presentation, which page the
|
|
1667
|
+
// request table: making them join the append-only action log to learn why a
|
|
1668
|
+
// row they already hold is cancelled buys nothing and would make the reason
|
|
1669
|
+
// unavailable to a plain list view. The action row still records the event
|
|
1670
|
+
// (`action: 'cancel'`); this column records the STATE.
|
|
1671
|
+
//
|
|
1672
|
+
// Null on every non-cancelled row, and on rows written before the column
|
|
1673
|
+
// existed — "not recorded", never "cancelled for no reason".
|
|
1674
|
+
cancel_reason: import_data.Field.select(
|
|
1675
|
+
import_contracts.APPROVAL_CANCEL_REASONS.map((value) => ({ value, label: import_contracts.APPROVAL_CANCEL_REASON_LABELS[value] })),
|
|
1676
|
+
{
|
|
1677
|
+
label: "Cancellation Reason",
|
|
1678
|
+
required: false,
|
|
1679
|
+
description: "Why the platform voided this request (set only when the status is cancelled)",
|
|
1680
|
+
group: "State"
|
|
1681
|
+
}
|
|
1682
|
+
),
|
|
1500
1683
|
current_step: import_data.Field.text({
|
|
1501
1684
|
label: "Current Step",
|
|
1502
1685
|
required: false,
|
|
@@ -1516,6 +1699,22 @@ var SysApprovalRequest = import_data.ObjectSchema.create({
|
|
|
1516
1699
|
description: "Comma-separated user ids who can act on the current step",
|
|
1517
1700
|
group: "State"
|
|
1518
1701
|
}),
|
|
1702
|
+
// The module docstring above used to justify this column with "used by
|
|
1703
|
+
// notifications so they can render before the record is locked or
|
|
1704
|
+
// changed". That consumer does not exist, and the claim was cited as the
|
|
1705
|
+
// reason the column holds a FULL row before anyone checked it. Measured
|
|
1706
|
+
// against every `this.notify(...)` call site in `approval-service.ts` —
|
|
1707
|
+
// all 12 of them: each passes `{ title, message, actionUrl }` (two also
|
|
1708
|
+
// `actions`), built from `object_name` / `record_id` and the caller's
|
|
1709
|
+
// comment. None reads this column or the parsed `payload`. The real
|
|
1710
|
+
// readers are the serve path (`rowFromRequest` -> `payload`, redacted per
|
|
1711
|
+
// reader), the decide-time approver re-resolution and the org backfill,
|
|
1712
|
+
// both under SYSTEM_CTX, and the free-text predicate.
|
|
1713
|
+
//
|
|
1714
|
+
// The `description` below is deliberately UNCHANGED: it is accurate, and
|
|
1715
|
+
// it is extracted into the four generated i18n bundles as `help`. The
|
|
1716
|
+
// false sentence lived only in the JSDoc above, which is not extracted —
|
|
1717
|
+
// so this correction moves no translation leaf.
|
|
1519
1718
|
payload_json: import_data.Field.textarea({
|
|
1520
1719
|
label: "Snapshot",
|
|
1521
1720
|
required: false,
|
|
@@ -1586,11 +1785,14 @@ var SysApprovalRequest = import_data.ObjectSchema.create({
|
|
|
1586
1785
|
// per-viewer block (#3310): approver actions on `record.viewer.can_act`
|
|
1587
1786
|
// (the caller is a current pending approver — same check the service
|
|
1588
1787
|
// authorizes a decision with, so position/team approvers resolve correctly),
|
|
1589
|
-
// submitter actions on `record.viewer.is_submitter`. The
|
|
1590
|
-
// (approve/reject/reassign
|
|
1591
|
-
//
|
|
1788
|
+
// submitter actions on `record.viewer.is_submitter`. The four levers the
|
|
1789
|
+
// #3424 override covers (approve/reject/reassign, and recall since #12716)
|
|
1790
|
+
// additionally OR in `record.viewer.can_override`
|
|
1791
|
+
// so a platform/tenant admin can rescue a request routed to an
|
|
1592
1792
|
// unstaffed position — otherwise undecidable, locking the record forever — by
|
|
1593
|
-
// approving, rejecting,
|
|
1793
|
+
// approving, rejecting, reassigning it to a real approver, or recalling it
|
|
1794
|
+
// (the lever that releases the record without recording a decision nobody
|
|
1795
|
+
// made). `viewer` is
|
|
1594
1796
|
// attached by getRequest/listRequests; where it is absent the predicate fails
|
|
1595
1797
|
// closed.
|
|
1596
1798
|
//
|
|
@@ -1726,8 +1928,13 @@ var SysApprovalRequest = import_data.ObjectSchema.create({
|
|
|
1726
1928
|
// Remind / recall (pending) and resubmit / recall (returned). These are the
|
|
1727
1929
|
// submitter's own levers, so `visible` gates on `record.viewer.is_submitter`
|
|
1728
1930
|
// (server-computed on the current viewer). The service re-checks ownership;
|
|
1729
|
-
// the predicate keeps a non-submitter from ever seeing a button they
|
|
1730
|
-
// use.
|
|
1931
|
+
// the predicate keeps a plain non-submitter from ever seeing a button they
|
|
1932
|
+
// cannot use.
|
|
1933
|
+
//
|
|
1934
|
+
// `recall` is the one exception, and it is not a widening: it ALSO ORs in
|
|
1935
|
+
// the #3424 admin override (#12716), because an override admin is a caller
|
|
1936
|
+
// `ApprovalService.recall` already authorises. Remind and resubmit keep no
|
|
1937
|
+
// override arm.
|
|
1731
1938
|
{
|
|
1732
1939
|
name: "approval_remind",
|
|
1733
1940
|
label: "Send reminder",
|
|
@@ -1758,7 +1965,26 @@ var SysApprovalRequest = import_data.ObjectSchema.create({
|
|
|
1758
1965
|
],
|
|
1759
1966
|
// Recall applies while the request is live for the submitter — pending
|
|
1760
1967
|
// (withdraw) or returned (abandon the revision instead of resubmitting).
|
|
1761
|
-
|
|
1968
|
+
//
|
|
1969
|
+
// The second arm is the #3424 admin override, spelled byte-identically to
|
|
1970
|
+
// the three core decision levers above (#12716). `ApprovalService.recall`
|
|
1971
|
+
// has admitted the override caller since #3424 — `isOverrideActor`'s own
|
|
1972
|
+
// doc block names recall as one of the four levers — so until this arm
|
|
1973
|
+
// landed, recall was the one authorised capability with no button: an
|
|
1974
|
+
// admin could approve or reject their way out of a stuck request (writing
|
|
1975
|
+
// a decision that did not happen) or reassign it, but could not withdraw.
|
|
1976
|
+
//
|
|
1977
|
+
// The override arm carries no status test of its own, on purpose, because
|
|
1978
|
+
// it does not need one and the siblings do not have one either: the flag
|
|
1979
|
+
// is already status-scoped where it is COMPUTED. `attachViewers` in
|
|
1980
|
+
// `approval-service.ts` sets
|
|
1981
|
+
// `can_override: row.status === 'pending' && isOverrideActor(...)` —
|
|
1982
|
+
// ANDed — so `record.viewer.can_override` can never be true off `pending`,
|
|
1983
|
+
// and this arm is pending-only in effect however CEL groups the
|
|
1984
|
+
// expression. Pinned in both directions in
|
|
1985
|
+
// `action-predicate-sparse-face.test.ts`, with the flag's own scoping
|
|
1986
|
+
// pinned against the real service in `approval-revise.test.ts`.
|
|
1987
|
+
visible: 'has(record.status) && (record.status == "pending" || record.status == "returned") && has(record.viewer) && has(record.viewer.is_submitter) && record.viewer.is_submitter == true || has(record.viewer) && has(record.viewer.can_override) && record.viewer.can_override == true',
|
|
1762
1988
|
locations: ["record_section"],
|
|
1763
1989
|
successMessage: "Recalled.",
|
|
1764
1990
|
refreshAfter: true
|
|
@@ -2113,10 +2339,12 @@ var SysApprovalDelegation = import_data4.ObjectSchema.create({
|
|
|
2113
2339
|
var import_node_crypto = require("crypto");
|
|
2114
2340
|
var import_automation = require("@objectstack/spec/automation");
|
|
2115
2341
|
var import_formula = require("@objectstack/formula");
|
|
2342
|
+
var import_metadata_core = require("@objectstack/metadata-core");
|
|
2116
2343
|
var import_types = require("@objectstack/types");
|
|
2117
2344
|
var import_identity = require("@objectstack/spec/identity");
|
|
2118
2345
|
var import_contracts3 = require("@objectstack/spec/contracts");
|
|
2119
2346
|
var import_data5 = require("@objectstack/spec/data");
|
|
2347
|
+
var import_system = require("@objectstack/spec/system");
|
|
2120
2348
|
var import_core = require("@objectstack/core");
|
|
2121
2349
|
|
|
2122
2350
|
// src/approver-org-scope.ts
|
|
@@ -2272,6 +2500,7 @@ var REMIND_COOLDOWN_MS = 4 * 60 * 60 * 1e3;
|
|
|
2272
2500
|
var ESCALATION_JOB_NAME = "approvals-sla-escalation";
|
|
2273
2501
|
var ESCALATION_SCAN_INTERVAL_MS = 5 * 60 * 1e3;
|
|
2274
2502
|
var SLA_ACTOR_ID = "system:sla";
|
|
2503
|
+
var ESCALATION_ENABLED_FLIP_CUTOFF_MS = Date.parse("2026-08-28T00:00:00Z");
|
|
2275
2504
|
var DEAD_RUN_ACTOR_ID = "system:dead-run";
|
|
2276
2505
|
var TERMINAL_RUN_STATUSES = /* @__PURE__ */ new Set([
|
|
2277
2506
|
"completed",
|
|
@@ -2280,8 +2509,37 @@ var TERMINAL_RUN_STATUSES = /* @__PURE__ */ new Set([
|
|
|
2280
2509
|
"timed_out"
|
|
2281
2510
|
]);
|
|
2282
2511
|
var STRANDABLE_REQUEST_STATUSES = ["approved", "rejected", "returned"];
|
|
2512
|
+
function classifyStrandedRunState(run) {
|
|
2513
|
+
if (!run) return "missing";
|
|
2514
|
+
switch (run.status) {
|
|
2515
|
+
// The resume consumed the pause and a downstream node threw. Reported.
|
|
2516
|
+
case "failed":
|
|
2517
|
+
return "failed";
|
|
2518
|
+
// ── The negatives, each for its own reason ──────────────────────────────
|
|
2519
|
+
// The decision advanced the flow and the flow finished. Healthy.
|
|
2520
|
+
case "completed":
|
|
2521
|
+
return void 0;
|
|
2522
|
+
// Deliberately terminated by an operator (`cancelRun`, ADR-0044). The run
|
|
2523
|
+
// stopping is the intended outcome, exactly as `recalled` is on the request
|
|
2524
|
+
// side — reporting it would bury the real findings under expected ones.
|
|
2525
|
+
case "cancelled":
|
|
2526
|
+
return void 0;
|
|
2527
|
+
// The history's last row says `paused` while the suspension store says no
|
|
2528
|
+
// live pause. That is AMBIGUOUS, and the ambiguity is not resolvable from
|
|
2529
|
+
// one scan: a resume in flight right now has consumed the suspension and
|
|
2530
|
+
// not yet written its terminal row, and reads exactly like a process that
|
|
2531
|
+
// died in the same window. Condemning it would name every concurrently
|
|
2532
|
+
// resuming approval — so this stays SKIPPED, the conservative arm this
|
|
2533
|
+
// whole method is built on.
|
|
2534
|
+
case "paused":
|
|
2535
|
+
return void 0;
|
|
2536
|
+
default:
|
|
2537
|
+
return void 0;
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2283
2540
|
var ACTION_TOKEN_TTL_MS = 72 * 60 * 60 * 1e3;
|
|
2284
2541
|
var SYSTEM_CTX2 = { isSystem: true, positions: [], permissions: [] };
|
|
2542
|
+
var RECORD_DELETE_CANCEL_LIMIT = 200;
|
|
2285
2543
|
function actingUserId(context) {
|
|
2286
2544
|
const userId = context?.userId;
|
|
2287
2545
|
return typeof userId === "string" && userId ? userId : null;
|
|
@@ -2342,6 +2600,10 @@ function rowFromRequest(row) {
|
|
|
2342
2600
|
submitter_id: row.submitter_id ?? void 0,
|
|
2343
2601
|
submitter_comment: row.submitter_comment ?? void 0,
|
|
2344
2602
|
status: row.status ?? "pending",
|
|
2603
|
+
// [#13568] Why a cancelled row was cancelled. Surfaced on every read for
|
|
2604
|
+
// the same reason `status` is: a client that can see the terminal state and
|
|
2605
|
+
// not the machine-readable cause is back to guessing from prose.
|
|
2606
|
+
cancel_reason: row.cancel_reason ?? void 0,
|
|
2345
2607
|
current_step: row.current_step ?? void 0,
|
|
2346
2608
|
current_step_index: row.current_step_index ?? void 0,
|
|
2347
2609
|
pending_approvers: csvSplit(row.pending_approvers),
|
|
@@ -2434,6 +2696,7 @@ function rowFromAction(row) {
|
|
|
2434
2696
|
var _ApprovalService = class _ApprovalService {
|
|
2435
2697
|
constructor(opts) {
|
|
2436
2698
|
this.engine = opts.engine;
|
|
2699
|
+
this.recordOrgResolver = (0, import_metadata_core.createRecordOrganizationResolver)(opts.engine);
|
|
2437
2700
|
this.clock = opts.clock ?? { now: () => /* @__PURE__ */ new Date() };
|
|
2438
2701
|
this.logger = opts.logger;
|
|
2439
2702
|
this.automation = opts.automation;
|
|
@@ -2441,6 +2704,7 @@ var _ApprovalService = class _ApprovalService {
|
|
|
2441
2704
|
this.publicBaseUrl = (opts.publicBaseUrl ?? "").replace(/\/$/, "");
|
|
2442
2705
|
this.tenancyPosture = opts.tenancyPosture;
|
|
2443
2706
|
this.fieldVisibility = opts.fieldVisibility;
|
|
2707
|
+
this.messageTranslator = opts.messageTranslator;
|
|
2444
2708
|
this.recordReaderVisibleObjects = new Set(
|
|
2445
2709
|
(Array.isArray(opts.recordReaderVisibleObjects) ? opts.recordReaderVisibleObjects : []).map((n) => String(n ?? "").trim()).filter(Boolean)
|
|
2446
2710
|
);
|
|
@@ -2593,6 +2857,41 @@ var _ApprovalService = class _ApprovalService {
|
|
|
2593
2857
|
const actorTenant = context.tenantId ?? context.organizationId ?? null;
|
|
2594
2858
|
return requestOrg == null || actorTenant != null && String(requestOrg) === String(actorTenant);
|
|
2595
2859
|
}
|
|
2860
|
+
/**
|
|
2861
|
+
* [#11993] The END USER's half of an approval refusal.
|
|
2862
|
+
*
|
|
2863
|
+
* `handleApprovalError` in `@objectstack/rest` maps this service's
|
|
2864
|
+
* `CODE: message` throws onto the wire by testing the prefix for the status
|
|
2865
|
+
* and then STRIPPING it (a leading run of `[A-Z_]` plus a colon and any
|
|
2866
|
+
* following whitespace), shipping what remains as the body's
|
|
2867
|
+
* human-readable `error` — which Console splices under its own localized
|
|
2868
|
+
* label ("撤回审批失败: …"). A hardcoded English reason therefore reaches an
|
|
2869
|
+
* operator in a fully Chinese deployment as a Chinese prefix glued onto an
|
|
2870
|
+
* English sentence they cannot act on.
|
|
2871
|
+
*
|
|
2872
|
+
* Rendered through the SHARED Operation Message Catalog
|
|
2873
|
+
* (`@objectstack/spec/system`), not a second mechanism: same
|
|
2874
|
+
* `errors.<key>` override address, same resolution ladder (deployment
|
|
2875
|
+
* override -> locale catalog -> `en` -> the key), same guarantee that a
|
|
2876
|
+
* misbehaving i18n service cannot turn a 403 into a 500. The catalog's
|
|
2877
|
+
* header names this seat explicitly; `plugin-security`'s
|
|
2878
|
+
* `userFacingDenialMessage` is the sibling consumer this mirrors.
|
|
2879
|
+
*
|
|
2880
|
+
* The CODE PREFIX stays on the message. It is not user copy — it is how the
|
|
2881
|
+
* REST layer derives the status and the ADR-0112 wire code, and it is
|
|
2882
|
+
* stripped before the sentence reaches a body. The developer's half moves
|
|
2883
|
+
* to the log, where the ids it names are legible to an operator and to
|
|
2884
|
+
* nobody else.
|
|
2885
|
+
*/
|
|
2886
|
+
userFacingRefusal(messageKey, context) {
|
|
2887
|
+
let translate;
|
|
2888
|
+
try {
|
|
2889
|
+
translate = this.messageTranslator?.();
|
|
2890
|
+
} catch {
|
|
2891
|
+
translate = void 0;
|
|
2892
|
+
}
|
|
2893
|
+
return (0, import_system.renderOperationMessage)({ messageKey }, { locale: context?.locale, translate });
|
|
2894
|
+
}
|
|
2596
2895
|
/**
|
|
2597
2896
|
* Pin the acting identity to the AUTHENTICATED CALLER (#3800).
|
|
2598
2897
|
*
|
|
@@ -3533,11 +3832,13 @@ var _ApprovalService = class _ApprovalService {
|
|
|
3533
3832
|
const substitutions = [];
|
|
3534
3833
|
const groups = {};
|
|
3535
3834
|
const liveRecord = await this.loadLiveRecord(input.object, input.recordId, input.record);
|
|
3835
|
+
const subjectOrg = this.recordOrgResolver.organizationOf(input.object, liveRecord, input.record);
|
|
3836
|
+
const requestOrg = subjectOrg ?? ctxOrg;
|
|
3536
3837
|
const resolvedFrom = {};
|
|
3537
3838
|
const approvers = await this.expandApprovers(
|
|
3538
3839
|
{ approvers: input.config.approvers },
|
|
3539
3840
|
liveRecord,
|
|
3540
|
-
|
|
3841
|
+
requestOrg,
|
|
3541
3842
|
{
|
|
3542
3843
|
now: nowDate.getTime(),
|
|
3543
3844
|
substitutions,
|
|
@@ -3601,16 +3902,16 @@ var _ApprovalService = class _ApprovalService {
|
|
|
3601
3902
|
flow_run_id: input.runId,
|
|
3602
3903
|
flow_node_id: input.nodeId,
|
|
3603
3904
|
node_config_json: JSON.stringify(configSnapshot),
|
|
3604
|
-
organization_id:
|
|
3905
|
+
organization_id: requestOrg,
|
|
3605
3906
|
created_at: now,
|
|
3606
3907
|
updated_at: now
|
|
3607
3908
|
};
|
|
3608
3909
|
await this.engine.insert("sys_approval_request", row, { context: SYSTEM_CTX2 });
|
|
3609
|
-
await this.syncApproverIndex(id, approvers,
|
|
3910
|
+
await this.syncApproverIndex(id, approvers, requestOrg, now);
|
|
3610
3911
|
await this.engine.insert("sys_approval_action", {
|
|
3611
3912
|
id: uid("aact"),
|
|
3612
3913
|
request_id: id,
|
|
3613
|
-
organization_id:
|
|
3914
|
+
organization_id: requestOrg,
|
|
3614
3915
|
step_name: input.nodeId,
|
|
3615
3916
|
step_index: 0,
|
|
3616
3917
|
action: "submit",
|
|
@@ -3622,7 +3923,7 @@ var _ApprovalService = class _ApprovalService {
|
|
|
3622
3923
|
await this.engine.insert("sys_approval_action", {
|
|
3623
3924
|
id: uid("aact"),
|
|
3624
3925
|
request_id: id,
|
|
3625
|
-
organization_id:
|
|
3926
|
+
organization_id: requestOrg,
|
|
3626
3927
|
step_name: input.nodeId,
|
|
3627
3928
|
step_index: 0,
|
|
3628
3929
|
action: "ooo_substitute",
|
|
@@ -4064,6 +4365,13 @@ var _ApprovalService = class _ApprovalService {
|
|
|
4064
4365
|
* is then paused at the revise-window node (no reject edge), so it is
|
|
4065
4366
|
* terminally cancelled via {@link ApprovalResumeSurface.cancelRun} rather
|
|
4066
4367
|
* than resumed.
|
|
4368
|
+
*
|
|
4369
|
+
* The #3424 privileged override reaches a PENDING request only (#12775,
|
|
4370
|
+
* maintainer ruling 2026-09-02). On `returned` an override actor is refused
|
|
4371
|
+
* exactly as any other non-submitter: the gate is spelled as `attachViewers`
|
|
4372
|
+
* computes `viewer.can_override`, so the gate, the
|
|
4373
|
+
* {@link ApprovalService.isOverrideActor} doc block and the viewer flag agree
|
|
4374
|
+
* at one point.
|
|
4067
4375
|
*/
|
|
4068
4376
|
async recall(requestId, input, context) {
|
|
4069
4377
|
if (!requestId) throw new Error("VALIDATION_FAILED: requestId is required");
|
|
@@ -4079,8 +4387,18 @@ var _ApprovalService = class _ApprovalService {
|
|
|
4079
4387
|
if (raw.status !== "pending" && !inReviseWindow) {
|
|
4080
4388
|
throw new Error(`INVALID_STATE: request is ${raw.status}`);
|
|
4081
4389
|
}
|
|
4082
|
-
|
|
4083
|
-
|
|
4390
|
+
const overrideAdmits = raw.status === "pending" && this.isOverrideActor(context, raw.organization_id ?? null);
|
|
4391
|
+
if (!overrideAdmits && raw.submitter_id && String(raw.submitter_id) !== String(actorId)) {
|
|
4392
|
+
const developerMessage = `[approvals] recall refused: actor '${actorId}' is not the submitter of request '${requestId}' (submitter '${String(raw.submitter_id)}') and holds no #3424 override for a '${raw.status}' request (the override reaches pending requests only)`;
|
|
4393
|
+
this.logger?.warn?.(developerMessage, {
|
|
4394
|
+
request: requestId,
|
|
4395
|
+
actor: actorId,
|
|
4396
|
+
submitter: String(raw.submitter_id),
|
|
4397
|
+
status: raw.status
|
|
4398
|
+
});
|
|
4399
|
+
throw new Error(
|
|
4400
|
+
`FORBIDDEN: ${this.userFacingRefusal("approval_recall_not_submitter", context)}`
|
|
4401
|
+
);
|
|
4084
4402
|
}
|
|
4085
4403
|
if (inReviseWindow) await this.assertLatestForRun(raw);
|
|
4086
4404
|
const config = parseJson(raw.node_config_json, { approvers: [], behavior: "first_response" });
|
|
@@ -4156,6 +4474,127 @@ var _ApprovalService = class _ApprovalService {
|
|
|
4156
4474
|
const fresh = await this.readBackRequest(requestId, context);
|
|
4157
4475
|
return { request: fresh, runId, resumed, ...resumeError ? { resumeError } : {} };
|
|
4158
4476
|
}
|
|
4477
|
+
// ── Record-delete lifecycle linkage (#13568) ─────────────────
|
|
4478
|
+
/**
|
|
4479
|
+
* Void every `pending` request about a record that has just been DELETED
|
|
4480
|
+
* (#13568, maintainer ruling 2026-08-31 「同意」).
|
|
4481
|
+
*
|
|
4482
|
+
* ## What the ruling asked for, and what each half costs
|
|
4483
|
+
*
|
|
4484
|
+
* status → `cancelled`, a machine-readable `cancel_reason`, the row KEPT
|
|
4485
|
+
* for audit, and the request out of the pending count and the inbox's
|
|
4486
|
+
* default view.
|
|
4487
|
+
*
|
|
4488
|
+
* The last clause needs both writes below, not just the status one: the
|
|
4489
|
+
* inbox's "My Pending" tab pages the request table by `status`, but the
|
|
4490
|
+
* approver filter resolves through the normalized `sys_approval_approver`
|
|
4491
|
+
* index (#1745), which is keyed on nothing but `request_id`. Leaving the
|
|
4492
|
+
* index rows behind would keep the request in `approverRequestIds`' answer
|
|
4493
|
+
* for every approver forever — so {@link syncApproverIndex} clears them, the
|
|
4494
|
+
* same call every other exit from `pending` makes.
|
|
4495
|
+
*
|
|
4496
|
+
* ## Three things this deliberately does NOT do
|
|
4497
|
+
*
|
|
4498
|
+
* 1. **No status mirror-back.** Every other terminal transition calls
|
|
4499
|
+
* {@link mirrorStatusField} to write the outcome onto the subject record's
|
|
4500
|
+
* `approvalStatusField`. Here the subject record is precisely what no
|
|
4501
|
+
* longer exists, so that write is guaranteed to fail — it is the very
|
|
4502
|
+
* `update_record(...) failed: Record ... not found` this card's own
|
|
4503
|
+
* forensics recorded on the reject door. Skipped by construction, not by a
|
|
4504
|
+
* swallowed error.
|
|
4505
|
+
*
|
|
4506
|
+
* 2. **No flow resume, and no run cancel.** A cancellation is a status write
|
|
4507
|
+
* plus a reason; it is not a decision, so there is no branch to resume
|
|
4508
|
+
* down and no downstream node that should run. The suspended run this
|
|
4509
|
+
* request gated is reported (below) and left alone — what becomes of an
|
|
4510
|
+
* approval run whose request was voided is a lifecycle question that
|
|
4511
|
+
* belongs to the automation service, not to a delete hook, and answering
|
|
4512
|
+
* it here by guessing would be exactly the kind of consumer-side
|
|
4513
|
+
* accommodation PD #12 refuses.
|
|
4514
|
+
*
|
|
4515
|
+
* 3. **No refusal of the delete.** The delete has already landed by the time
|
|
4516
|
+
* this runs, and the "forbid delete while an approval is pending"
|
|
4517
|
+
* direction was VETOED in the same ruling (`lockRecord` already blocks the
|
|
4518
|
+
* edit; blocking the delete too locks an author onto a record they cannot
|
|
4519
|
+
* fix). So nothing here throws — a failure degrades to a warning and the
|
|
4520
|
+
* stale row, which is the pre-existing state, never to a failed delete.
|
|
4521
|
+
*
|
|
4522
|
+
* Terminal rows are untouched: the `where` below names `status: 'pending'`,
|
|
4523
|
+
* so an `approved` / `rejected` / `recalled` / `returned` request about the
|
|
4524
|
+
* same record keeps its recorded outcome. That is the ruling's second half —
|
|
4525
|
+
* history is kept; only the presentation of its dead record reference is
|
|
4526
|
+
* someone else's card.
|
|
4527
|
+
*
|
|
4528
|
+
* @returns the request ids cancelled and the suspended run ids left behind.
|
|
4529
|
+
*/
|
|
4530
|
+
async cancelForDeletedRecord(objectName, recordId) {
|
|
4531
|
+
const empty = { cancelled: [], suspendedRuns: [] };
|
|
4532
|
+
const object = String(objectName ?? "").trim();
|
|
4533
|
+
const record = String(recordId ?? "").trim();
|
|
4534
|
+
if (!object || !record) return empty;
|
|
4535
|
+
let pending;
|
|
4536
|
+
try {
|
|
4537
|
+
const rows = await this.engine.find("sys_approval_request", {
|
|
4538
|
+
where: { object_name: object, record_id: record, status: "pending" },
|
|
4539
|
+
limit: RECORD_DELETE_CANCEL_LIMIT,
|
|
4540
|
+
context: SYSTEM_CTX2
|
|
4541
|
+
});
|
|
4542
|
+
pending = Array.isArray(rows) ? rows : [];
|
|
4543
|
+
} catch (err) {
|
|
4544
|
+
this.logger?.warn?.(
|
|
4545
|
+
"[approvals] could not read the pending requests of a deleted record \u2014 they stay in the inbox until the record is deleted again or an operator finalises them",
|
|
4546
|
+
{ object, record, error: err?.message ?? String(err) }
|
|
4547
|
+
);
|
|
4548
|
+
return empty;
|
|
4549
|
+
}
|
|
4550
|
+
if (pending.length === 0) return empty;
|
|
4551
|
+
const now = this.clock.now().toISOString();
|
|
4552
|
+
const cancelled = [];
|
|
4553
|
+
const suspendedRuns = [];
|
|
4554
|
+
for (const raw of pending) {
|
|
4555
|
+
const requestId = String(raw?.id ?? "");
|
|
4556
|
+
if (!requestId) continue;
|
|
4557
|
+
const org = raw?.organization_id ?? null;
|
|
4558
|
+
const nodeId = raw?.flow_node_id ?? raw?.current_step ?? null;
|
|
4559
|
+
try {
|
|
4560
|
+
await this.engine.insert("sys_approval_action", {
|
|
4561
|
+
id: uid("aact"),
|
|
4562
|
+
request_id: requestId,
|
|
4563
|
+
organization_id: org,
|
|
4564
|
+
step_name: nodeId,
|
|
4565
|
+
step_index: 0,
|
|
4566
|
+
action: "cancel",
|
|
4567
|
+
actor_id: null,
|
|
4568
|
+
comment: `Auto-cancelled: the ${object} record '${record}' this request is about was deleted`,
|
|
4569
|
+
created_at: now
|
|
4570
|
+
}, { context: SYSTEM_CTX2 });
|
|
4571
|
+
await this.engine.update("sys_approval_request", {
|
|
4572
|
+
id: requestId,
|
|
4573
|
+
status: "cancelled",
|
|
4574
|
+
cancel_reason: "record_deleted",
|
|
4575
|
+
pending_approvers: null,
|
|
4576
|
+
completed_at: now,
|
|
4577
|
+
updated_at: now
|
|
4578
|
+
}, { context: SYSTEM_CTX2 });
|
|
4579
|
+
await this.syncApproverIndex(requestId, [], org, now);
|
|
4580
|
+
cancelled.push(requestId);
|
|
4581
|
+
const runId = raw?.flow_run_id ? String(raw.flow_run_id) : "";
|
|
4582
|
+
if (runId) suspendedRuns.push(runId);
|
|
4583
|
+
} catch (err) {
|
|
4584
|
+
this.logger?.warn?.(
|
|
4585
|
+
"[approvals] could not auto-cancel a pending request whose record was deleted \u2014 it stays in the inbox pointing at a record that no longer exists",
|
|
4586
|
+
{ object, record, request: requestId, error: err?.message ?? String(err) }
|
|
4587
|
+
);
|
|
4588
|
+
}
|
|
4589
|
+
}
|
|
4590
|
+
if (suspendedRuns.length) {
|
|
4591
|
+
this.logger?.warn?.(
|
|
4592
|
+
"[approvals] auto-cancelled approval request(s) for a deleted record \u2014 the automation run(s) they gated stay suspended and are not resumed by a cancellation",
|
|
4593
|
+
{ object, record, requests: cancelled, runs: suspendedRuns }
|
|
4594
|
+
);
|
|
4595
|
+
}
|
|
4596
|
+
return { cancelled, suspendedRuns };
|
|
4597
|
+
}
|
|
4159
4598
|
// ── Send back for revision / resubmit (ADR-0044) ─────────────
|
|
4160
4599
|
/**
|
|
4161
4600
|
* ADR-0044 send back for revision. Finalises the pending request as
|
|
@@ -4786,7 +5225,10 @@ var _ApprovalService = class _ApprovalService {
|
|
|
4786
5225
|
* `escalation.timeoutHours` and whose deadline has passed is escalated
|
|
4787
5226
|
* **at most once, ever** — the `escalate` audit row is the idempotency
|
|
4788
5227
|
* marker, written before any mutation (audit-first, like reassign). One
|
|
4789
|
-
* bad row never stops the sweep.
|
|
5228
|
+
* bad row never stops the sweep. A block that explicitly declares
|
|
5229
|
+
* `enabled: false` is skipped (#12278 — the declared switch is enforced;
|
|
5230
|
+
* pre-flip snapshots ride the legacy window on
|
|
5231
|
+
* {@link ESCALATION_ENABLED_FLIP_CUTOFF_MS}).
|
|
4790
5232
|
*/
|
|
4791
5233
|
async runEscalations() {
|
|
4792
5234
|
let rows = [];
|
|
@@ -4808,6 +5250,7 @@ var _ApprovalService = class _ApprovalService {
|
|
|
4808
5250
|
const cfg = parseJson(raw.node_config_json, void 0);
|
|
4809
5251
|
const esc2 = cfg?.escalation;
|
|
4810
5252
|
if (!esc2 || typeof esc2.timeoutHours !== "number" || esc2.timeoutHours <= 0) continue;
|
|
5253
|
+
if (esc2.enabled === false && Date.parse(raw.created_at ?? "") >= ESCALATION_ENABLED_FLIP_CUTOFF_MS) continue;
|
|
4811
5254
|
const due = slaDueAt(raw.created_at, cfg);
|
|
4812
5255
|
if (!due || Date.parse(due) > this.clock.now().getTime()) continue;
|
|
4813
5256
|
const prior = await this.engine.find("sys_approval_action", {
|
|
@@ -4888,9 +5331,35 @@ var _ApprovalService = class _ApprovalService {
|
|
|
4888
5331
|
* live pause exists. It THROWS when the store cannot be read, and that
|
|
4889
5332
|
* case is SKIPPED, never counted as dead: an unreadable store means
|
|
4890
5333
|
* "unknown", and a storage outage must not be published as a lost run.
|
|
4891
|
-
* - `getRun(runId)
|
|
4892
|
-
* prefixed rows in `sys_automation_run`). A run
|
|
4893
|
-
* not stranded; a request whose run neither waits
|
|
5334
|
+
* - `classifyStrandedRunState` over `getRun(runId)` — the run's own
|
|
5335
|
+
* history row (the `run_` prefixed rows in `sys_automation_run`). A run
|
|
5336
|
+
* that merely finished is not stranded; a request whose run neither waits
|
|
5337
|
+
* nor completed is.
|
|
5338
|
+
*
|
|
5339
|
+
* **The second oracle was widened (#13909), and this is the whole point of
|
|
5340
|
+
* that card's first slice.** It used to be `if (terminal) continue` — the
|
|
5341
|
+
* existence of ANY history row ended the check, on the reading "the run ran to
|
|
5342
|
+
* a terminal state, it is not dangling". That is true of a run that COMPLETED
|
|
5343
|
+
* and false of one that FAILED: the engine consumes a suspension *before*
|
|
5344
|
+
* running the downstream nodes (`AutomationEngine.resumeInternal` calls
|
|
5345
|
+
* `forgetSuspendedRun(run, 'resumed')` and only then `traverseNext`), so a
|
|
5346
|
+
* downstream node that merely threw threw with the pause already gone, and the
|
|
5347
|
+
* catch arm wrote a terminal `failed` row. The decision is durable, the
|
|
5348
|
+
* continuation stopped half-way, `resume` answers `RUN_NOT_FOUND` and
|
|
5349
|
+
* `cancelRun` is a no-op — and the terminal row this oracle used to read as
|
|
5350
|
+
* health is written BY the very failure that stranded it. So this inspection
|
|
5351
|
+
* reported `0` for the one shape an operator most needs to see.
|
|
5352
|
+
*
|
|
5353
|
+
* ⚠️ The widening does NOT reverse the conservatism: `completed`, `cancelled`
|
|
5354
|
+
* and `paused` are each still skipped, for reasons named one at a time in
|
|
5355
|
+
* `classifyStrandedRunState`, and an unrecognised status is skipped too.
|
|
5356
|
+
* What the widening buys is that a `failed` run is now reported with
|
|
5357
|
+
* `runState: 'failed'` instead of counted as healthy.
|
|
5358
|
+
*
|
|
5359
|
+
* ⚠️ **What this can and cannot size.** It makes the condition *visible* in a
|
|
5360
|
+
* deployment; it is not itself a census, and it says nothing about this
|
|
5361
|
+
* repository. How many runs are already in this state can only be answered
|
|
5362
|
+
* against a real deployment's tables — see the card.
|
|
4894
5363
|
*
|
|
4895
5364
|
* **Reports; never rewrites.** No status is changed and no run is cancelled.
|
|
4896
5365
|
* The decision genuinely happened — a human approved or rejected — and
|
|
@@ -4948,7 +5417,8 @@ var _ApprovalService = class _ApprovalService {
|
|
|
4948
5417
|
});
|
|
4949
5418
|
continue;
|
|
4950
5419
|
}
|
|
4951
|
-
|
|
5420
|
+
const runState = classifyStrandedRunState(terminal);
|
|
5421
|
+
if (!runState) continue;
|
|
4952
5422
|
const config = parseJson(
|
|
4953
5423
|
raw.node_config_json,
|
|
4954
5424
|
{ approvers: [], behavior: "first_response" }
|
|
@@ -4971,6 +5441,7 @@ var _ApprovalService = class _ApprovalService {
|
|
|
4971
5441
|
requestId: String(raw.id),
|
|
4972
5442
|
status: raw.status,
|
|
4973
5443
|
runId,
|
|
5444
|
+
runState,
|
|
4974
5445
|
flowName: typeof raw.process_name === "string" ? raw.process_name.replace(/^flow:/, "") : void 0,
|
|
4975
5446
|
nodeId: raw.flow_node_id ?? raw.current_step ?? void 0,
|
|
4976
5447
|
objectName: raw.object_name,
|
|
@@ -4982,11 +5453,13 @@ var _ApprovalService = class _ApprovalService {
|
|
|
4982
5453
|
});
|
|
4983
5454
|
}
|
|
4984
5455
|
if (stranded.length || undetermined) {
|
|
4985
|
-
this.logger?.warn?.("[approvals] stranded terminal requests (decision recorded, flow run
|
|
5456
|
+
this.logger?.warn?.("[approvals] stranded terminal requests (decision recorded, flow run unrecoverable)", {
|
|
4986
5457
|
scanned: rows.length,
|
|
4987
5458
|
stranded: stranded.length,
|
|
4988
5459
|
undetermined,
|
|
4989
|
-
|
|
5460
|
+
runMissing: stranded.filter((s) => s.runState === "missing").length,
|
|
5461
|
+
runFailed: stranded.filter((s) => s.runState === "failed").length,
|
|
5462
|
+
requests: stranded.map((s) => `${s.requestId}@${s.nodeId ?? "?"} \u2192 run ${s.runId} (${s.runState})`)
|
|
4990
5463
|
});
|
|
4991
5464
|
}
|
|
4992
5465
|
return { scanned: rows.length, stranded, undetermined };
|
|
@@ -5840,9 +6313,28 @@ var _ApprovalService = class _ApprovalService {
|
|
|
5840
6313
|
* flow-driven resume, a service-to-service call), turning a successful write
|
|
5841
6314
|
* into a `null` result. Gating belongs on the read API, not on an
|
|
5842
6315
|
* operation's own return value.
|
|
6316
|
+
*
|
|
6317
|
+
* [#12769] The read still narrows by the CALLER's organization
|
|
6318
|
+
* (`loadRequest` — a deliberate tenancy wall, untouched here), and an
|
|
6319
|
+
* org-less request row is invisible inside an org-scoped caller's narrowing.
|
|
6320
|
+
* Org-less rows arise by construction on every schedule / time-relative /
|
|
6321
|
+
* api trigger run (#10131; #9132 pinned that behaviour rather than repairing
|
|
6322
|
+
* it), so this is a live state, not an edge case: the write IS recorded and
|
|
6323
|
+
* the echo cannot be built. It used to escape as `null` behind the callers'
|
|
6324
|
+
* `fresh!` non-null assertions — a well-formed 200 whose declared-non-null
|
|
6325
|
+
* `request` was `null` on the wire. Now it refuses loudly instead, which
|
|
6326
|
+
* keeps every declared result type true: a result that cannot be built is
|
|
6327
|
+
* never returned. Callers must NOT catch this to fall back to `null` —
|
|
6328
|
+
* that would re-open the type-lie one level up.
|
|
5843
6329
|
*/
|
|
5844
6330
|
async readBackRequest(requestId, context) {
|
|
5845
|
-
|
|
6331
|
+
const fresh = await this.loadRequest(requestId, context, false);
|
|
6332
|
+
if (!fresh) {
|
|
6333
|
+
throw new Error(
|
|
6334
|
+
`READ_BACK_FAILED: the write to approval request '${requestId}' was recorded, but the updated row is not visible inside the caller's organization scope, so the result envelope cannot be built. The write is NOT rolled back \u2014 read the request back with a system or matching-organization context.`
|
|
6335
|
+
);
|
|
6336
|
+
}
|
|
6337
|
+
return fresh;
|
|
5846
6338
|
}
|
|
5847
6339
|
async getRequest(requestId, context) {
|
|
5848
6340
|
return this.loadRequest(requestId, context, true);
|
|
@@ -6334,6 +6826,41 @@ function bindApprovalLockHook(engine, logger) {
|
|
|
6334
6826
|
}, { packageId: APPROVALS_HOOK_PACKAGE, priority: 50 });
|
|
6335
6827
|
logger?.info?.("[approvals] record-lock hook bound");
|
|
6336
6828
|
}
|
|
6829
|
+
var APPROVALS_OWN_OBJECTS = [
|
|
6830
|
+
"sys_approval_request",
|
|
6831
|
+
"sys_approval_action",
|
|
6832
|
+
"sys_approval_approver",
|
|
6833
|
+
"sys_approval_token",
|
|
6834
|
+
"sys_approval_delegation"
|
|
6835
|
+
];
|
|
6836
|
+
function bindRecordDeleteCancelHook(engine, service, logger) {
|
|
6837
|
+
engine.registerHook("afterDelete", async (ctx) => {
|
|
6838
|
+
const object = ctx?.object ?? ctx?.objectName;
|
|
6839
|
+
if (!object) return;
|
|
6840
|
+
const raw = ctx?.previous?.id ?? ctx?.input?.id;
|
|
6841
|
+
const recordId = raw == null ? "" : String(raw);
|
|
6842
|
+
if (!recordId) {
|
|
6843
|
+
logger?.warn?.(
|
|
6844
|
+
"[approvals] a delete reached the approval-cancel hook with no record id \u2014 pending approvals for the deleted record (if any) stay in the inbox",
|
|
6845
|
+
{ object }
|
|
6846
|
+
);
|
|
6847
|
+
return;
|
|
6848
|
+
}
|
|
6849
|
+
try {
|
|
6850
|
+
await service.cancelForDeletedRecord(String(object), recordId);
|
|
6851
|
+
} catch (err) {
|
|
6852
|
+
logger?.warn?.(
|
|
6853
|
+
"[approvals] the approval-cancel linkage failed for a deleted record \u2014 its pending approvals stay in the inbox pointing at a record that no longer exists",
|
|
6854
|
+
{ object, record: recordId, error: err?.message ?? String(err) }
|
|
6855
|
+
);
|
|
6856
|
+
}
|
|
6857
|
+
}, {
|
|
6858
|
+
excludeObjects: [...APPROVALS_OWN_OBJECTS],
|
|
6859
|
+
packageId: APPROVALS_HOOK_PACKAGE,
|
|
6860
|
+
priority: 50
|
|
6861
|
+
});
|
|
6862
|
+
logger?.info?.("[approvals] record-delete cancel hook bound");
|
|
6863
|
+
}
|
|
6337
6864
|
var DELEGATION_OBJECT = "sys_approval_delegation";
|
|
6338
6865
|
function bindDelegationWriteGuard(engine, logger) {
|
|
6339
6866
|
const makeGuard = (isInsert) => async (ctx) => {
|
|
@@ -6677,6 +7204,23 @@ var ApprovalsServicePlugin = class {
|
|
|
6677
7204
|
} catch {
|
|
6678
7205
|
return void 0;
|
|
6679
7206
|
}
|
|
7207
|
+
},
|
|
7208
|
+
// [#11993] Deployment override lookup for user-facing refusal copy.
|
|
7209
|
+
// Resolved LAZILY for the reason the two providers above are: the i18n
|
|
7210
|
+
// service is contributed by another plugin (ADR-0029 D8) and may start
|
|
7211
|
+
// after this one. Without it the service still renders the built-in
|
|
7212
|
+
// catalog in the caller's locale; with it, a deployment's own
|
|
7213
|
+
// `translation` for `errors.approval_recall_not_submitter` wins — the
|
|
7214
|
+
// override address the catalog documents, made real for this emitter.
|
|
7215
|
+
messageTranslator: () => {
|
|
7216
|
+
try {
|
|
7217
|
+
const i18n = ctx.getService("i18n");
|
|
7218
|
+
const t = i18n?.t;
|
|
7219
|
+
if (typeof t !== "function") return void 0;
|
|
7220
|
+
return (key, locale, params) => t.call(i18n, key, locale, params);
|
|
7221
|
+
} catch {
|
|
7222
|
+
return void 0;
|
|
7223
|
+
}
|
|
6680
7224
|
}
|
|
6681
7225
|
});
|
|
6682
7226
|
const fieldVisibility = () => {
|
|
@@ -6697,6 +7241,7 @@ var ApprovalsServicePlugin = class {
|
|
|
6697
7241
|
try {
|
|
6698
7242
|
unbindAllHooks(engine);
|
|
6699
7243
|
bindApprovalLockHook(engine, ctx.logger);
|
|
7244
|
+
bindRecordDeleteCancelHook(engine, this.service, ctx.logger);
|
|
6700
7245
|
bindDelegationWriteGuard(engine, ctx.logger);
|
|
6701
7246
|
bindSnapshotRedactionMiddleware(engine, fieldVisibility, ctx.logger);
|
|
6702
7247
|
} catch (err) {
|