@objectstack/plugin-approvals 16.0.0-rc.0 → 16.0.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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +220 -0
- package/dist/index.d.mts +44 -22
- package/dist/index.d.ts +44 -22
- package/dist/index.js +51 -21
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +51 -21
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -7
- package/src/approval-service.test.ts +27 -0
- package/src/approval-service.ts +22 -0
- package/src/sys-approval-approver.object.ts +1 -1
- package/src/sys-approval-request.object.test.ts +14 -5
- package/src/sys-approval-request.object.ts +26 -17
- package/src/sys-approval-token.object.ts +1 -1
package/dist/index.mjs
CHANGED
|
@@ -855,7 +855,7 @@ var SysApprovalRequest = ObjectSchema.create({
|
|
|
855
855
|
pluralLabel: "Approval Requests",
|
|
856
856
|
icon: "inbox",
|
|
857
857
|
isSystem: true,
|
|
858
|
-
managedBy: "
|
|
858
|
+
managedBy: "engine-owned",
|
|
859
859
|
description: "Live approval instance tracked per submission",
|
|
860
860
|
displayNameField: "id",
|
|
861
861
|
nameField: "id",
|
|
@@ -1043,8 +1043,12 @@ var SysApprovalRequest = ObjectSchema.create({
|
|
|
1043
1043
|
// (and their params) ship as metadata, not as hand-written buttons. Each
|
|
1044
1044
|
// targets the existing approvals REST route; `{id}` resolves from the row
|
|
1045
1045
|
// and `actorId` defaults to the caller server-side. The service remains the
|
|
1046
|
-
// authority on who may act
|
|
1047
|
-
//
|
|
1046
|
+
// authority on who may act; `visible` gates on the server-computed
|
|
1047
|
+
// per-viewer block (#3310): approver actions on `record.viewer.can_act`
|
|
1048
|
+
// (the caller is a current pending approver — same check the service
|
|
1049
|
+
// authorizes a decision with, so position/team approvers resolve correctly),
|
|
1050
|
+
// submitter actions on `record.viewer.is_submitter`. `viewer` is attached by
|
|
1051
|
+
// getRequest/listRequests; where it is absent the predicate fails closed.
|
|
1048
1052
|
actions: [
|
|
1049
1053
|
{
|
|
1050
1054
|
name: "approval_approve",
|
|
@@ -1054,9 +1058,13 @@ var SysApprovalRequest = ObjectSchema.create({
|
|
|
1054
1058
|
method: "POST",
|
|
1055
1059
|
target: "/api/v1/approvals/requests/{id}/approve",
|
|
1056
1060
|
params: [
|
|
1057
|
-
{ name: "comment", label: "Comment", type: "textarea", required: false }
|
|
1061
|
+
{ name: "comment", label: "Comment", type: "textarea", required: false },
|
|
1062
|
+
// Decision attachments (#3266). The console renders `type:'file'` params
|
|
1063
|
+
// through the shared upload widget and POSTs the resolved `attachments:
|
|
1064
|
+
// string[]`; the decision route persists them on `sys_approval_action`.
|
|
1065
|
+
{ name: "attachments", label: "Attachments", type: "file", multiple: true, required: false }
|
|
1058
1066
|
],
|
|
1059
|
-
visible:
|
|
1067
|
+
visible: "record.viewer.can_act",
|
|
1060
1068
|
locations: ["record_section", "list_item"],
|
|
1061
1069
|
successMessage: "Approved.",
|
|
1062
1070
|
refreshAfter: true
|
|
@@ -1069,9 +1077,10 @@ var SysApprovalRequest = ObjectSchema.create({
|
|
|
1069
1077
|
method: "POST",
|
|
1070
1078
|
target: "/api/v1/approvals/requests/{id}/reject",
|
|
1071
1079
|
params: [
|
|
1072
|
-
{ name: "comment", label: "Comment", type: "textarea", required: false }
|
|
1080
|
+
{ name: "comment", label: "Comment", type: "textarea", required: false },
|
|
1081
|
+
{ name: "attachments", label: "Attachments", type: "file", multiple: true, required: false }
|
|
1073
1082
|
],
|
|
1074
|
-
visible:
|
|
1083
|
+
visible: "record.viewer.can_act",
|
|
1075
1084
|
confirmText: "Reject this request? A rejection is final for every approver.",
|
|
1076
1085
|
locations: ["record_section", "list_item"],
|
|
1077
1086
|
successMessage: "Rejected.",
|
|
@@ -1093,15 +1102,15 @@ var SysApprovalRequest = ObjectSchema.create({
|
|
|
1093
1102
|
{ field: "submitter_id", name: "to", label: "New approver", required: true, helpText: "User to hand this step to" },
|
|
1094
1103
|
{ name: "comment", label: "Comment", type: "textarea", required: false }
|
|
1095
1104
|
],
|
|
1096
|
-
visible:
|
|
1105
|
+
visible: "record.viewer.can_act",
|
|
1097
1106
|
locations: ["record_section"],
|
|
1098
1107
|
successMessage: "Reassigned.",
|
|
1099
1108
|
refreshAfter: true
|
|
1100
1109
|
},
|
|
1101
1110
|
// ── Approver secondary decisions ────────────────────────────────
|
|
1102
1111
|
// Send back for revision / request more info (ADR-0044). Both are approver
|
|
1103
|
-
// actions
|
|
1104
|
-
//
|
|
1112
|
+
// actions, so `visible` gates on `record.viewer.can_act` (a current pending
|
|
1113
|
+
// approver) — same as approve/reject. The service stays the authority.
|
|
1105
1114
|
{
|
|
1106
1115
|
name: "approval_send_back",
|
|
1107
1116
|
label: "Send back",
|
|
@@ -1112,7 +1121,7 @@ var SysApprovalRequest = ObjectSchema.create({
|
|
|
1112
1121
|
params: [
|
|
1113
1122
|
{ name: "comment", label: "Reason", type: "textarea", required: false }
|
|
1114
1123
|
],
|
|
1115
|
-
visible:
|
|
1124
|
+
visible: "record.viewer.can_act",
|
|
1116
1125
|
locations: ["record_section"],
|
|
1117
1126
|
successMessage: "Sent back for revision.",
|
|
1118
1127
|
refreshAfter: true
|
|
@@ -1127,17 +1136,17 @@ var SysApprovalRequest = ObjectSchema.create({
|
|
|
1127
1136
|
params: [
|
|
1128
1137
|
{ name: "comment", label: "What do you need?", type: "textarea", required: true }
|
|
1129
1138
|
],
|
|
1130
|
-
visible:
|
|
1139
|
+
visible: "record.viewer.can_act",
|
|
1131
1140
|
locations: ["record_section"],
|
|
1132
1141
|
successMessage: "Information requested.",
|
|
1133
1142
|
refreshAfter: true
|
|
1134
1143
|
},
|
|
1135
1144
|
// ── Submitter continuity actions ────────────────────────────────
|
|
1136
1145
|
// Remind / recall (pending) and resubmit / recall (returned). These are the
|
|
1137
|
-
// submitter's own levers, so `visible` gates on `
|
|
1138
|
-
//
|
|
1139
|
-
//
|
|
1140
|
-
//
|
|
1146
|
+
// submitter's own levers, so `visible` gates on `record.viewer.is_submitter`
|
|
1147
|
+
// (server-computed on the current viewer). The service re-checks ownership;
|
|
1148
|
+
// the predicate keeps a non-submitter from ever seeing a button they cannot
|
|
1149
|
+
// use.
|
|
1141
1150
|
{
|
|
1142
1151
|
name: "approval_remind",
|
|
1143
1152
|
label: "Send reminder",
|
|
@@ -1148,7 +1157,7 @@ var SysApprovalRequest = ObjectSchema.create({
|
|
|
1148
1157
|
params: [
|
|
1149
1158
|
{ name: "comment", label: "Note", type: "textarea", required: false }
|
|
1150
1159
|
],
|
|
1151
|
-
visible: 'record.status == "pending" && record.
|
|
1160
|
+
visible: 'record.status == "pending" && record.viewer.is_submitter',
|
|
1152
1161
|
locations: ["record_section"],
|
|
1153
1162
|
successMessage: "Reminder sent.",
|
|
1154
1163
|
refreshAfter: true
|
|
@@ -1165,7 +1174,7 @@ var SysApprovalRequest = ObjectSchema.create({
|
|
|
1165
1174
|
],
|
|
1166
1175
|
// Recall applies while the request is live for the submitter — pending
|
|
1167
1176
|
// (withdraw) or returned (abandon the revision instead of resubmitting).
|
|
1168
|
-
visible: '(record.status == "pending" || record.status == "returned") && record.
|
|
1177
|
+
visible: '(record.status == "pending" || record.status == "returned") && record.viewer.is_submitter',
|
|
1169
1178
|
confirmText: "Recall this request? Approvers can no longer act on it and the record is unlocked.",
|
|
1170
1179
|
locations: ["record_section"],
|
|
1171
1180
|
successMessage: "Recalled.",
|
|
@@ -1181,7 +1190,7 @@ var SysApprovalRequest = ObjectSchema.create({
|
|
|
1181
1190
|
params: [
|
|
1182
1191
|
{ name: "comment", label: "What changed?", type: "textarea", required: false }
|
|
1183
1192
|
],
|
|
1184
|
-
visible: 'record.status == "returned" && record.
|
|
1193
|
+
visible: 'record.status == "returned" && record.viewer.is_submitter',
|
|
1185
1194
|
locations: ["record_section"],
|
|
1186
1195
|
successMessage: "Resubmitted.",
|
|
1187
1196
|
refreshAfter: true
|
|
@@ -1319,7 +1328,7 @@ var SysApprovalApprover = ObjectSchema3.create({
|
|
|
1319
1328
|
pluralLabel: "Approval Approvers",
|
|
1320
1329
|
icon: "users",
|
|
1321
1330
|
isSystem: true,
|
|
1322
|
-
managedBy: "
|
|
1331
|
+
managedBy: "engine-owned",
|
|
1323
1332
|
description: "Normalized pending-approver rows for indexed inbox queries",
|
|
1324
1333
|
displayNameField: "id",
|
|
1325
1334
|
nameField: "id",
|
|
@@ -3328,6 +3337,7 @@ var _ApprovalService = class _ApprovalService {
|
|
|
3328
3337
|
const rows = await this.engine.find("sys_approval_request", findOpts);
|
|
3329
3338
|
const list = Array.isArray(rows) ? rows.map(rowFromRequest) : [];
|
|
3330
3339
|
await this.enrichRows(list);
|
|
3340
|
+
this.attachViewers(list, context);
|
|
3331
3341
|
return list;
|
|
3332
3342
|
}
|
|
3333
3343
|
async countRequests(filter, context) {
|
|
@@ -3369,6 +3379,7 @@ var _ApprovalService = class _ApprovalService {
|
|
|
3369
3379
|
await this.enrichRows([row]);
|
|
3370
3380
|
await this.attachFlowSteps(row);
|
|
3371
3381
|
await this.attachDecisionProgress(row, rows[0]);
|
|
3382
|
+
this.attachViewers([row], context);
|
|
3372
3383
|
return row;
|
|
3373
3384
|
}
|
|
3374
3385
|
/**
|
|
@@ -3416,6 +3427,25 @@ var _ApprovalService = class _ApprovalService {
|
|
|
3416
3427
|
} catch {
|
|
3417
3428
|
}
|
|
3418
3429
|
}
|
|
3430
|
+
/**
|
|
3431
|
+
* Attach the per-viewer capability block (#3310) from the caller's context.
|
|
3432
|
+
* `can_act` mirrors the exact authorization the decision methods enforce — the
|
|
3433
|
+
* caller's user id is in the resolved `pending_approvers` while the request is
|
|
3434
|
+
* still `pending` (position/team/manager approvers are already resolved to
|
|
3435
|
+
* concrete user ids at open time, so a plain membership test is faithful).
|
|
3436
|
+
* `is_submitter` is a straight owner check. System/tokenless contexts get a
|
|
3437
|
+
* both-false block. Cheap + synchronous — safe on list reads.
|
|
3438
|
+
*/
|
|
3439
|
+
attachViewers(rows, context) {
|
|
3440
|
+
const uid2 = context?.userId != null ? String(context.userId) : null;
|
|
3441
|
+
for (const row of rows) {
|
|
3442
|
+
const pending = row.pending_approvers ?? [];
|
|
3443
|
+
row.viewer = {
|
|
3444
|
+
can_act: row.status === "pending" && !!uid2 && pending.includes(uid2),
|
|
3445
|
+
is_submitter: !!uid2 && row.submitter_id != null && String(row.submitter_id) === uid2
|
|
3446
|
+
};
|
|
3447
|
+
}
|
|
3448
|
+
}
|
|
3419
3449
|
/**
|
|
3420
3450
|
* Derive approval-step progress from the owning flow's graph (single-read
|
|
3421
3451
|
* enrichment only — list reads skip it). Walks from the start node
|
|
@@ -3484,7 +3514,7 @@ var SysApprovalToken = ObjectSchema5.create({
|
|
|
3484
3514
|
pluralLabel: "Approval Action Tokens",
|
|
3485
3515
|
icon: "key",
|
|
3486
3516
|
isSystem: true,
|
|
3487
|
-
managedBy: "
|
|
3517
|
+
managedBy: "engine-owned",
|
|
3488
3518
|
description: "Single-use tokens behind actionable approval links",
|
|
3489
3519
|
displayNameField: "id",
|
|
3490
3520
|
nameField: "id",
|