@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.mjs
CHANGED
|
@@ -29,7 +29,7 @@ var init_en_objects_generated = __esm({
|
|
|
29
29
|
},
|
|
30
30
|
organization_id: {
|
|
31
31
|
label: "Organization",
|
|
32
|
-
help: "
|
|
32
|
+
help: "Organization of the record this request is about (falls back to the acting context when the record has none)"
|
|
33
33
|
},
|
|
34
34
|
process_name: {
|
|
35
35
|
label: "Source",
|
|
@@ -55,7 +55,15 @@ var init_en_objects_generated = __esm({
|
|
|
55
55
|
approved: "Approved",
|
|
56
56
|
rejected: "Rejected",
|
|
57
57
|
recalled: "Recalled",
|
|
58
|
-
returned: "Returned"
|
|
58
|
+
returned: "Returned",
|
|
59
|
+
cancelled: "Cancelled"
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
cancel_reason: {
|
|
63
|
+
label: "Cancellation Reason",
|
|
64
|
+
help: "Why the platform voided this request (set only when the status is cancelled)",
|
|
65
|
+
options: {
|
|
66
|
+
record_deleted: "Related record deleted"
|
|
59
67
|
}
|
|
60
68
|
},
|
|
61
69
|
current_step: {
|
|
@@ -236,7 +244,8 @@ var init_en_objects_generated = __esm({
|
|
|
236
244
|
comment: "Comment",
|
|
237
245
|
revise: "Revise",
|
|
238
246
|
resubmit: "Resubmit",
|
|
239
|
-
ooo_substitute: "Out-of-Office Substitution"
|
|
247
|
+
ooo_substitute: "Out-of-Office Substitution",
|
|
248
|
+
cancel: "Cancel"
|
|
240
249
|
}
|
|
241
250
|
},
|
|
242
251
|
actor_id: {
|
|
@@ -350,7 +359,7 @@ var init_zh_CN_objects_generated = __esm({
|
|
|
350
359
|
},
|
|
351
360
|
organization_id: {
|
|
352
361
|
label: "\u7EC4\u7EC7",
|
|
353
|
-
help: "\
|
|
362
|
+
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"
|
|
354
363
|
},
|
|
355
364
|
process_name: {
|
|
356
365
|
label: "\u6765\u6E90",
|
|
@@ -376,7 +385,15 @@ var init_zh_CN_objects_generated = __esm({
|
|
|
376
385
|
approved: "\u5DF2\u6279\u51C6",
|
|
377
386
|
rejected: "\u5DF2\u62D2\u7EDD",
|
|
378
387
|
recalled: "\u5DF2\u64A4\u56DE",
|
|
379
|
-
returned: "\u5DF2\u9000\u56DE\u4FEE\u6539"
|
|
388
|
+
returned: "\u5DF2\u9000\u56DE\u4FEE\u6539",
|
|
389
|
+
cancelled: "\u5DF2\u4F5C\u5E9F"
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
cancel_reason: {
|
|
393
|
+
label: "\u4F5C\u5E9F\u539F\u56E0",
|
|
394
|
+
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)",
|
|
395
|
+
options: {
|
|
396
|
+
record_deleted: "\u5173\u8054\u8BB0\u5F55\u5DF2\u5220\u9664"
|
|
380
397
|
}
|
|
381
398
|
},
|
|
382
399
|
current_step: {
|
|
@@ -557,7 +574,8 @@ var init_zh_CN_objects_generated = __esm({
|
|
|
557
574
|
comment: "\u8BC4\u8BBA",
|
|
558
575
|
revise: "\u9000\u56DE\u4FEE\u6539",
|
|
559
576
|
resubmit: "\u91CD\u65B0\u63D0\u4EA4",
|
|
560
|
-
ooo_substitute: "\u4E0D\u5728\u5C97\u6539\u6D3E"
|
|
577
|
+
ooo_substitute: "\u4E0D\u5728\u5C97\u6539\u6D3E",
|
|
578
|
+
cancel: "\u4F5C\u5E9F"
|
|
561
579
|
}
|
|
562
580
|
},
|
|
563
581
|
actor_id: {
|
|
@@ -671,7 +689,7 @@ var init_ja_JP_objects_generated = __esm({
|
|
|
671
689
|
},
|
|
672
690
|
organization_id: {
|
|
673
691
|
label: "\u7D44\u7E54",
|
|
674
|
-
help: "\u3053\u306E\
|
|
692
|
+
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"
|
|
675
693
|
},
|
|
676
694
|
process_name: {
|
|
677
695
|
label: "\u30BD\u30FC\u30B9",
|
|
@@ -697,7 +715,15 @@ var init_ja_JP_objects_generated = __esm({
|
|
|
697
715
|
approved: "\u627F\u8A8D\u6E08\u307F",
|
|
698
716
|
rejected: "\u5374\u4E0B\u6E08\u307F",
|
|
699
717
|
recalled: "\u53D6\u308A\u6D88\u3057\u6E08\u307F",
|
|
700
|
-
returned: "\u5DEE\u3057\u623B\u3057\u6E08\u307F"
|
|
718
|
+
returned: "\u5DEE\u3057\u623B\u3057\u6E08\u307F",
|
|
719
|
+
cancelled: "\u7121\u52B9\u5316\u6E08\u307F"
|
|
720
|
+
}
|
|
721
|
+
},
|
|
722
|
+
cancel_reason: {
|
|
723
|
+
label: "\u7121\u52B9\u5316\u7406\u7531",
|
|
724
|
+
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)",
|
|
725
|
+
options: {
|
|
726
|
+
record_deleted: "\u95A2\u9023\u30EC\u30B3\u30FC\u30C9\u304C\u524A\u9664\u3055\u308C\u307E\u3057\u305F"
|
|
701
727
|
}
|
|
702
728
|
},
|
|
703
729
|
current_step: {
|
|
@@ -878,7 +904,8 @@ var init_ja_JP_objects_generated = __esm({
|
|
|
878
904
|
comment: "\u30B3\u30E1\u30F3\u30C8",
|
|
879
905
|
revise: "\u5DEE\u3057\u623B\u3057",
|
|
880
906
|
resubmit: "\u518D\u63D0\u51FA",
|
|
881
|
-
ooo_substitute: "\u4E0D\u5728\u6642\u4EE3\u7406"
|
|
907
|
+
ooo_substitute: "\u4E0D\u5728\u6642\u4EE3\u7406",
|
|
908
|
+
cancel: "\u7121\u52B9\u5316"
|
|
882
909
|
}
|
|
883
910
|
},
|
|
884
911
|
actor_id: {
|
|
@@ -992,7 +1019,7 @@ var init_es_ES_objects_generated = __esm({
|
|
|
992
1019
|
},
|
|
993
1020
|
organization_id: {
|
|
994
1021
|
label: "Organizaci\xF3n",
|
|
995
|
-
help: "
|
|
1022
|
+
help: "Organizaci\xF3n del registro al que se refiere esta solicitud (recurre al contexto del actor cuando el registro no tiene ninguna)."
|
|
996
1023
|
},
|
|
997
1024
|
process_name: {
|
|
998
1025
|
label: "Origen",
|
|
@@ -1018,7 +1045,15 @@ var init_es_ES_objects_generated = __esm({
|
|
|
1018
1045
|
approved: "Aprobada",
|
|
1019
1046
|
rejected: "Rechazada",
|
|
1020
1047
|
recalled: "Retirada",
|
|
1021
|
-
returned: "Devuelta para revisi\xF3n"
|
|
1048
|
+
returned: "Devuelta para revisi\xF3n",
|
|
1049
|
+
cancelled: "Anulada"
|
|
1050
|
+
}
|
|
1051
|
+
},
|
|
1052
|
+
cancel_reason: {
|
|
1053
|
+
label: "Motivo de anulaci\xF3n",
|
|
1054
|
+
help: "Por qu\xE9 la plataforma anul\xF3 esta solicitud (solo se establece cuando el estado es Anulada)",
|
|
1055
|
+
options: {
|
|
1056
|
+
record_deleted: "Registro relacionado eliminado"
|
|
1022
1057
|
}
|
|
1023
1058
|
},
|
|
1024
1059
|
current_step: {
|
|
@@ -1199,7 +1234,8 @@ var init_es_ES_objects_generated = __esm({
|
|
|
1199
1234
|
comment: "Comentario",
|
|
1200
1235
|
revise: "Devoluci\xF3n",
|
|
1201
1236
|
resubmit: "Reenv\xEDo",
|
|
1202
|
-
ooo_substitute: "Sustituci\xF3n por ausencia"
|
|
1237
|
+
ooo_substitute: "Sustituci\xF3n por ausencia",
|
|
1238
|
+
cancel: "Anulaci\xF3n"
|
|
1203
1239
|
}
|
|
1204
1240
|
},
|
|
1205
1241
|
actor_id: {
|
|
@@ -1297,12 +1333,104 @@ var init_es_ES_objects_generated = __esm({
|
|
|
1297
1333
|
}
|
|
1298
1334
|
});
|
|
1299
1335
|
|
|
1336
|
+
// src/translations/zh-CN.source-hashes.generated.ts
|
|
1337
|
+
var zhCNGeneratedSourceHashes;
|
|
1338
|
+
var init_zh_CN_source_hashes_generated = __esm({
|
|
1339
|
+
"src/translations/zh-CN.source-hashes.generated.ts"() {
|
|
1340
|
+
"use strict";
|
|
1341
|
+
zhCNGeneratedSourceHashes = {
|
|
1342
|
+
"objects.sys_approval_request.fields.flow_node_id.help": "154aa23b4eee4cae",
|
|
1343
|
+
"objects.sys_approval_request.fields.flow_node_id.label": "052ad568aa41227c",
|
|
1344
|
+
"objects.sys_approval_request.fields.flow_run_id.help": "35c92818f5e11090",
|
|
1345
|
+
"objects.sys_approval_request.fields.flow_run_id.label": "d0124e323da966f9",
|
|
1346
|
+
"objects.sys_approval_request.fields.node_config_json.help": "7ee00ef9b3ca7e80",
|
|
1347
|
+
"objects.sys_approval_request.fields.node_config_json.label": "e64eb4bf7d7ea033"
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
});
|
|
1351
|
+
|
|
1352
|
+
// src/translations/ja-JP.source-hashes.generated.ts
|
|
1353
|
+
var jaJPGeneratedSourceHashes;
|
|
1354
|
+
var init_ja_JP_source_hashes_generated = __esm({
|
|
1355
|
+
"src/translations/ja-JP.source-hashes.generated.ts"() {
|
|
1356
|
+
"use strict";
|
|
1357
|
+
jaJPGeneratedSourceHashes = {
|
|
1358
|
+
"objects.sys_approval_delegation._views.active.emptyState.message": "1095d61c017b6db2",
|
|
1359
|
+
"objects.sys_approval_delegation._views.active.emptyState.title": "799d28e51d88963e",
|
|
1360
|
+
"objects.sys_approval_delegation._views.active.label": "eee107cf4e466779",
|
|
1361
|
+
"objects.sys_approval_delegation.description": "432b0435aa15f6b9",
|
|
1362
|
+
"objects.sys_approval_delegation.fields.created_at.label": "1f02d416befb595b",
|
|
1363
|
+
"objects.sys_approval_delegation.fields.delegate_id.help": "9cfc641f7b0b04f9",
|
|
1364
|
+
"objects.sys_approval_delegation.fields.delegate_id.label": "afd6d8733dc5bc14",
|
|
1365
|
+
"objects.sys_approval_delegation.fields.delegator_id.help": "c4686c5c9f24e0be",
|
|
1366
|
+
"objects.sys_approval_delegation.fields.delegator_id.label": "f76b1f95f2fdabff",
|
|
1367
|
+
"objects.sys_approval_delegation.fields.id.label": "3383564051b4b76d",
|
|
1368
|
+
"objects.sys_approval_delegation.fields.organization_id.help": "f02982e88229d9ca",
|
|
1369
|
+
"objects.sys_approval_delegation.fields.organization_id.label": "3e55836156e1c1de",
|
|
1370
|
+
"objects.sys_approval_delegation.fields.reason.help": "712516f1461dff2b",
|
|
1371
|
+
"objects.sys_approval_delegation.fields.reason.label": "ac399f03d7f2e64d",
|
|
1372
|
+
"objects.sys_approval_delegation.fields.updated_at.label": "aba63dc2a9c79b8d",
|
|
1373
|
+
"objects.sys_approval_delegation.fields.valid_from.help": "01386010c454bfbd",
|
|
1374
|
+
"objects.sys_approval_delegation.fields.valid_from.label": "02eaf201e57c9eda",
|
|
1375
|
+
"objects.sys_approval_delegation.fields.valid_until.help": "a48ea34e65933e9d",
|
|
1376
|
+
"objects.sys_approval_delegation.fields.valid_until.label": "a68db5bd4fc72a13",
|
|
1377
|
+
"objects.sys_approval_delegation.label": "4389c4fe5f5c9746",
|
|
1378
|
+
"objects.sys_approval_delegation.pluralLabel": "f07fec3ffd74e6db",
|
|
1379
|
+
"objects.sys_approval_request.fields.flow_node_id.help": "154aa23b4eee4cae",
|
|
1380
|
+
"objects.sys_approval_request.fields.flow_node_id.label": "052ad568aa41227c",
|
|
1381
|
+
"objects.sys_approval_request.fields.flow_run_id.help": "35c92818f5e11090",
|
|
1382
|
+
"objects.sys_approval_request.fields.flow_run_id.label": "d0124e323da966f9",
|
|
1383
|
+
"objects.sys_approval_request.fields.node_config_json.help": "7ee00ef9b3ca7e80",
|
|
1384
|
+
"objects.sys_approval_request.fields.node_config_json.label": "e64eb4bf7d7ea033"
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1387
|
+
});
|
|
1388
|
+
|
|
1389
|
+
// src/translations/es-ES.source-hashes.generated.ts
|
|
1390
|
+
var esESGeneratedSourceHashes;
|
|
1391
|
+
var init_es_ES_source_hashes_generated = __esm({
|
|
1392
|
+
"src/translations/es-ES.source-hashes.generated.ts"() {
|
|
1393
|
+
"use strict";
|
|
1394
|
+
esESGeneratedSourceHashes = {
|
|
1395
|
+
"objects.sys_approval_action.fields.actor_id.label": "b155813f8a7f06e3",
|
|
1396
|
+
"objects.sys_approval_delegation._views.active.emptyState.message": "1095d61c017b6db2",
|
|
1397
|
+
"objects.sys_approval_delegation._views.active.emptyState.title": "799d28e51d88963e",
|
|
1398
|
+
"objects.sys_approval_delegation._views.active.label": "eee107cf4e466779",
|
|
1399
|
+
"objects.sys_approval_delegation.description": "432b0435aa15f6b9",
|
|
1400
|
+
"objects.sys_approval_delegation.fields.created_at.label": "1f02d416befb595b",
|
|
1401
|
+
"objects.sys_approval_delegation.fields.delegate_id.help": "9cfc641f7b0b04f9",
|
|
1402
|
+
"objects.sys_approval_delegation.fields.delegate_id.label": "afd6d8733dc5bc14",
|
|
1403
|
+
"objects.sys_approval_delegation.fields.delegator_id.help": "c4686c5c9f24e0be",
|
|
1404
|
+
"objects.sys_approval_delegation.fields.delegator_id.label": "f76b1f95f2fdabff",
|
|
1405
|
+
"objects.sys_approval_delegation.fields.id.label": "3383564051b4b76d",
|
|
1406
|
+
"objects.sys_approval_delegation.fields.organization_id.help": "f02982e88229d9ca",
|
|
1407
|
+
"objects.sys_approval_delegation.fields.organization_id.label": "3e55836156e1c1de",
|
|
1408
|
+
"objects.sys_approval_delegation.fields.reason.help": "712516f1461dff2b",
|
|
1409
|
+
"objects.sys_approval_delegation.fields.reason.label": "ac399f03d7f2e64d",
|
|
1410
|
+
"objects.sys_approval_delegation.fields.updated_at.label": "aba63dc2a9c79b8d",
|
|
1411
|
+
"objects.sys_approval_delegation.fields.valid_from.help": "01386010c454bfbd",
|
|
1412
|
+
"objects.sys_approval_delegation.fields.valid_from.label": "02eaf201e57c9eda",
|
|
1413
|
+
"objects.sys_approval_delegation.fields.valid_until.help": "a48ea34e65933e9d",
|
|
1414
|
+
"objects.sys_approval_delegation.fields.valid_until.label": "a68db5bd4fc72a13",
|
|
1415
|
+
"objects.sys_approval_delegation.label": "4389c4fe5f5c9746",
|
|
1416
|
+
"objects.sys_approval_delegation.pluralLabel": "f07fec3ffd74e6db",
|
|
1417
|
+
"objects.sys_approval_request.fields.flow_node_id.help": "154aa23b4eee4cae",
|
|
1418
|
+
"objects.sys_approval_request.fields.flow_node_id.label": "052ad568aa41227c",
|
|
1419
|
+
"objects.sys_approval_request.fields.flow_run_id.help": "35c92818f5e11090",
|
|
1420
|
+
"objects.sys_approval_request.fields.flow_run_id.label": "d0124e323da966f9",
|
|
1421
|
+
"objects.sys_approval_request.fields.node_config_json.help": "7ee00ef9b3ca7e80",
|
|
1422
|
+
"objects.sys_approval_request.fields.node_config_json.label": "e64eb4bf7d7ea033"
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
});
|
|
1426
|
+
|
|
1300
1427
|
// src/translations/index.ts
|
|
1301
1428
|
var translations_exports = {};
|
|
1302
1429
|
__export(translations_exports, {
|
|
1303
1430
|
ApprovalsTranslations: () => ApprovalsTranslations
|
|
1304
1431
|
});
|
|
1305
|
-
|
|
1432
|
+
import { withSourceFallback } from "@objectstack/platform-objects/apps";
|
|
1433
|
+
var enSource, ApprovalsTranslations;
|
|
1306
1434
|
var init_translations = __esm({
|
|
1307
1435
|
"src/translations/index.ts"() {
|
|
1308
1436
|
"use strict";
|
|
@@ -1310,18 +1438,27 @@ var init_translations = __esm({
|
|
|
1310
1438
|
init_zh_CN_objects_generated();
|
|
1311
1439
|
init_ja_JP_objects_generated();
|
|
1312
1440
|
init_es_ES_objects_generated();
|
|
1441
|
+
init_zh_CN_source_hashes_generated();
|
|
1442
|
+
init_ja_JP_source_hashes_generated();
|
|
1443
|
+
init_es_ES_source_hashes_generated();
|
|
1444
|
+
enSource = { objects: enObjects };
|
|
1313
1445
|
ApprovalsTranslations = {
|
|
1314
|
-
en:
|
|
1315
|
-
"zh-CN": { objects: zhCNObjects },
|
|
1316
|
-
"ja-JP": { objects: jaJPObjects },
|
|
1317
|
-
"es-ES": { objects: esESObjects }
|
|
1446
|
+
en: enSource,
|
|
1447
|
+
"zh-CN": withSourceFallback({ objects: zhCNObjects }, enSource, void 0, zhCNGeneratedSourceHashes),
|
|
1448
|
+
"ja-JP": withSourceFallback({ objects: jaJPObjects }, enSource, void 0, jaJPGeneratedSourceHashes),
|
|
1449
|
+
"es-ES": withSourceFallback({ objects: esESObjects }, enSource, void 0, esESGeneratedSourceHashes)
|
|
1318
1450
|
};
|
|
1319
1451
|
}
|
|
1320
1452
|
});
|
|
1321
1453
|
|
|
1322
1454
|
// src/sys-approval-request.object.ts
|
|
1323
1455
|
import { ObjectSchema, Field } from "@objectstack/spec/data";
|
|
1324
|
-
import {
|
|
1456
|
+
import {
|
|
1457
|
+
APPROVAL_STATUSES,
|
|
1458
|
+
APPROVAL_STATUS_LABELS,
|
|
1459
|
+
APPROVAL_CANCEL_REASONS,
|
|
1460
|
+
APPROVAL_CANCEL_REASON_LABELS
|
|
1461
|
+
} from "@objectstack/spec/contracts";
|
|
1325
1462
|
var SysApprovalRequest = ObjectSchema.create({
|
|
1326
1463
|
name: "sys_approval_request",
|
|
1327
1464
|
label: "Approval Request",
|
|
@@ -1368,7 +1505,10 @@ var SysApprovalRequest = ObjectSchema.create({
|
|
|
1368
1505
|
label: "Completed",
|
|
1369
1506
|
data: { provider: "object", object: "sys_approval_request" },
|
|
1370
1507
|
columns: ["process_name", "object_name", "record_id", "status", "submitter_id", "completed_at"],
|
|
1371
|
-
|
|
1508
|
+
// Every terminal state, `cancelled` included (#13568) — a platform-voided
|
|
1509
|
+
// request is kept as audit evidence, and a terminal state absent from the
|
|
1510
|
+
// only curated terminal view is evidence nobody can find.
|
|
1511
|
+
filter: [{ field: "status", operator: "in", value: ["approved", "rejected", "recalled", "cancelled"] }],
|
|
1372
1512
|
sort: [{ field: "completed_at", order: "desc" }],
|
|
1373
1513
|
pagination: { pageSize: 25 }
|
|
1374
1514
|
},
|
|
@@ -1384,46 +1524,40 @@ var SysApprovalRequest = ObjectSchema.create({
|
|
|
1384
1524
|
},
|
|
1385
1525
|
fields: {
|
|
1386
1526
|
id: Field.text({ label: "Request ID", required: true, readonly: true, group: "System" }),
|
|
1387
|
-
//
|
|
1388
|
-
//
|
|
1389
|
-
//
|
|
1390
|
-
//
|
|
1391
|
-
//
|
|
1392
|
-
//
|
|
1393
|
-
// whatever filter each surface happens to apply rather than by the data.
|
|
1394
|
-
//
|
|
1395
|
-
// The value is resolved from the ACTING CONTEXT only (`openNodeRequest`'s
|
|
1396
|
-
// `ctxOrg`), so it is NULL whenever the flow that opened the request ran
|
|
1397
|
-
// without one — every schedule / time-relative / api triggered run, none of
|
|
1398
|
-
// which sets a tenant. On a walled single-database HotCRM SaaS boot this
|
|
1399
|
-
// measured 27 of 27 rows org-less, each naming an `object_name` /
|
|
1400
|
-
// `record_id` owned by a specific customer.
|
|
1527
|
+
// [#10101, the cloud#1395 Option A ruling] The SUBJECT record's
|
|
1528
|
+
// organization, with the acting context as fallback — resolved by the
|
|
1529
|
+
// SHARED platform-row resolver (`resolveRecordOrganizationField`,
|
|
1530
|
+
// `@objectstack/metadata-core`) in `openNodeRequest`, the row's only
|
|
1531
|
+
// writer. The same `requestOrg` stamps `sys_approval_action` and the
|
|
1532
|
+
// `sys_approval_approver` index, so all three move together.
|
|
1401
1533
|
//
|
|
1402
|
-
//
|
|
1403
|
-
//
|
|
1404
|
-
//
|
|
1405
|
-
//
|
|
1406
|
-
//
|
|
1407
|
-
//
|
|
1408
|
-
//
|
|
1534
|
+
// Why subject-first: an approval request is read through the organization
|
|
1535
|
+
// wall by the approvals inbox, and the acting context is NULL on every
|
|
1536
|
+
// schedule / time-relative / api triggered run (none carries a tenant, by
|
|
1537
|
+
// construction). Stamped from the actor alone — the pre-#10101 behaviour,
|
|
1538
|
+
// measured on cloud#1395 as 27 of 27 rows org-less on a walled HotCRM SaaS
|
|
1539
|
+
// boot — such a request LOCKED the record it was about while being
|
|
1540
|
+
// invisible in every inbox, its owner's included. Subject-first is also
|
|
1541
|
+
// what `sys_audit_log`'s writer already did (#8707 honouring #8287's
|
|
1542
|
+
// ruling), so an approval row and an audit row about the same record now
|
|
1543
|
+
// land behind the same wall instead of two.
|
|
1409
1544
|
//
|
|
1410
|
-
//
|
|
1411
|
-
//
|
|
1412
|
-
//
|
|
1413
|
-
//
|
|
1414
|
-
//
|
|
1545
|
+
// The `sys_api_key` divergence is deliberate and preserved: its
|
|
1546
|
+
// `tenancy.organizationField: 'active_organization_id'` (stamp-only,
|
|
1547
|
+
// #8778) wins limb 0 of the shared resolver, while the credential table
|
|
1548
|
+
// itself stays unwalled (`tenancy.enabled: false`) — who a row is ABOUT
|
|
1549
|
+
// and what an object is WALLED by remain different questions.
|
|
1415
1550
|
organization_id: Field.lookup("sys_organization", {
|
|
1416
1551
|
label: "Organization",
|
|
1417
1552
|
required: false,
|
|
1418
1553
|
group: "System",
|
|
1419
|
-
//
|
|
1420
|
-
//
|
|
1421
|
-
//
|
|
1422
|
-
//
|
|
1423
|
-
//
|
|
1424
|
-
//
|
|
1425
|
-
|
|
1426
|
-
description: "Tenant that owns this approval request (propagated from submitter context)"
|
|
1554
|
+
// Reworded with the #10101 write-side fix (was "Tenant that owns this
|
|
1555
|
+
// approval request (propagated from submitter context)" — it claimed a
|
|
1556
|
+
// propagation that measurably did not happen, and said "Tenant" where
|
|
1557
|
+
// ADR-0120 §Terminology requires "organization"). Extracted into the
|
|
1558
|
+
// generated i18n bundles as `help`; the four locales regenerate in the
|
|
1559
|
+
// same pass.
|
|
1560
|
+
description: "Organization of the record this request is about (falls back to the acting context when the record has none)"
|
|
1427
1561
|
}),
|
|
1428
1562
|
process_name: Field.text({
|
|
1429
1563
|
label: "Source",
|
|
@@ -1442,7 +1576,38 @@ var SysApprovalRequest = ObjectSchema.create({
|
|
|
1442
1576
|
label: "Record ID",
|
|
1443
1577
|
required: true,
|
|
1444
1578
|
maxLength: 100,
|
|
1445
|
-
|
|
1579
|
+
// [#11386] The id half of this object's pointer pair (ADR-0052 §5),
|
|
1580
|
+
// adopting the #11339 carrier. VERIFIED for THIS object: the pair is not
|
|
1581
|
+
// decoration but the key the approval machinery QUERIES ON —
|
|
1582
|
+
// `approval-service.ts` finds a record's pending request with
|
|
1583
|
+
// `where: { object_name, record_id, status: 'pending' }`, and
|
|
1584
|
+
// `lifecycle-hooks.ts` holds the record LOCK on the same pair
|
|
1585
|
+
// (single-record and `$in` batch forms). `submit()` writes it from
|
|
1586
|
+
// `input.object` / `input.recordId`, so a stored value is always a
|
|
1587
|
+
// record id of the object the sibling names.
|
|
1588
|
+
//
|
|
1589
|
+
// Consequence of declaring, sharper here than elsewhere: a seeded
|
|
1590
|
+
// request whose `record_id` stayed a verbatim natural key locked
|
|
1591
|
+
// NOTHING and appeared under no record — it looked like a pending
|
|
1592
|
+
// approval while being invisible to both queries that give the row its
|
|
1593
|
+
// meaning. That is now a loud seed-time refusal. Both halves are
|
|
1594
|
+
// `required: true`, so the un-addressable case (id half authored, type
|
|
1595
|
+
// half empty) is already unreachable on this object.
|
|
1596
|
+
group: "Target",
|
|
1597
|
+
//
|
|
1598
|
+
// ⚠️ ORDERING CONSTRAINT — this id half is `required: true`, and that
|
|
1599
|
+
// makes it ORDER-DEPENDENT in seeds even though a pointer pair
|
|
1600
|
+
// contributes no static ordering edge (#11674, measured against the real
|
|
1601
|
+
// engine in `packages/objectql/src/engine-seed-required-deferral.test.ts`):
|
|
1602
|
+
// the seed loader defers an unresolvable reference by DELETING the column
|
|
1603
|
+
// from the pass-1 insert, required-validation rejects that row, and pass 2
|
|
1604
|
+
// is then left with no row to back-fill. So the pass-2 healing that makes
|
|
1605
|
+
// an OPTIONAL id half order-independent (`sys_audit_log`) does not reach
|
|
1606
|
+
// this one. ⇒ SEED THE TARGET DATASET FIRST. The failure if you do not is
|
|
1607
|
+
// loud in three places — a write error naming this column, a
|
|
1608
|
+
// dropped-deferral error, and `success: false` — and since #11674 the
|
|
1609
|
+
// loader also WARNS at load time, before the engine rejects the row.
|
|
1610
|
+
referenceVia: "object_name"
|
|
1446
1611
|
}),
|
|
1447
1612
|
submitter_id: Field.lookup("sys_user", {
|
|
1448
1613
|
label: "Submitter",
|
|
@@ -1470,6 +1635,29 @@ var SysApprovalRequest = ObjectSchema.create({
|
|
|
1470
1635
|
group: "State"
|
|
1471
1636
|
}
|
|
1472
1637
|
),
|
|
1638
|
+
// [#13568] Why a `cancelled` request was cancelled — the machine-readable
|
|
1639
|
+
// half of the maintainer's 2026-08-31 ruling ("状态转 cancelled + 机器可读
|
|
1640
|
+
// 原因"). Derived from `APPROVAL_CANCEL_REASONS`, never re-typed, on the
|
|
1641
|
+
// same #3786 rule the `status` column above follows.
|
|
1642
|
+
//
|
|
1643
|
+
// On the ROW rather than on the `sys_approval_action` audit entry, because
|
|
1644
|
+
// the readers are the inbox and the tombstone presentation, which page the
|
|
1645
|
+
// request table: making them join the append-only action log to learn why a
|
|
1646
|
+
// row they already hold is cancelled buys nothing and would make the reason
|
|
1647
|
+
// unavailable to a plain list view. The action row still records the event
|
|
1648
|
+
// (`action: 'cancel'`); this column records the STATE.
|
|
1649
|
+
//
|
|
1650
|
+
// Null on every non-cancelled row, and on rows written before the column
|
|
1651
|
+
// existed — "not recorded", never "cancelled for no reason".
|
|
1652
|
+
cancel_reason: Field.select(
|
|
1653
|
+
APPROVAL_CANCEL_REASONS.map((value) => ({ value, label: APPROVAL_CANCEL_REASON_LABELS[value] })),
|
|
1654
|
+
{
|
|
1655
|
+
label: "Cancellation Reason",
|
|
1656
|
+
required: false,
|
|
1657
|
+
description: "Why the platform voided this request (set only when the status is cancelled)",
|
|
1658
|
+
group: "State"
|
|
1659
|
+
}
|
|
1660
|
+
),
|
|
1473
1661
|
current_step: Field.text({
|
|
1474
1662
|
label: "Current Step",
|
|
1475
1663
|
required: false,
|
|
@@ -1489,6 +1677,22 @@ var SysApprovalRequest = ObjectSchema.create({
|
|
|
1489
1677
|
description: "Comma-separated user ids who can act on the current step",
|
|
1490
1678
|
group: "State"
|
|
1491
1679
|
}),
|
|
1680
|
+
// The module docstring above used to justify this column with "used by
|
|
1681
|
+
// notifications so they can render before the record is locked or
|
|
1682
|
+
// changed". That consumer does not exist, and the claim was cited as the
|
|
1683
|
+
// reason the column holds a FULL row before anyone checked it. Measured
|
|
1684
|
+
// against every `this.notify(...)` call site in `approval-service.ts` —
|
|
1685
|
+
// all 12 of them: each passes `{ title, message, actionUrl }` (two also
|
|
1686
|
+
// `actions`), built from `object_name` / `record_id` and the caller's
|
|
1687
|
+
// comment. None reads this column or the parsed `payload`. The real
|
|
1688
|
+
// readers are the serve path (`rowFromRequest` -> `payload`, redacted per
|
|
1689
|
+
// reader), the decide-time approver re-resolution and the org backfill,
|
|
1690
|
+
// both under SYSTEM_CTX, and the free-text predicate.
|
|
1691
|
+
//
|
|
1692
|
+
// The `description` below is deliberately UNCHANGED: it is accurate, and
|
|
1693
|
+
// it is extracted into the four generated i18n bundles as `help`. The
|
|
1694
|
+
// false sentence lived only in the JSDoc above, which is not extracted —
|
|
1695
|
+
// so this correction moves no translation leaf.
|
|
1492
1696
|
payload_json: Field.textarea({
|
|
1493
1697
|
label: "Snapshot",
|
|
1494
1698
|
required: false,
|
|
@@ -1559,11 +1763,14 @@ var SysApprovalRequest = ObjectSchema.create({
|
|
|
1559
1763
|
// per-viewer block (#3310): approver actions on `record.viewer.can_act`
|
|
1560
1764
|
// (the caller is a current pending approver — same check the service
|
|
1561
1765
|
// authorizes a decision with, so position/team approvers resolve correctly),
|
|
1562
|
-
// submitter actions on `record.viewer.is_submitter`. The
|
|
1563
|
-
// (approve/reject/reassign
|
|
1564
|
-
//
|
|
1766
|
+
// submitter actions on `record.viewer.is_submitter`. The four levers the
|
|
1767
|
+
// #3424 override covers (approve/reject/reassign, and recall since #12716)
|
|
1768
|
+
// additionally OR in `record.viewer.can_override`
|
|
1769
|
+
// so a platform/tenant admin can rescue a request routed to an
|
|
1565
1770
|
// unstaffed position — otherwise undecidable, locking the record forever — by
|
|
1566
|
-
// approving, rejecting,
|
|
1771
|
+
// approving, rejecting, reassigning it to a real approver, or recalling it
|
|
1772
|
+
// (the lever that releases the record without recording a decision nobody
|
|
1773
|
+
// made). `viewer` is
|
|
1567
1774
|
// attached by getRequest/listRequests; where it is absent the predicate fails
|
|
1568
1775
|
// closed.
|
|
1569
1776
|
//
|
|
@@ -1699,8 +1906,13 @@ var SysApprovalRequest = ObjectSchema.create({
|
|
|
1699
1906
|
// Remind / recall (pending) and resubmit / recall (returned). These are the
|
|
1700
1907
|
// submitter's own levers, so `visible` gates on `record.viewer.is_submitter`
|
|
1701
1908
|
// (server-computed on the current viewer). The service re-checks ownership;
|
|
1702
|
-
// the predicate keeps a non-submitter from ever seeing a button they
|
|
1703
|
-
// use.
|
|
1909
|
+
// the predicate keeps a plain non-submitter from ever seeing a button they
|
|
1910
|
+
// cannot use.
|
|
1911
|
+
//
|
|
1912
|
+
// `recall` is the one exception, and it is not a widening: it ALSO ORs in
|
|
1913
|
+
// the #3424 admin override (#12716), because an override admin is a caller
|
|
1914
|
+
// `ApprovalService.recall` already authorises. Remind and resubmit keep no
|
|
1915
|
+
// override arm.
|
|
1704
1916
|
{
|
|
1705
1917
|
name: "approval_remind",
|
|
1706
1918
|
label: "Send reminder",
|
|
@@ -1731,7 +1943,26 @@ var SysApprovalRequest = ObjectSchema.create({
|
|
|
1731
1943
|
],
|
|
1732
1944
|
// Recall applies while the request is live for the submitter — pending
|
|
1733
1945
|
// (withdraw) or returned (abandon the revision instead of resubmitting).
|
|
1734
|
-
|
|
1946
|
+
//
|
|
1947
|
+
// The second arm is the #3424 admin override, spelled byte-identically to
|
|
1948
|
+
// the three core decision levers above (#12716). `ApprovalService.recall`
|
|
1949
|
+
// has admitted the override caller since #3424 — `isOverrideActor`'s own
|
|
1950
|
+
// doc block names recall as one of the four levers — so until this arm
|
|
1951
|
+
// landed, recall was the one authorised capability with no button: an
|
|
1952
|
+
// admin could approve or reject their way out of a stuck request (writing
|
|
1953
|
+
// a decision that did not happen) or reassign it, but could not withdraw.
|
|
1954
|
+
//
|
|
1955
|
+
// The override arm carries no status test of its own, on purpose, because
|
|
1956
|
+
// it does not need one and the siblings do not have one either: the flag
|
|
1957
|
+
// is already status-scoped where it is COMPUTED. `attachViewers` in
|
|
1958
|
+
// `approval-service.ts` sets
|
|
1959
|
+
// `can_override: row.status === 'pending' && isOverrideActor(...)` —
|
|
1960
|
+
// ANDed — so `record.viewer.can_override` can never be true off `pending`,
|
|
1961
|
+
// and this arm is pending-only in effect however CEL groups the
|
|
1962
|
+
// expression. Pinned in both directions in
|
|
1963
|
+
// `action-predicate-sparse-face.test.ts`, with the flag's own scoping
|
|
1964
|
+
// pinned against the real service in `approval-revise.test.ts`.
|
|
1965
|
+
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',
|
|
1735
1966
|
locations: ["record_section"],
|
|
1736
1967
|
successMessage: "Recalled.",
|
|
1737
1968
|
refreshAfter: true
|
|
@@ -2092,6 +2323,7 @@ import {
|
|
|
2092
2323
|
normalizeDecisionOutputs
|
|
2093
2324
|
} from "@objectstack/spec/automation";
|
|
2094
2325
|
import { ExpressionEngine, collectCelRootIdentifiers } from "@objectstack/formula";
|
|
2326
|
+
import { createRecordOrganizationResolver } from "@objectstack/metadata-core";
|
|
2095
2327
|
import { keysetWalk } from "@objectstack/types";
|
|
2096
2328
|
import {
|
|
2097
2329
|
ADMIN_FULL_ACCESS,
|
|
@@ -2102,6 +2334,7 @@ import {
|
|
|
2102
2334
|
} from "@objectstack/spec/identity";
|
|
2103
2335
|
import { RESUME_AUTHORITY_SERVICE } from "@objectstack/spec/contracts";
|
|
2104
2336
|
import { isFileIdToken } from "@objectstack/spec/data";
|
|
2337
|
+
import { renderOperationMessage } from "@objectstack/spec/system";
|
|
2105
2338
|
import { isGrantActive } from "@objectstack/core";
|
|
2106
2339
|
|
|
2107
2340
|
// src/approver-org-scope.ts
|
|
@@ -2257,6 +2490,7 @@ var REMIND_COOLDOWN_MS = 4 * 60 * 60 * 1e3;
|
|
|
2257
2490
|
var ESCALATION_JOB_NAME = "approvals-sla-escalation";
|
|
2258
2491
|
var ESCALATION_SCAN_INTERVAL_MS = 5 * 60 * 1e3;
|
|
2259
2492
|
var SLA_ACTOR_ID = "system:sla";
|
|
2493
|
+
var ESCALATION_ENABLED_FLIP_CUTOFF_MS = Date.parse("2026-08-28T00:00:00Z");
|
|
2260
2494
|
var DEAD_RUN_ACTOR_ID = "system:dead-run";
|
|
2261
2495
|
var TERMINAL_RUN_STATUSES = /* @__PURE__ */ new Set([
|
|
2262
2496
|
"completed",
|
|
@@ -2265,8 +2499,37 @@ var TERMINAL_RUN_STATUSES = /* @__PURE__ */ new Set([
|
|
|
2265
2499
|
"timed_out"
|
|
2266
2500
|
]);
|
|
2267
2501
|
var STRANDABLE_REQUEST_STATUSES = ["approved", "rejected", "returned"];
|
|
2502
|
+
function classifyStrandedRunState(run) {
|
|
2503
|
+
if (!run) return "missing";
|
|
2504
|
+
switch (run.status) {
|
|
2505
|
+
// The resume consumed the pause and a downstream node threw. Reported.
|
|
2506
|
+
case "failed":
|
|
2507
|
+
return "failed";
|
|
2508
|
+
// ── The negatives, each for its own reason ──────────────────────────────
|
|
2509
|
+
// The decision advanced the flow and the flow finished. Healthy.
|
|
2510
|
+
case "completed":
|
|
2511
|
+
return void 0;
|
|
2512
|
+
// Deliberately terminated by an operator (`cancelRun`, ADR-0044). The run
|
|
2513
|
+
// stopping is the intended outcome, exactly as `recalled` is on the request
|
|
2514
|
+
// side — reporting it would bury the real findings under expected ones.
|
|
2515
|
+
case "cancelled":
|
|
2516
|
+
return void 0;
|
|
2517
|
+
// The history's last row says `paused` while the suspension store says no
|
|
2518
|
+
// live pause. That is AMBIGUOUS, and the ambiguity is not resolvable from
|
|
2519
|
+
// one scan: a resume in flight right now has consumed the suspension and
|
|
2520
|
+
// not yet written its terminal row, and reads exactly like a process that
|
|
2521
|
+
// died in the same window. Condemning it would name every concurrently
|
|
2522
|
+
// resuming approval — so this stays SKIPPED, the conservative arm this
|
|
2523
|
+
// whole method is built on.
|
|
2524
|
+
case "paused":
|
|
2525
|
+
return void 0;
|
|
2526
|
+
default:
|
|
2527
|
+
return void 0;
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2268
2530
|
var ACTION_TOKEN_TTL_MS = 72 * 60 * 60 * 1e3;
|
|
2269
2531
|
var SYSTEM_CTX2 = { isSystem: true, positions: [], permissions: [] };
|
|
2532
|
+
var RECORD_DELETE_CANCEL_LIMIT = 200;
|
|
2270
2533
|
function actingUserId(context) {
|
|
2271
2534
|
const userId = context?.userId;
|
|
2272
2535
|
return typeof userId === "string" && userId ? userId : null;
|
|
@@ -2327,6 +2590,10 @@ function rowFromRequest(row) {
|
|
|
2327
2590
|
submitter_id: row.submitter_id ?? void 0,
|
|
2328
2591
|
submitter_comment: row.submitter_comment ?? void 0,
|
|
2329
2592
|
status: row.status ?? "pending",
|
|
2593
|
+
// [#13568] Why a cancelled row was cancelled. Surfaced on every read for
|
|
2594
|
+
// the same reason `status` is: a client that can see the terminal state and
|
|
2595
|
+
// not the machine-readable cause is back to guessing from prose.
|
|
2596
|
+
cancel_reason: row.cancel_reason ?? void 0,
|
|
2330
2597
|
current_step: row.current_step ?? void 0,
|
|
2331
2598
|
current_step_index: row.current_step_index ?? void 0,
|
|
2332
2599
|
pending_approvers: csvSplit(row.pending_approvers),
|
|
@@ -2419,6 +2686,7 @@ function rowFromAction(row) {
|
|
|
2419
2686
|
var _ApprovalService = class _ApprovalService {
|
|
2420
2687
|
constructor(opts) {
|
|
2421
2688
|
this.engine = opts.engine;
|
|
2689
|
+
this.recordOrgResolver = createRecordOrganizationResolver(opts.engine);
|
|
2422
2690
|
this.clock = opts.clock ?? { now: () => /* @__PURE__ */ new Date() };
|
|
2423
2691
|
this.logger = opts.logger;
|
|
2424
2692
|
this.automation = opts.automation;
|
|
@@ -2426,6 +2694,7 @@ var _ApprovalService = class _ApprovalService {
|
|
|
2426
2694
|
this.publicBaseUrl = (opts.publicBaseUrl ?? "").replace(/\/$/, "");
|
|
2427
2695
|
this.tenancyPosture = opts.tenancyPosture;
|
|
2428
2696
|
this.fieldVisibility = opts.fieldVisibility;
|
|
2697
|
+
this.messageTranslator = opts.messageTranslator;
|
|
2429
2698
|
this.recordReaderVisibleObjects = new Set(
|
|
2430
2699
|
(Array.isArray(opts.recordReaderVisibleObjects) ? opts.recordReaderVisibleObjects : []).map((n) => String(n ?? "").trim()).filter(Boolean)
|
|
2431
2700
|
);
|
|
@@ -2578,6 +2847,41 @@ var _ApprovalService = class _ApprovalService {
|
|
|
2578
2847
|
const actorTenant = context.tenantId ?? context.organizationId ?? null;
|
|
2579
2848
|
return requestOrg == null || actorTenant != null && String(requestOrg) === String(actorTenant);
|
|
2580
2849
|
}
|
|
2850
|
+
/**
|
|
2851
|
+
* [#11993] The END USER's half of an approval refusal.
|
|
2852
|
+
*
|
|
2853
|
+
* `handleApprovalError` in `@objectstack/rest` maps this service's
|
|
2854
|
+
* `CODE: message` throws onto the wire by testing the prefix for the status
|
|
2855
|
+
* and then STRIPPING it (a leading run of `[A-Z_]` plus a colon and any
|
|
2856
|
+
* following whitespace), shipping what remains as the body's
|
|
2857
|
+
* human-readable `error` — which Console splices under its own localized
|
|
2858
|
+
* label ("撤回审批失败: …"). A hardcoded English reason therefore reaches an
|
|
2859
|
+
* operator in a fully Chinese deployment as a Chinese prefix glued onto an
|
|
2860
|
+
* English sentence they cannot act on.
|
|
2861
|
+
*
|
|
2862
|
+
* Rendered through the SHARED Operation Message Catalog
|
|
2863
|
+
* (`@objectstack/spec/system`), not a second mechanism: same
|
|
2864
|
+
* `errors.<key>` override address, same resolution ladder (deployment
|
|
2865
|
+
* override -> locale catalog -> `en` -> the key), same guarantee that a
|
|
2866
|
+
* misbehaving i18n service cannot turn a 403 into a 500. The catalog's
|
|
2867
|
+
* header names this seat explicitly; `plugin-security`'s
|
|
2868
|
+
* `userFacingDenialMessage` is the sibling consumer this mirrors.
|
|
2869
|
+
*
|
|
2870
|
+
* The CODE PREFIX stays on the message. It is not user copy — it is how the
|
|
2871
|
+
* REST layer derives the status and the ADR-0112 wire code, and it is
|
|
2872
|
+
* stripped before the sentence reaches a body. The developer's half moves
|
|
2873
|
+
* to the log, where the ids it names are legible to an operator and to
|
|
2874
|
+
* nobody else.
|
|
2875
|
+
*/
|
|
2876
|
+
userFacingRefusal(messageKey, context) {
|
|
2877
|
+
let translate;
|
|
2878
|
+
try {
|
|
2879
|
+
translate = this.messageTranslator?.();
|
|
2880
|
+
} catch {
|
|
2881
|
+
translate = void 0;
|
|
2882
|
+
}
|
|
2883
|
+
return renderOperationMessage({ messageKey }, { locale: context?.locale, translate });
|
|
2884
|
+
}
|
|
2581
2885
|
/**
|
|
2582
2886
|
* Pin the acting identity to the AUTHENTICATED CALLER (#3800).
|
|
2583
2887
|
*
|
|
@@ -3518,11 +3822,13 @@ var _ApprovalService = class _ApprovalService {
|
|
|
3518
3822
|
const substitutions = [];
|
|
3519
3823
|
const groups = {};
|
|
3520
3824
|
const liveRecord = await this.loadLiveRecord(input.object, input.recordId, input.record);
|
|
3825
|
+
const subjectOrg = this.recordOrgResolver.organizationOf(input.object, liveRecord, input.record);
|
|
3826
|
+
const requestOrg = subjectOrg ?? ctxOrg;
|
|
3521
3827
|
const resolvedFrom = {};
|
|
3522
3828
|
const approvers = await this.expandApprovers(
|
|
3523
3829
|
{ approvers: input.config.approvers },
|
|
3524
3830
|
liveRecord,
|
|
3525
|
-
|
|
3831
|
+
requestOrg,
|
|
3526
3832
|
{
|
|
3527
3833
|
now: nowDate.getTime(),
|
|
3528
3834
|
substitutions,
|
|
@@ -3586,16 +3892,16 @@ var _ApprovalService = class _ApprovalService {
|
|
|
3586
3892
|
flow_run_id: input.runId,
|
|
3587
3893
|
flow_node_id: input.nodeId,
|
|
3588
3894
|
node_config_json: JSON.stringify(configSnapshot),
|
|
3589
|
-
organization_id:
|
|
3895
|
+
organization_id: requestOrg,
|
|
3590
3896
|
created_at: now,
|
|
3591
3897
|
updated_at: now
|
|
3592
3898
|
};
|
|
3593
3899
|
await this.engine.insert("sys_approval_request", row, { context: SYSTEM_CTX2 });
|
|
3594
|
-
await this.syncApproverIndex(id, approvers,
|
|
3900
|
+
await this.syncApproverIndex(id, approvers, requestOrg, now);
|
|
3595
3901
|
await this.engine.insert("sys_approval_action", {
|
|
3596
3902
|
id: uid("aact"),
|
|
3597
3903
|
request_id: id,
|
|
3598
|
-
organization_id:
|
|
3904
|
+
organization_id: requestOrg,
|
|
3599
3905
|
step_name: input.nodeId,
|
|
3600
3906
|
step_index: 0,
|
|
3601
3907
|
action: "submit",
|
|
@@ -3607,7 +3913,7 @@ var _ApprovalService = class _ApprovalService {
|
|
|
3607
3913
|
await this.engine.insert("sys_approval_action", {
|
|
3608
3914
|
id: uid("aact"),
|
|
3609
3915
|
request_id: id,
|
|
3610
|
-
organization_id:
|
|
3916
|
+
organization_id: requestOrg,
|
|
3611
3917
|
step_name: input.nodeId,
|
|
3612
3918
|
step_index: 0,
|
|
3613
3919
|
action: "ooo_substitute",
|
|
@@ -4049,6 +4355,13 @@ var _ApprovalService = class _ApprovalService {
|
|
|
4049
4355
|
* is then paused at the revise-window node (no reject edge), so it is
|
|
4050
4356
|
* terminally cancelled via {@link ApprovalResumeSurface.cancelRun} rather
|
|
4051
4357
|
* than resumed.
|
|
4358
|
+
*
|
|
4359
|
+
* The #3424 privileged override reaches a PENDING request only (#12775,
|
|
4360
|
+
* maintainer ruling 2026-09-02). On `returned` an override actor is refused
|
|
4361
|
+
* exactly as any other non-submitter: the gate is spelled as `attachViewers`
|
|
4362
|
+
* computes `viewer.can_override`, so the gate, the
|
|
4363
|
+
* {@link ApprovalService.isOverrideActor} doc block and the viewer flag agree
|
|
4364
|
+
* at one point.
|
|
4052
4365
|
*/
|
|
4053
4366
|
async recall(requestId, input, context) {
|
|
4054
4367
|
if (!requestId) throw new Error("VALIDATION_FAILED: requestId is required");
|
|
@@ -4064,8 +4377,18 @@ var _ApprovalService = class _ApprovalService {
|
|
|
4064
4377
|
if (raw.status !== "pending" && !inReviseWindow) {
|
|
4065
4378
|
throw new Error(`INVALID_STATE: request is ${raw.status}`);
|
|
4066
4379
|
}
|
|
4067
|
-
|
|
4068
|
-
|
|
4380
|
+
const overrideAdmits = raw.status === "pending" && this.isOverrideActor(context, raw.organization_id ?? null);
|
|
4381
|
+
if (!overrideAdmits && raw.submitter_id && String(raw.submitter_id) !== String(actorId)) {
|
|
4382
|
+
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)`;
|
|
4383
|
+
this.logger?.warn?.(developerMessage, {
|
|
4384
|
+
request: requestId,
|
|
4385
|
+
actor: actorId,
|
|
4386
|
+
submitter: String(raw.submitter_id),
|
|
4387
|
+
status: raw.status
|
|
4388
|
+
});
|
|
4389
|
+
throw new Error(
|
|
4390
|
+
`FORBIDDEN: ${this.userFacingRefusal("approval_recall_not_submitter", context)}`
|
|
4391
|
+
);
|
|
4069
4392
|
}
|
|
4070
4393
|
if (inReviseWindow) await this.assertLatestForRun(raw);
|
|
4071
4394
|
const config = parseJson(raw.node_config_json, { approvers: [], behavior: "first_response" });
|
|
@@ -4141,6 +4464,127 @@ var _ApprovalService = class _ApprovalService {
|
|
|
4141
4464
|
const fresh = await this.readBackRequest(requestId, context);
|
|
4142
4465
|
return { request: fresh, runId, resumed, ...resumeError ? { resumeError } : {} };
|
|
4143
4466
|
}
|
|
4467
|
+
// ── Record-delete lifecycle linkage (#13568) ─────────────────
|
|
4468
|
+
/**
|
|
4469
|
+
* Void every `pending` request about a record that has just been DELETED
|
|
4470
|
+
* (#13568, maintainer ruling 2026-08-31 「同意」).
|
|
4471
|
+
*
|
|
4472
|
+
* ## What the ruling asked for, and what each half costs
|
|
4473
|
+
*
|
|
4474
|
+
* status → `cancelled`, a machine-readable `cancel_reason`, the row KEPT
|
|
4475
|
+
* for audit, and the request out of the pending count and the inbox's
|
|
4476
|
+
* default view.
|
|
4477
|
+
*
|
|
4478
|
+
* The last clause needs both writes below, not just the status one: the
|
|
4479
|
+
* inbox's "My Pending" tab pages the request table by `status`, but the
|
|
4480
|
+
* approver filter resolves through the normalized `sys_approval_approver`
|
|
4481
|
+
* index (#1745), which is keyed on nothing but `request_id`. Leaving the
|
|
4482
|
+
* index rows behind would keep the request in `approverRequestIds`' answer
|
|
4483
|
+
* for every approver forever — so {@link syncApproverIndex} clears them, the
|
|
4484
|
+
* same call every other exit from `pending` makes.
|
|
4485
|
+
*
|
|
4486
|
+
* ## Three things this deliberately does NOT do
|
|
4487
|
+
*
|
|
4488
|
+
* 1. **No status mirror-back.** Every other terminal transition calls
|
|
4489
|
+
* {@link mirrorStatusField} to write the outcome onto the subject record's
|
|
4490
|
+
* `approvalStatusField`. Here the subject record is precisely what no
|
|
4491
|
+
* longer exists, so that write is guaranteed to fail — it is the very
|
|
4492
|
+
* `update_record(...) failed: Record ... not found` this card's own
|
|
4493
|
+
* forensics recorded on the reject door. Skipped by construction, not by a
|
|
4494
|
+
* swallowed error.
|
|
4495
|
+
*
|
|
4496
|
+
* 2. **No flow resume, and no run cancel.** A cancellation is a status write
|
|
4497
|
+
* plus a reason; it is not a decision, so there is no branch to resume
|
|
4498
|
+
* down and no downstream node that should run. The suspended run this
|
|
4499
|
+
* request gated is reported (below) and left alone — what becomes of an
|
|
4500
|
+
* approval run whose request was voided is a lifecycle question that
|
|
4501
|
+
* belongs to the automation service, not to a delete hook, and answering
|
|
4502
|
+
* it here by guessing would be exactly the kind of consumer-side
|
|
4503
|
+
* accommodation PD #12 refuses.
|
|
4504
|
+
*
|
|
4505
|
+
* 3. **No refusal of the delete.** The delete has already landed by the time
|
|
4506
|
+
* this runs, and the "forbid delete while an approval is pending"
|
|
4507
|
+
* direction was VETOED in the same ruling (`lockRecord` already blocks the
|
|
4508
|
+
* edit; blocking the delete too locks an author onto a record they cannot
|
|
4509
|
+
* fix). So nothing here throws — a failure degrades to a warning and the
|
|
4510
|
+
* stale row, which is the pre-existing state, never to a failed delete.
|
|
4511
|
+
*
|
|
4512
|
+
* Terminal rows are untouched: the `where` below names `status: 'pending'`,
|
|
4513
|
+
* so an `approved` / `rejected` / `recalled` / `returned` request about the
|
|
4514
|
+
* same record keeps its recorded outcome. That is the ruling's second half —
|
|
4515
|
+
* history is kept; only the presentation of its dead record reference is
|
|
4516
|
+
* someone else's card.
|
|
4517
|
+
*
|
|
4518
|
+
* @returns the request ids cancelled and the suspended run ids left behind.
|
|
4519
|
+
*/
|
|
4520
|
+
async cancelForDeletedRecord(objectName, recordId) {
|
|
4521
|
+
const empty = { cancelled: [], suspendedRuns: [] };
|
|
4522
|
+
const object = String(objectName ?? "").trim();
|
|
4523
|
+
const record = String(recordId ?? "").trim();
|
|
4524
|
+
if (!object || !record) return empty;
|
|
4525
|
+
let pending;
|
|
4526
|
+
try {
|
|
4527
|
+
const rows = await this.engine.find("sys_approval_request", {
|
|
4528
|
+
where: { object_name: object, record_id: record, status: "pending" },
|
|
4529
|
+
limit: RECORD_DELETE_CANCEL_LIMIT,
|
|
4530
|
+
context: SYSTEM_CTX2
|
|
4531
|
+
});
|
|
4532
|
+
pending = Array.isArray(rows) ? rows : [];
|
|
4533
|
+
} catch (err) {
|
|
4534
|
+
this.logger?.warn?.(
|
|
4535
|
+
"[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",
|
|
4536
|
+
{ object, record, error: err?.message ?? String(err) }
|
|
4537
|
+
);
|
|
4538
|
+
return empty;
|
|
4539
|
+
}
|
|
4540
|
+
if (pending.length === 0) return empty;
|
|
4541
|
+
const now = this.clock.now().toISOString();
|
|
4542
|
+
const cancelled = [];
|
|
4543
|
+
const suspendedRuns = [];
|
|
4544
|
+
for (const raw of pending) {
|
|
4545
|
+
const requestId = String(raw?.id ?? "");
|
|
4546
|
+
if (!requestId) continue;
|
|
4547
|
+
const org = raw?.organization_id ?? null;
|
|
4548
|
+
const nodeId = raw?.flow_node_id ?? raw?.current_step ?? null;
|
|
4549
|
+
try {
|
|
4550
|
+
await this.engine.insert("sys_approval_action", {
|
|
4551
|
+
id: uid("aact"),
|
|
4552
|
+
request_id: requestId,
|
|
4553
|
+
organization_id: org,
|
|
4554
|
+
step_name: nodeId,
|
|
4555
|
+
step_index: 0,
|
|
4556
|
+
action: "cancel",
|
|
4557
|
+
actor_id: null,
|
|
4558
|
+
comment: `Auto-cancelled: the ${object} record '${record}' this request is about was deleted`,
|
|
4559
|
+
created_at: now
|
|
4560
|
+
}, { context: SYSTEM_CTX2 });
|
|
4561
|
+
await this.engine.update("sys_approval_request", {
|
|
4562
|
+
id: requestId,
|
|
4563
|
+
status: "cancelled",
|
|
4564
|
+
cancel_reason: "record_deleted",
|
|
4565
|
+
pending_approvers: null,
|
|
4566
|
+
completed_at: now,
|
|
4567
|
+
updated_at: now
|
|
4568
|
+
}, { context: SYSTEM_CTX2 });
|
|
4569
|
+
await this.syncApproverIndex(requestId, [], org, now);
|
|
4570
|
+
cancelled.push(requestId);
|
|
4571
|
+
const runId = raw?.flow_run_id ? String(raw.flow_run_id) : "";
|
|
4572
|
+
if (runId) suspendedRuns.push(runId);
|
|
4573
|
+
} catch (err) {
|
|
4574
|
+
this.logger?.warn?.(
|
|
4575
|
+
"[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",
|
|
4576
|
+
{ object, record, request: requestId, error: err?.message ?? String(err) }
|
|
4577
|
+
);
|
|
4578
|
+
}
|
|
4579
|
+
}
|
|
4580
|
+
if (suspendedRuns.length) {
|
|
4581
|
+
this.logger?.warn?.(
|
|
4582
|
+
"[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",
|
|
4583
|
+
{ object, record, requests: cancelled, runs: suspendedRuns }
|
|
4584
|
+
);
|
|
4585
|
+
}
|
|
4586
|
+
return { cancelled, suspendedRuns };
|
|
4587
|
+
}
|
|
4144
4588
|
// ── Send back for revision / resubmit (ADR-0044) ─────────────
|
|
4145
4589
|
/**
|
|
4146
4590
|
* ADR-0044 send back for revision. Finalises the pending request as
|
|
@@ -4771,7 +5215,10 @@ var _ApprovalService = class _ApprovalService {
|
|
|
4771
5215
|
* `escalation.timeoutHours` and whose deadline has passed is escalated
|
|
4772
5216
|
* **at most once, ever** — the `escalate` audit row is the idempotency
|
|
4773
5217
|
* marker, written before any mutation (audit-first, like reassign). One
|
|
4774
|
-
* bad row never stops the sweep.
|
|
5218
|
+
* bad row never stops the sweep. A block that explicitly declares
|
|
5219
|
+
* `enabled: false` is skipped (#12278 — the declared switch is enforced;
|
|
5220
|
+
* pre-flip snapshots ride the legacy window on
|
|
5221
|
+
* {@link ESCALATION_ENABLED_FLIP_CUTOFF_MS}).
|
|
4775
5222
|
*/
|
|
4776
5223
|
async runEscalations() {
|
|
4777
5224
|
let rows = [];
|
|
@@ -4793,6 +5240,7 @@ var _ApprovalService = class _ApprovalService {
|
|
|
4793
5240
|
const cfg = parseJson(raw.node_config_json, void 0);
|
|
4794
5241
|
const esc2 = cfg?.escalation;
|
|
4795
5242
|
if (!esc2 || typeof esc2.timeoutHours !== "number" || esc2.timeoutHours <= 0) continue;
|
|
5243
|
+
if (esc2.enabled === false && Date.parse(raw.created_at ?? "") >= ESCALATION_ENABLED_FLIP_CUTOFF_MS) continue;
|
|
4796
5244
|
const due = slaDueAt(raw.created_at, cfg);
|
|
4797
5245
|
if (!due || Date.parse(due) > this.clock.now().getTime()) continue;
|
|
4798
5246
|
const prior = await this.engine.find("sys_approval_action", {
|
|
@@ -4873,9 +5321,35 @@ var _ApprovalService = class _ApprovalService {
|
|
|
4873
5321
|
* live pause exists. It THROWS when the store cannot be read, and that
|
|
4874
5322
|
* case is SKIPPED, never counted as dead: an unreadable store means
|
|
4875
5323
|
* "unknown", and a storage outage must not be published as a lost run.
|
|
4876
|
-
* - `getRun(runId)
|
|
4877
|
-
* prefixed rows in `sys_automation_run`). A run
|
|
4878
|
-
* not stranded; a request whose run neither waits
|
|
5324
|
+
* - `classifyStrandedRunState` over `getRun(runId)` — the run's own
|
|
5325
|
+
* history row (the `run_` prefixed rows in `sys_automation_run`). A run
|
|
5326
|
+
* that merely finished is not stranded; a request whose run neither waits
|
|
5327
|
+
* nor completed is.
|
|
5328
|
+
*
|
|
5329
|
+
* **The second oracle was widened (#13909), and this is the whole point of
|
|
5330
|
+
* that card's first slice.** It used to be `if (terminal) continue` — the
|
|
5331
|
+
* existence of ANY history row ended the check, on the reading "the run ran to
|
|
5332
|
+
* a terminal state, it is not dangling". That is true of a run that COMPLETED
|
|
5333
|
+
* and false of one that FAILED: the engine consumes a suspension *before*
|
|
5334
|
+
* running the downstream nodes (`AutomationEngine.resumeInternal` calls
|
|
5335
|
+
* `forgetSuspendedRun(run, 'resumed')` and only then `traverseNext`), so a
|
|
5336
|
+
* downstream node that merely threw threw with the pause already gone, and the
|
|
5337
|
+
* catch arm wrote a terminal `failed` row. The decision is durable, the
|
|
5338
|
+
* continuation stopped half-way, `resume` answers `RUN_NOT_FOUND` and
|
|
5339
|
+
* `cancelRun` is a no-op — and the terminal row this oracle used to read as
|
|
5340
|
+
* health is written BY the very failure that stranded it. So this inspection
|
|
5341
|
+
* reported `0` for the one shape an operator most needs to see.
|
|
5342
|
+
*
|
|
5343
|
+
* ⚠️ The widening does NOT reverse the conservatism: `completed`, `cancelled`
|
|
5344
|
+
* and `paused` are each still skipped, for reasons named one at a time in
|
|
5345
|
+
* `classifyStrandedRunState`, and an unrecognised status is skipped too.
|
|
5346
|
+
* What the widening buys is that a `failed` run is now reported with
|
|
5347
|
+
* `runState: 'failed'` instead of counted as healthy.
|
|
5348
|
+
*
|
|
5349
|
+
* ⚠️ **What this can and cannot size.** It makes the condition *visible* in a
|
|
5350
|
+
* deployment; it is not itself a census, and it says nothing about this
|
|
5351
|
+
* repository. How many runs are already in this state can only be answered
|
|
5352
|
+
* against a real deployment's tables — see the card.
|
|
4879
5353
|
*
|
|
4880
5354
|
* **Reports; never rewrites.** No status is changed and no run is cancelled.
|
|
4881
5355
|
* The decision genuinely happened — a human approved or rejected — and
|
|
@@ -4933,7 +5407,8 @@ var _ApprovalService = class _ApprovalService {
|
|
|
4933
5407
|
});
|
|
4934
5408
|
continue;
|
|
4935
5409
|
}
|
|
4936
|
-
|
|
5410
|
+
const runState = classifyStrandedRunState(terminal);
|
|
5411
|
+
if (!runState) continue;
|
|
4937
5412
|
const config = parseJson(
|
|
4938
5413
|
raw.node_config_json,
|
|
4939
5414
|
{ approvers: [], behavior: "first_response" }
|
|
@@ -4956,6 +5431,7 @@ var _ApprovalService = class _ApprovalService {
|
|
|
4956
5431
|
requestId: String(raw.id),
|
|
4957
5432
|
status: raw.status,
|
|
4958
5433
|
runId,
|
|
5434
|
+
runState,
|
|
4959
5435
|
flowName: typeof raw.process_name === "string" ? raw.process_name.replace(/^flow:/, "") : void 0,
|
|
4960
5436
|
nodeId: raw.flow_node_id ?? raw.current_step ?? void 0,
|
|
4961
5437
|
objectName: raw.object_name,
|
|
@@ -4967,11 +5443,13 @@ var _ApprovalService = class _ApprovalService {
|
|
|
4967
5443
|
});
|
|
4968
5444
|
}
|
|
4969
5445
|
if (stranded.length || undetermined) {
|
|
4970
|
-
this.logger?.warn?.("[approvals] stranded terminal requests (decision recorded, flow run
|
|
5446
|
+
this.logger?.warn?.("[approvals] stranded terminal requests (decision recorded, flow run unrecoverable)", {
|
|
4971
5447
|
scanned: rows.length,
|
|
4972
5448
|
stranded: stranded.length,
|
|
4973
5449
|
undetermined,
|
|
4974
|
-
|
|
5450
|
+
runMissing: stranded.filter((s) => s.runState === "missing").length,
|
|
5451
|
+
runFailed: stranded.filter((s) => s.runState === "failed").length,
|
|
5452
|
+
requests: stranded.map((s) => `${s.requestId}@${s.nodeId ?? "?"} \u2192 run ${s.runId} (${s.runState})`)
|
|
4975
5453
|
});
|
|
4976
5454
|
}
|
|
4977
5455
|
return { scanned: rows.length, stranded, undetermined };
|
|
@@ -5825,9 +6303,28 @@ var _ApprovalService = class _ApprovalService {
|
|
|
5825
6303
|
* flow-driven resume, a service-to-service call), turning a successful write
|
|
5826
6304
|
* into a `null` result. Gating belongs on the read API, not on an
|
|
5827
6305
|
* operation's own return value.
|
|
6306
|
+
*
|
|
6307
|
+
* [#12769] The read still narrows by the CALLER's organization
|
|
6308
|
+
* (`loadRequest` — a deliberate tenancy wall, untouched here), and an
|
|
6309
|
+
* org-less request row is invisible inside an org-scoped caller's narrowing.
|
|
6310
|
+
* Org-less rows arise by construction on every schedule / time-relative /
|
|
6311
|
+
* api trigger run (#10131; #9132 pinned that behaviour rather than repairing
|
|
6312
|
+
* it), so this is a live state, not an edge case: the write IS recorded and
|
|
6313
|
+
* the echo cannot be built. It used to escape as `null` behind the callers'
|
|
6314
|
+
* `fresh!` non-null assertions — a well-formed 200 whose declared-non-null
|
|
6315
|
+
* `request` was `null` on the wire. Now it refuses loudly instead, which
|
|
6316
|
+
* keeps every declared result type true: a result that cannot be built is
|
|
6317
|
+
* never returned. Callers must NOT catch this to fall back to `null` —
|
|
6318
|
+
* that would re-open the type-lie one level up.
|
|
5828
6319
|
*/
|
|
5829
6320
|
async readBackRequest(requestId, context) {
|
|
5830
|
-
|
|
6321
|
+
const fresh = await this.loadRequest(requestId, context, false);
|
|
6322
|
+
if (!fresh) {
|
|
6323
|
+
throw new Error(
|
|
6324
|
+
`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.`
|
|
6325
|
+
);
|
|
6326
|
+
}
|
|
6327
|
+
return fresh;
|
|
5831
6328
|
}
|
|
5832
6329
|
async getRequest(requestId, context) {
|
|
5833
6330
|
return this.loadRequest(requestId, context, true);
|
|
@@ -6319,6 +6816,41 @@ function bindApprovalLockHook(engine, logger) {
|
|
|
6319
6816
|
}, { packageId: APPROVALS_HOOK_PACKAGE, priority: 50 });
|
|
6320
6817
|
logger?.info?.("[approvals] record-lock hook bound");
|
|
6321
6818
|
}
|
|
6819
|
+
var APPROVALS_OWN_OBJECTS = [
|
|
6820
|
+
"sys_approval_request",
|
|
6821
|
+
"sys_approval_action",
|
|
6822
|
+
"sys_approval_approver",
|
|
6823
|
+
"sys_approval_token",
|
|
6824
|
+
"sys_approval_delegation"
|
|
6825
|
+
];
|
|
6826
|
+
function bindRecordDeleteCancelHook(engine, service, logger) {
|
|
6827
|
+
engine.registerHook("afterDelete", async (ctx) => {
|
|
6828
|
+
const object = ctx?.object ?? ctx?.objectName;
|
|
6829
|
+
if (!object) return;
|
|
6830
|
+
const raw = ctx?.previous?.id ?? ctx?.input?.id;
|
|
6831
|
+
const recordId = raw == null ? "" : String(raw);
|
|
6832
|
+
if (!recordId) {
|
|
6833
|
+
logger?.warn?.(
|
|
6834
|
+
"[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",
|
|
6835
|
+
{ object }
|
|
6836
|
+
);
|
|
6837
|
+
return;
|
|
6838
|
+
}
|
|
6839
|
+
try {
|
|
6840
|
+
await service.cancelForDeletedRecord(String(object), recordId);
|
|
6841
|
+
} catch (err) {
|
|
6842
|
+
logger?.warn?.(
|
|
6843
|
+
"[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",
|
|
6844
|
+
{ object, record: recordId, error: err?.message ?? String(err) }
|
|
6845
|
+
);
|
|
6846
|
+
}
|
|
6847
|
+
}, {
|
|
6848
|
+
excludeObjects: [...APPROVALS_OWN_OBJECTS],
|
|
6849
|
+
packageId: APPROVALS_HOOK_PACKAGE,
|
|
6850
|
+
priority: 50
|
|
6851
|
+
});
|
|
6852
|
+
logger?.info?.("[approvals] record-delete cancel hook bound");
|
|
6853
|
+
}
|
|
6322
6854
|
var DELEGATION_OBJECT = "sys_approval_delegation";
|
|
6323
6855
|
function bindDelegationWriteGuard(engine, logger) {
|
|
6324
6856
|
const makeGuard = (isInsert) => async (ctx) => {
|
|
@@ -6670,6 +7202,23 @@ var ApprovalsServicePlugin = class {
|
|
|
6670
7202
|
} catch {
|
|
6671
7203
|
return void 0;
|
|
6672
7204
|
}
|
|
7205
|
+
},
|
|
7206
|
+
// [#11993] Deployment override lookup for user-facing refusal copy.
|
|
7207
|
+
// Resolved LAZILY for the reason the two providers above are: the i18n
|
|
7208
|
+
// service is contributed by another plugin (ADR-0029 D8) and may start
|
|
7209
|
+
// after this one. Without it the service still renders the built-in
|
|
7210
|
+
// catalog in the caller's locale; with it, a deployment's own
|
|
7211
|
+
// `translation` for `errors.approval_recall_not_submitter` wins — the
|
|
7212
|
+
// override address the catalog documents, made real for this emitter.
|
|
7213
|
+
messageTranslator: () => {
|
|
7214
|
+
try {
|
|
7215
|
+
const i18n = ctx.getService("i18n");
|
|
7216
|
+
const t = i18n?.t;
|
|
7217
|
+
if (typeof t !== "function") return void 0;
|
|
7218
|
+
return (key, locale, params) => t.call(i18n, key, locale, params);
|
|
7219
|
+
} catch {
|
|
7220
|
+
return void 0;
|
|
7221
|
+
}
|
|
6673
7222
|
}
|
|
6674
7223
|
});
|
|
6675
7224
|
const fieldVisibility = () => {
|
|
@@ -6690,6 +7239,7 @@ var ApprovalsServicePlugin = class {
|
|
|
6690
7239
|
try {
|
|
6691
7240
|
unbindAllHooks(engine);
|
|
6692
7241
|
bindApprovalLockHook(engine, ctx.logger);
|
|
7242
|
+
bindRecordDeleteCancelHook(engine, this.service, ctx.logger);
|
|
6693
7243
|
bindDelegationWriteGuard(engine, ctx.logger);
|
|
6694
7244
|
bindSnapshotRedactionMiddleware(engine, fieldVisibility, ctx.logger);
|
|
6695
7245
|
} catch (err) {
|