@objectstack/plugin-approvals 7.2.1 → 7.4.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 +86 -0
- package/dist/index.d.mts +6431 -107
- package/dist/index.d.ts +6431 -107
- package/dist/index.js +1237 -776
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1244 -779
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -7
- package/scripts/i18n-extract.config.ts +32 -0
- package/src/approval-node.test.ts +182 -0
- package/src/approval-node.ts +131 -0
- package/src/approval-service.test.ts +205 -304
- package/src/approval-service.ts +208 -491
- package/src/approvals-plugin.ts +61 -53
- package/src/index.ts +12 -11
- package/src/lifecycle-hooks.ts +67 -202
- package/src/nav-contribution.test.ts +46 -0
- package/src/sys-approval-action.object.ts +120 -0
- package/src/sys-approval-request.object.ts +227 -0
- package/src/translations/en.objects.generated.ts +156 -0
- package/src/translations/es-ES.objects.generated.ts +156 -0
- package/src/translations/index.ts +23 -0
- package/src/translations/ja-JP.objects.generated.ts +156 -0
- package/src/translations/zh-CN.objects.generated.ts +156 -0
- package/src/action-executor.ts +0 -313
- package/src/phase-b.test.ts +0 -263
package/dist/index.mjs
CHANGED
|
@@ -1,207 +1,939 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
SysApprovalAction as SysApprovalAction2
|
|
6
|
-
} from "@objectstack/platform-objects/audit";
|
|
7
|
-
|
|
8
|
-
// src/approval-service.ts
|
|
9
|
-
import { ApprovalProcessSchema } from "@objectstack/spec/automation";
|
|
10
|
-
|
|
11
|
-
// src/action-executor.ts
|
|
12
|
-
var SYSTEM_CTX = { isSystem: true, roles: [], permissions: [] };
|
|
13
|
-
var noopLogger = {
|
|
14
|
-
info: () => {
|
|
15
|
-
},
|
|
16
|
-
warn: () => {
|
|
17
|
-
},
|
|
18
|
-
error: () => {
|
|
19
|
-
},
|
|
20
|
-
debug: () => {
|
|
21
|
-
}
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
22
5
|
};
|
|
23
|
-
var
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// src/translations/en.objects.generated.ts
|
|
12
|
+
var enObjects;
|
|
13
|
+
var init_en_objects_generated = __esm({
|
|
14
|
+
"src/translations/en.objects.generated.ts"() {
|
|
15
|
+
"use strict";
|
|
16
|
+
enObjects = {
|
|
17
|
+
sys_approval_request: {
|
|
18
|
+
label: "Approval Request",
|
|
19
|
+
pluralLabel: "Approval Requests",
|
|
20
|
+
description: "Live approval instance tracked per submission",
|
|
21
|
+
fields: {
|
|
22
|
+
id: {
|
|
23
|
+
label: "Request ID"
|
|
24
|
+
},
|
|
25
|
+
organization_id: {
|
|
26
|
+
label: "Organization",
|
|
27
|
+
help: "Tenant that owns this approval request (propagated from submitter context)"
|
|
28
|
+
},
|
|
29
|
+
process_name: {
|
|
30
|
+
label: "Source",
|
|
31
|
+
help: "Origin of the request \u2014 `flow:<flowName|nodeId>` for node-driven approvals"
|
|
32
|
+
},
|
|
33
|
+
object_name: {
|
|
34
|
+
label: "Object"
|
|
35
|
+
},
|
|
36
|
+
record_id: {
|
|
37
|
+
label: "Record ID"
|
|
38
|
+
},
|
|
39
|
+
submitter_id: {
|
|
40
|
+
label: "Submitter"
|
|
41
|
+
},
|
|
42
|
+
submitter_comment: {
|
|
43
|
+
label: "Submitter Comment"
|
|
44
|
+
},
|
|
45
|
+
status: {
|
|
46
|
+
label: "Status",
|
|
47
|
+
help: "Lifecycle state of the request",
|
|
48
|
+
options: {
|
|
49
|
+
pending: "pending",
|
|
50
|
+
approved: "approved",
|
|
51
|
+
rejected: "rejected",
|
|
52
|
+
recalled: "recalled"
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
current_step: {
|
|
56
|
+
label: "Current Step",
|
|
57
|
+
help: "Machine name of the step awaiting approval"
|
|
58
|
+
},
|
|
59
|
+
current_step_index: {
|
|
60
|
+
label: "Current Step Index"
|
|
61
|
+
},
|
|
62
|
+
pending_approvers: {
|
|
63
|
+
label: "Pending Approvers",
|
|
64
|
+
help: "Comma-separated user ids who can act on the current step"
|
|
65
|
+
},
|
|
66
|
+
payload_json: {
|
|
67
|
+
label: "Snapshot",
|
|
68
|
+
help: "Record snapshot at submission time"
|
|
69
|
+
},
|
|
70
|
+
flow_run_id: {
|
|
71
|
+
label: "Flow Run",
|
|
72
|
+
help: "Suspended automation run id this request gates (ADR-0019). The decision resumes it."
|
|
73
|
+
},
|
|
74
|
+
flow_node_id: {
|
|
75
|
+
label: "Flow Node",
|
|
76
|
+
help: "Approval node id within the flow that opened this request (ADR-0019)."
|
|
77
|
+
},
|
|
78
|
+
node_config_json: {
|
|
79
|
+
label: "Node Config",
|
|
80
|
+
help: "Snapshot of the Approval node config (approvers/behavior) for node-driven requests (ADR-0019)."
|
|
81
|
+
},
|
|
82
|
+
completed_at: {
|
|
83
|
+
label: "Completed At"
|
|
84
|
+
},
|
|
85
|
+
created_at: {
|
|
86
|
+
label: "Created At"
|
|
87
|
+
},
|
|
88
|
+
updated_at: {
|
|
89
|
+
label: "Updated At"
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
_views: {
|
|
93
|
+
my_pending: {
|
|
94
|
+
label: "My Pending"
|
|
95
|
+
},
|
|
96
|
+
submitted_by_me: {
|
|
97
|
+
label: "I Submitted"
|
|
98
|
+
},
|
|
99
|
+
completed: {
|
|
100
|
+
label: "Completed"
|
|
101
|
+
},
|
|
102
|
+
all_requests: {
|
|
103
|
+
label: "All"
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
sys_approval_action: {
|
|
108
|
+
label: "Approval Action",
|
|
109
|
+
pluralLabel: "Approval Actions",
|
|
110
|
+
description: "Append-only audit trail for approval actions",
|
|
111
|
+
fields: {
|
|
112
|
+
id: {
|
|
113
|
+
label: "Action ID"
|
|
114
|
+
},
|
|
115
|
+
organization_id: {
|
|
116
|
+
label: "Organization",
|
|
117
|
+
help: "Tenant that owns this action (mirrors the parent request)"
|
|
118
|
+
},
|
|
119
|
+
request_id: {
|
|
120
|
+
label: "Request"
|
|
121
|
+
},
|
|
122
|
+
step_name: {
|
|
123
|
+
label: "Step",
|
|
124
|
+
help: "Machine name of the step at the time of the action"
|
|
125
|
+
},
|
|
126
|
+
step_index: {
|
|
127
|
+
label: "Step Index"
|
|
128
|
+
},
|
|
129
|
+
action: {
|
|
130
|
+
label: "Action",
|
|
131
|
+
options: {
|
|
132
|
+
submit: "submit",
|
|
133
|
+
approve: "approve",
|
|
134
|
+
reject: "reject",
|
|
135
|
+
recall: "recall",
|
|
136
|
+
escalate: "escalate"
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
actor_id: {
|
|
140
|
+
label: "Actor"
|
|
141
|
+
},
|
|
142
|
+
comment: {
|
|
143
|
+
label: "Comment"
|
|
144
|
+
},
|
|
145
|
+
created_at: {
|
|
146
|
+
label: "Created At"
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
_views: {
|
|
150
|
+
recent: {
|
|
151
|
+
label: "Recent"
|
|
152
|
+
},
|
|
153
|
+
by_actor: {
|
|
154
|
+
label: "By Actor"
|
|
155
|
+
},
|
|
156
|
+
all_actions: {
|
|
157
|
+
label: "All"
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
};
|
|
110
162
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
"
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// src/translations/zh-CN.objects.generated.ts
|
|
166
|
+
var zhCNObjects;
|
|
167
|
+
var init_zh_CN_objects_generated = __esm({
|
|
168
|
+
"src/translations/zh-CN.objects.generated.ts"() {
|
|
169
|
+
"use strict";
|
|
170
|
+
zhCNObjects = {
|
|
171
|
+
sys_approval_request: {
|
|
172
|
+
label: "\u5BA1\u6279\u8BF7\u6C42",
|
|
173
|
+
pluralLabel: "\u5BA1\u6279\u8BF7\u6C42",
|
|
174
|
+
description: "\u6309\u63D0\u4EA4\u8BB0\u5F55\u8DDF\u8E2A\u7684\u5B9E\u65F6\u5BA1\u6279\u5B9E\u4F8B",
|
|
175
|
+
fields: {
|
|
176
|
+
id: {
|
|
177
|
+
label: "\u8BF7\u6C42 ID"
|
|
178
|
+
},
|
|
179
|
+
organization_id: {
|
|
180
|
+
label: "\u7EC4\u7EC7",
|
|
181
|
+
help: "\u62E5\u6709\u8BE5\u5BA1\u6279\u8BF7\u6C42\u7684\u79DF\u6237\uFF08\u4ECE\u63D0\u4EA4\u65B9\u4E0A\u4E0B\u6587\u4F20\u64AD\uFF09"
|
|
182
|
+
},
|
|
183
|
+
process_name: {
|
|
184
|
+
label: "\u6765\u6E90",
|
|
185
|
+
help: "\u8BF7\u6C42\u6765\u6E90 \u2014\u2014 \u8282\u70B9\u9A71\u52A8\u7684\u5BA1\u6279\u4E3A `flow:<flowName|nodeId>`"
|
|
186
|
+
},
|
|
187
|
+
object_name: {
|
|
188
|
+
label: "\u5BF9\u8C61"
|
|
189
|
+
},
|
|
190
|
+
record_id: {
|
|
191
|
+
label: "\u8BB0\u5F55 ID"
|
|
192
|
+
},
|
|
193
|
+
submitter_id: {
|
|
194
|
+
label: "\u63D0\u4EA4\u4EBA"
|
|
195
|
+
},
|
|
196
|
+
submitter_comment: {
|
|
197
|
+
label: "\u63D0\u4EA4\u5907\u6CE8"
|
|
198
|
+
},
|
|
199
|
+
status: {
|
|
200
|
+
label: "\u72B6\u6001",
|
|
201
|
+
help: "\u8BF7\u6C42\u7684\u751F\u547D\u5468\u671F\u72B6\u6001",
|
|
202
|
+
options: {
|
|
203
|
+
pending: "\u5F85\u5904\u7406",
|
|
204
|
+
approved: "\u5DF2\u6279\u51C6",
|
|
205
|
+
rejected: "\u5DF2\u62D2\u7EDD",
|
|
206
|
+
recalled: "\u5DF2\u64A4\u56DE"
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
current_step: {
|
|
210
|
+
label: "\u5F53\u524D\u6B65\u9AA4",
|
|
211
|
+
help: "\u5F53\u524D\u7B49\u5F85\u5BA1\u6279\u7684\u6B65\u9AA4\u673A\u5668\u540D\u79F0"
|
|
212
|
+
},
|
|
213
|
+
current_step_index: {
|
|
214
|
+
label: "\u5F53\u524D\u6B65\u9AA4\u7D22\u5F15"
|
|
215
|
+
},
|
|
216
|
+
pending_approvers: {
|
|
217
|
+
label: "\u5F85\u5BA1\u6279\u4EBA",
|
|
218
|
+
help: "\u53EF\u5728\u5F53\u524D\u6B65\u9AA4\u6267\u884C\u64CD\u4F5C\u7684\u7528\u6237 ID\uFF0C\u9017\u53F7\u5206\u9694"
|
|
219
|
+
},
|
|
220
|
+
payload_json: {
|
|
221
|
+
label: "\u5FEB\u7167",
|
|
222
|
+
help: "\u63D0\u4EA4\u65F6\u7684\u8BB0\u5F55\u5FEB\u7167"
|
|
223
|
+
},
|
|
224
|
+
flow_run_id: {
|
|
225
|
+
label: "Flow Run",
|
|
226
|
+
help: "Suspended automation run id this request gates (ADR-0019). The decision resumes it."
|
|
227
|
+
},
|
|
228
|
+
flow_node_id: {
|
|
229
|
+
label: "Flow Node",
|
|
230
|
+
help: "Approval node id within the flow that opened this request (ADR-0019)."
|
|
231
|
+
},
|
|
232
|
+
node_config_json: {
|
|
233
|
+
label: "Node Config",
|
|
234
|
+
help: "Snapshot of the Approval node config (approvers/behavior) for node-driven requests (ADR-0019)."
|
|
235
|
+
},
|
|
236
|
+
completed_at: {
|
|
237
|
+
label: "\u5B8C\u6210\u65F6\u95F4"
|
|
238
|
+
},
|
|
239
|
+
created_at: {
|
|
240
|
+
label: "\u521B\u5EFA\u65F6\u95F4"
|
|
241
|
+
},
|
|
242
|
+
updated_at: {
|
|
243
|
+
label: "\u66F4\u65B0\u65F6\u95F4"
|
|
244
|
+
}
|
|
133
245
|
},
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
246
|
+
_views: {
|
|
247
|
+
my_pending: {
|
|
248
|
+
label: "\u6211\u7684\u5F85\u529E"
|
|
249
|
+
},
|
|
250
|
+
submitted_by_me: {
|
|
251
|
+
label: "\u6211\u63D0\u4EA4\u7684"
|
|
252
|
+
},
|
|
253
|
+
completed: {
|
|
254
|
+
label: "\u5DF2\u5B8C\u6210"
|
|
255
|
+
},
|
|
256
|
+
all_requests: {
|
|
257
|
+
label: "\u5168\u90E8"
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
sys_approval_action: {
|
|
262
|
+
label: "\u5BA1\u6279\u52A8\u4F5C",
|
|
263
|
+
pluralLabel: "\u5BA1\u6279\u52A8\u4F5C",
|
|
264
|
+
description: "\u8FFD\u52A0\u5199\u5165\u7684\u5BA1\u6279\u64CD\u4F5C\u5BA1\u8BA1\u8BB0\u5F55",
|
|
265
|
+
fields: {
|
|
266
|
+
id: {
|
|
267
|
+
label: "\u52A8\u4F5C ID"
|
|
268
|
+
},
|
|
269
|
+
organization_id: {
|
|
270
|
+
label: "\u7EC4\u7EC7",
|
|
271
|
+
help: "\u62E5\u6709\u8BE5\u52A8\u4F5C\u7684\u79DF\u6237\uFF08\u4E0E\u7236\u8BF7\u6C42\u4FDD\u6301\u4E00\u81F4\uFF09"
|
|
272
|
+
},
|
|
273
|
+
request_id: {
|
|
274
|
+
label: "\u8BF7\u6C42"
|
|
275
|
+
},
|
|
276
|
+
step_name: {
|
|
277
|
+
label: "\u6B65\u9AA4",
|
|
278
|
+
help: "\u6267\u884C\u8BE5\u52A8\u4F5C\u65F6\u5BF9\u5E94\u6B65\u9AA4\u7684\u673A\u5668\u540D\u79F0"
|
|
279
|
+
},
|
|
280
|
+
step_index: {
|
|
281
|
+
label: "\u6B65\u9AA4\u7D22\u5F15"
|
|
282
|
+
},
|
|
283
|
+
action: {
|
|
284
|
+
label: "\u64CD\u4F5C",
|
|
285
|
+
options: {
|
|
286
|
+
submit: "\u63D0\u4EA4",
|
|
287
|
+
approve: "\u6279\u51C6",
|
|
288
|
+
reject: "\u62D2\u7EDD",
|
|
289
|
+
recall: "\u64A4\u56DE",
|
|
290
|
+
escalate: "\u5347\u7EA7"
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
actor_id: {
|
|
294
|
+
label: "\u6267\u884C\u4EBA"
|
|
295
|
+
},
|
|
296
|
+
comment: {
|
|
297
|
+
label: "\u8BC4\u8BBA"
|
|
298
|
+
},
|
|
299
|
+
created_at: {
|
|
300
|
+
label: "\u521B\u5EFA\u65F6\u95F4"
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
_views: {
|
|
304
|
+
recent: {
|
|
305
|
+
label: "\u6700\u8FD1"
|
|
306
|
+
},
|
|
307
|
+
by_actor: {
|
|
308
|
+
label: "\u6309\u6267\u884C\u4EBA"
|
|
309
|
+
},
|
|
310
|
+
all_actions: {
|
|
311
|
+
label: "\u5168\u90E8"
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
};
|
|
139
316
|
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// src/translations/ja-JP.objects.generated.ts
|
|
320
|
+
var jaJPObjects;
|
|
321
|
+
var init_ja_JP_objects_generated = __esm({
|
|
322
|
+
"src/translations/ja-JP.objects.generated.ts"() {
|
|
323
|
+
"use strict";
|
|
324
|
+
jaJPObjects = {
|
|
325
|
+
sys_approval_request: {
|
|
326
|
+
label: "\u627F\u8A8D\u30EA\u30AF\u30A8\u30B9\u30C8",
|
|
327
|
+
pluralLabel: "\u627F\u8A8D\u30EA\u30AF\u30A8\u30B9\u30C8",
|
|
328
|
+
description: "\u9001\u4FE1\u3054\u3068\u306B\u8FFD\u8DE1\u3055\u308C\u308B\u30E9\u30A4\u30D6\u627F\u8A8D\u30A4\u30F3\u30B9\u30BF\u30F3\u30B9",
|
|
329
|
+
fields: {
|
|
330
|
+
id: {
|
|
331
|
+
label: "\u30EA\u30AF\u30A8\u30B9\u30C8 ID"
|
|
332
|
+
},
|
|
333
|
+
organization_id: {
|
|
334
|
+
label: "\u7D44\u7E54",
|
|
335
|
+
help: "\u3053\u306E\u627F\u8A8D\u30EA\u30AF\u30A8\u30B9\u30C8\u3092\u6240\u6709\u3059\u308B\u30C6\u30CA\u30F3\u30C8\uFF08\u9001\u4FE1\u8005\u30B3\u30F3\u30C6\u30AD\u30B9\u30C8\u304B\u3089\u4F1D\u64AD\uFF09"
|
|
336
|
+
},
|
|
337
|
+
process_name: {
|
|
338
|
+
label: "\u30BD\u30FC\u30B9",
|
|
339
|
+
help: "\u30EA\u30AF\u30A8\u30B9\u30C8\u306E\u767A\u751F\u5143 \u2014 \u30CE\u30FC\u30C9\u99C6\u52D5\u306E\u627F\u8A8D\u3067\u306F `flow:<flowName|nodeId>`"
|
|
340
|
+
},
|
|
341
|
+
object_name: {
|
|
342
|
+
label: "\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8"
|
|
343
|
+
},
|
|
344
|
+
record_id: {
|
|
345
|
+
label: "\u30EC\u30B3\u30FC\u30C9 ID"
|
|
346
|
+
},
|
|
347
|
+
submitter_id: {
|
|
348
|
+
label: "\u9001\u4FE1\u8005"
|
|
349
|
+
},
|
|
350
|
+
submitter_comment: {
|
|
351
|
+
label: "\u9001\u4FE1\u8005\u30B3\u30E1\u30F3\u30C8"
|
|
352
|
+
},
|
|
353
|
+
status: {
|
|
354
|
+
label: "\u30B9\u30C6\u30FC\u30BF\u30B9",
|
|
355
|
+
help: "\u30EA\u30AF\u30A8\u30B9\u30C8\u306E\u30E9\u30A4\u30D5\u30B5\u30A4\u30AF\u30EB\u72B6\u614B",
|
|
356
|
+
options: {
|
|
357
|
+
pending: "\u4FDD\u7559\u4E2D",
|
|
358
|
+
approved: "\u627F\u8A8D\u6E08\u307F",
|
|
359
|
+
rejected: "\u5374\u4E0B\u6E08\u307F",
|
|
360
|
+
recalled: "\u53D6\u308A\u6D88\u3057\u6E08\u307F"
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
current_step: {
|
|
364
|
+
label: "\u73FE\u5728\u306E\u30B9\u30C6\u30C3\u30D7",
|
|
365
|
+
help: "\u627F\u8A8D\u5F85\u3061\u306E\u30B9\u30C6\u30C3\u30D7\u306E\u6A5F\u68B0\u540D"
|
|
366
|
+
},
|
|
367
|
+
current_step_index: {
|
|
368
|
+
label: "\u73FE\u5728\u306E\u30B9\u30C6\u30C3\u30D7\u756A\u53F7"
|
|
369
|
+
},
|
|
370
|
+
pending_approvers: {
|
|
371
|
+
label: "\u627F\u8A8D\u5F85\u3061\u627F\u8A8D\u8005",
|
|
372
|
+
help: "\u73FE\u5728\u306E\u30B9\u30C6\u30C3\u30D7\u3092\u51E6\u7406\u3067\u304D\u308B\u30E6\u30FC\u30B6\u30FC ID \u306E\u30AB\u30F3\u30DE\u533A\u5207\u308A\u30EA\u30B9\u30C8"
|
|
373
|
+
},
|
|
374
|
+
payload_json: {
|
|
375
|
+
label: "\u30B9\u30CA\u30C3\u30D7\u30B7\u30E7\u30C3\u30C8",
|
|
376
|
+
help: "\u9001\u4FE1\u6642\u306E\u30EC\u30B3\u30FC\u30C9\u30B9\u30CA\u30C3\u30D7\u30B7\u30E7\u30C3\u30C8"
|
|
377
|
+
},
|
|
378
|
+
flow_run_id: {
|
|
379
|
+
label: "Flow Run",
|
|
380
|
+
help: "Suspended automation run id this request gates (ADR-0019). The decision resumes it."
|
|
381
|
+
},
|
|
382
|
+
flow_node_id: {
|
|
383
|
+
label: "Flow Node",
|
|
384
|
+
help: "Approval node id within the flow that opened this request (ADR-0019)."
|
|
385
|
+
},
|
|
386
|
+
node_config_json: {
|
|
387
|
+
label: "Node Config",
|
|
388
|
+
help: "Snapshot of the Approval node config (approvers/behavior) for node-driven requests (ADR-0019)."
|
|
389
|
+
},
|
|
390
|
+
completed_at: {
|
|
391
|
+
label: "\u5B8C\u4E86\u65E5\u6642"
|
|
392
|
+
},
|
|
393
|
+
created_at: {
|
|
394
|
+
label: "\u4F5C\u6210\u65E5\u6642"
|
|
395
|
+
},
|
|
396
|
+
updated_at: {
|
|
397
|
+
label: "\u66F4\u65B0\u65E5\u6642"
|
|
398
|
+
}
|
|
399
|
+
},
|
|
400
|
+
_views: {
|
|
401
|
+
my_pending: {
|
|
402
|
+
label: "\u81EA\u5206\u306E\u4FDD\u7559\u4E2D"
|
|
403
|
+
},
|
|
404
|
+
submitted_by_me: {
|
|
405
|
+
label: "\u81EA\u5206\u304C\u9001\u4FE1"
|
|
406
|
+
},
|
|
407
|
+
completed: {
|
|
408
|
+
label: "\u5B8C\u4E86\u6E08\u307F"
|
|
409
|
+
},
|
|
410
|
+
all_requests: {
|
|
411
|
+
label: "\u3059\u3079\u3066"
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
sys_approval_action: {
|
|
416
|
+
label: "\u627F\u8A8D\u30A2\u30AF\u30B7\u30E7\u30F3",
|
|
417
|
+
pluralLabel: "\u627F\u8A8D\u30A2\u30AF\u30B7\u30E7\u30F3",
|
|
418
|
+
description: "\u627F\u8A8D\u30A2\u30AF\u30B7\u30E7\u30F3\u306E\u8FFD\u8A18\u5C02\u7528\u76E3\u67FB\u8A3C\u8DE1",
|
|
419
|
+
fields: {
|
|
420
|
+
id: {
|
|
421
|
+
label: "\u30A2\u30AF\u30B7\u30E7\u30F3 ID"
|
|
422
|
+
},
|
|
423
|
+
organization_id: {
|
|
424
|
+
label: "\u7D44\u7E54",
|
|
425
|
+
help: "\u3053\u306E\u30A2\u30AF\u30B7\u30E7\u30F3\u3092\u6240\u6709\u3059\u308B\u30C6\u30CA\u30F3\u30C8\uFF08\u89AA\u30EA\u30AF\u30A8\u30B9\u30C8\u3068\u540C\u3058\uFF09"
|
|
426
|
+
},
|
|
427
|
+
request_id: {
|
|
428
|
+
label: "\u30EA\u30AF\u30A8\u30B9\u30C8"
|
|
429
|
+
},
|
|
430
|
+
step_name: {
|
|
431
|
+
label: "\u30B9\u30C6\u30C3\u30D7",
|
|
432
|
+
help: "\u30A2\u30AF\u30B7\u30E7\u30F3\u6642\u70B9\u306E\u30B9\u30C6\u30C3\u30D7\u306E\u6A5F\u68B0\u540D"
|
|
433
|
+
},
|
|
434
|
+
step_index: {
|
|
435
|
+
label: "\u30B9\u30C6\u30C3\u30D7\u756A\u53F7"
|
|
436
|
+
},
|
|
437
|
+
action: {
|
|
438
|
+
label: "\u30A2\u30AF\u30B7\u30E7\u30F3",
|
|
439
|
+
options: {
|
|
440
|
+
submit: "\u7533\u8ACB",
|
|
441
|
+
approve: "\u627F\u8A8D",
|
|
442
|
+
reject: "\u5374\u4E0B",
|
|
443
|
+
recall: "\u53D6\u6D88",
|
|
444
|
+
escalate: "\u30A8\u30B9\u30AB\u30EC\u30FC\u30B7\u30E7\u30F3"
|
|
445
|
+
}
|
|
446
|
+
},
|
|
447
|
+
actor_id: {
|
|
448
|
+
label: "\u64CD\u4F5C\u8005"
|
|
449
|
+
},
|
|
450
|
+
comment: {
|
|
451
|
+
label: "\u30B3\u30E1\u30F3\u30C8"
|
|
452
|
+
},
|
|
453
|
+
created_at: {
|
|
454
|
+
label: "\u4F5C\u6210\u65E5\u6642"
|
|
455
|
+
}
|
|
456
|
+
},
|
|
457
|
+
_views: {
|
|
458
|
+
recent: {
|
|
459
|
+
label: "\u6700\u8FD1"
|
|
460
|
+
},
|
|
461
|
+
by_actor: {
|
|
462
|
+
label: "\u64CD\u4F5C\u8005\u5225"
|
|
463
|
+
},
|
|
464
|
+
all_actions: {
|
|
465
|
+
label: "\u3059\u3079\u3066"
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
};
|
|
152
470
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// src/translations/es-ES.objects.generated.ts
|
|
474
|
+
var esESObjects;
|
|
475
|
+
var init_es_ES_objects_generated = __esm({
|
|
476
|
+
"src/translations/es-ES.objects.generated.ts"() {
|
|
477
|
+
"use strict";
|
|
478
|
+
esESObjects = {
|
|
479
|
+
sys_approval_request: {
|
|
480
|
+
label: "Solicitud de aprobaci\xF3n",
|
|
481
|
+
pluralLabel: "Solicitudes de aprobaci\xF3n",
|
|
482
|
+
description: "Instancia activa de aprobaci\xF3n registrada por env\xEDo",
|
|
483
|
+
fields: {
|
|
484
|
+
id: {
|
|
485
|
+
label: "ID de solicitud"
|
|
486
|
+
},
|
|
487
|
+
organization_id: {
|
|
488
|
+
label: "Organizaci\xF3n",
|
|
489
|
+
help: "Tenant que posee esta solicitud de aprobaci\xF3n (propagado desde el contexto del solicitante)."
|
|
490
|
+
},
|
|
491
|
+
process_name: {
|
|
492
|
+
label: "Origen",
|
|
493
|
+
help: "Origen de la solicitud \u2014 `flow:<flowName|nodeId>` para aprobaciones por nodo"
|
|
494
|
+
},
|
|
495
|
+
object_name: {
|
|
496
|
+
label: "Objeto"
|
|
497
|
+
},
|
|
498
|
+
record_id: {
|
|
499
|
+
label: "ID de registro"
|
|
500
|
+
},
|
|
501
|
+
submitter_id: {
|
|
502
|
+
label: "Solicitante"
|
|
503
|
+
},
|
|
504
|
+
submitter_comment: {
|
|
505
|
+
label: "Comentario del solicitante"
|
|
506
|
+
},
|
|
507
|
+
status: {
|
|
508
|
+
label: "Estado",
|
|
509
|
+
help: "Estado del ciclo de vida de la solicitud.",
|
|
510
|
+
options: {
|
|
511
|
+
pending: "Pendiente",
|
|
512
|
+
approved: "Aprobada",
|
|
513
|
+
rejected: "Rechazada",
|
|
514
|
+
recalled: "Retirada"
|
|
515
|
+
}
|
|
516
|
+
},
|
|
517
|
+
current_step: {
|
|
518
|
+
label: "Paso actual",
|
|
519
|
+
help: "Nombre t\xE9cnico del paso pendiente de aprobaci\xF3n."
|
|
520
|
+
},
|
|
521
|
+
current_step_index: {
|
|
522
|
+
label: "\xCDndice del paso actual"
|
|
523
|
+
},
|
|
524
|
+
pending_approvers: {
|
|
525
|
+
label: "Aprobadores pendientes",
|
|
526
|
+
help: "ID de usuario separados por comas que pueden actuar en el paso actual."
|
|
527
|
+
},
|
|
528
|
+
payload_json: {
|
|
529
|
+
label: "Instant\xE1nea",
|
|
530
|
+
help: "Instant\xE1nea del registro en el momento del env\xEDo."
|
|
531
|
+
},
|
|
532
|
+
flow_run_id: {
|
|
533
|
+
label: "Flow Run",
|
|
534
|
+
help: "Suspended automation run id this request gates (ADR-0019). The decision resumes it."
|
|
535
|
+
},
|
|
536
|
+
flow_node_id: {
|
|
537
|
+
label: "Flow Node",
|
|
538
|
+
help: "Approval node id within the flow that opened this request (ADR-0019)."
|
|
539
|
+
},
|
|
540
|
+
node_config_json: {
|
|
541
|
+
label: "Node Config",
|
|
542
|
+
help: "Snapshot of the Approval node config (approvers/behavior) for node-driven requests (ADR-0019)."
|
|
543
|
+
},
|
|
544
|
+
completed_at: {
|
|
545
|
+
label: "Completado el"
|
|
546
|
+
},
|
|
547
|
+
created_at: {
|
|
548
|
+
label: "Creado el"
|
|
549
|
+
},
|
|
550
|
+
updated_at: {
|
|
551
|
+
label: "Actualizado el"
|
|
552
|
+
}
|
|
553
|
+
},
|
|
554
|
+
_views: {
|
|
555
|
+
my_pending: {
|
|
556
|
+
label: "Mis pendientes"
|
|
557
|
+
},
|
|
558
|
+
submitted_by_me: {
|
|
559
|
+
label: "Enviadas por m\xED"
|
|
560
|
+
},
|
|
561
|
+
completed: {
|
|
562
|
+
label: "Completadas"
|
|
563
|
+
},
|
|
564
|
+
all_requests: {
|
|
565
|
+
label: "Todas"
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
},
|
|
569
|
+
sys_approval_action: {
|
|
570
|
+
label: "Acci\xF3n de aprobaci\xF3n",
|
|
571
|
+
pluralLabel: "Acciones de aprobaci\xF3n",
|
|
572
|
+
description: "Registro de auditor\xEDa append-only para acciones de aprobaci\xF3n",
|
|
573
|
+
fields: {
|
|
574
|
+
id: {
|
|
575
|
+
label: "ID de acci\xF3n"
|
|
576
|
+
},
|
|
577
|
+
organization_id: {
|
|
578
|
+
label: "Organizaci\xF3n",
|
|
579
|
+
help: "Tenant que posee esta acci\xF3n (refleja la solicitud principal)."
|
|
580
|
+
},
|
|
581
|
+
request_id: {
|
|
582
|
+
label: "Solicitud"
|
|
583
|
+
},
|
|
584
|
+
step_name: {
|
|
585
|
+
label: "Paso",
|
|
586
|
+
help: "Nombre t\xE9cnico del paso en el momento de la acci\xF3n."
|
|
587
|
+
},
|
|
588
|
+
step_index: {
|
|
589
|
+
label: "\xCDndice del paso"
|
|
590
|
+
},
|
|
591
|
+
action: {
|
|
592
|
+
label: "Acci\xF3n",
|
|
593
|
+
options: {
|
|
594
|
+
submit: "Enviar",
|
|
595
|
+
approve: "Aprobar",
|
|
596
|
+
reject: "Rechazar",
|
|
597
|
+
recall: "Retirar",
|
|
598
|
+
escalate: "Escalar"
|
|
599
|
+
}
|
|
600
|
+
},
|
|
601
|
+
actor_id: {
|
|
602
|
+
label: "Actor"
|
|
603
|
+
},
|
|
604
|
+
comment: {
|
|
605
|
+
label: "Comentario"
|
|
606
|
+
},
|
|
607
|
+
created_at: {
|
|
608
|
+
label: "Creado el"
|
|
609
|
+
}
|
|
610
|
+
},
|
|
611
|
+
_views: {
|
|
612
|
+
recent: {
|
|
613
|
+
label: "Recientes"
|
|
614
|
+
},
|
|
615
|
+
by_actor: {
|
|
616
|
+
label: "Por actor"
|
|
617
|
+
},
|
|
618
|
+
all_actions: {
|
|
619
|
+
label: "Todas"
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
};
|
|
166
624
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
// src/translations/index.ts
|
|
628
|
+
var translations_exports = {};
|
|
629
|
+
__export(translations_exports, {
|
|
630
|
+
ApprovalsTranslations: () => ApprovalsTranslations
|
|
631
|
+
});
|
|
632
|
+
var ApprovalsTranslations;
|
|
633
|
+
var init_translations = __esm({
|
|
634
|
+
"src/translations/index.ts"() {
|
|
635
|
+
"use strict";
|
|
636
|
+
init_en_objects_generated();
|
|
637
|
+
init_zh_CN_objects_generated();
|
|
638
|
+
init_ja_JP_objects_generated();
|
|
639
|
+
init_es_ES_objects_generated();
|
|
640
|
+
ApprovalsTranslations = {
|
|
641
|
+
en: { objects: enObjects },
|
|
642
|
+
"zh-CN": { objects: zhCNObjects },
|
|
643
|
+
"ja-JP": { objects: jaJPObjects },
|
|
644
|
+
"es-ES": { objects: esESObjects }
|
|
645
|
+
};
|
|
171
646
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
// src/sys-approval-request.object.ts
|
|
650
|
+
import { ObjectSchema, Field } from "@objectstack/spec/data";
|
|
651
|
+
var SysApprovalRequest = ObjectSchema.create({
|
|
652
|
+
name: "sys_approval_request",
|
|
653
|
+
label: "Approval Request",
|
|
654
|
+
pluralLabel: "Approval Requests",
|
|
655
|
+
icon: "inbox",
|
|
656
|
+
isSystem: true,
|
|
657
|
+
managedBy: "system",
|
|
658
|
+
description: "Live approval instance tracked per submission",
|
|
659
|
+
displayNameField: "id",
|
|
660
|
+
titleFormat: "{process_name} \xB7 {record_id}",
|
|
661
|
+
compactLayout: ["process_name", "object_name", "record_id", "status", "current_step", "submitter_id", "updated_at"],
|
|
662
|
+
// Curated built-in list views — render as segmented tabs in the console.
|
|
663
|
+
// Filters use {current_user_id} substitution wired by the console.
|
|
664
|
+
listViews: {
|
|
665
|
+
my_pending: {
|
|
666
|
+
type: "grid",
|
|
667
|
+
name: "my_pending",
|
|
668
|
+
label: "My Pending",
|
|
669
|
+
data: { provider: "object", object: "sys_approval_request" },
|
|
670
|
+
columns: ["process_name", "object_name", "record_id", "current_step", "submitter_id", "updated_at"],
|
|
671
|
+
filter: [
|
|
672
|
+
{ field: "status", operator: "equals", value: "pending" },
|
|
673
|
+
{ field: "pending_approvers", operator: "contains", value: "{current_user_id}" }
|
|
674
|
+
],
|
|
675
|
+
sort: [{ field: "updated_at", order: "desc" }],
|
|
676
|
+
pagination: { pageSize: 25 },
|
|
677
|
+
emptyState: { title: "No pending approvals", message: "You're all caught up." }
|
|
678
|
+
},
|
|
679
|
+
submitted_by_me: {
|
|
680
|
+
type: "grid",
|
|
681
|
+
name: "submitted_by_me",
|
|
682
|
+
label: "I Submitted",
|
|
683
|
+
data: { provider: "object", object: "sys_approval_request" },
|
|
684
|
+
columns: ["process_name", "object_name", "record_id", "status", "current_step", "updated_at"],
|
|
685
|
+
filter: [{ field: "submitter_id", operator: "equals", value: "{current_user_id}" }],
|
|
686
|
+
sort: [{ field: "updated_at", order: "desc" }],
|
|
687
|
+
pagination: { pageSize: 25 }
|
|
688
|
+
},
|
|
689
|
+
completed: {
|
|
690
|
+
type: "grid",
|
|
691
|
+
name: "completed",
|
|
692
|
+
label: "Completed",
|
|
693
|
+
data: { provider: "object", object: "sys_approval_request" },
|
|
694
|
+
columns: ["process_name", "object_name", "record_id", "status", "submitter_id", "completed_at"],
|
|
695
|
+
filter: [{ field: "status", operator: "in", value: ["approved", "rejected", "recalled"] }],
|
|
696
|
+
sort: [{ field: "completed_at", order: "desc" }],
|
|
697
|
+
pagination: { pageSize: 25 }
|
|
698
|
+
},
|
|
699
|
+
all_requests: {
|
|
700
|
+
type: "grid",
|
|
701
|
+
name: "all_requests",
|
|
702
|
+
label: "All",
|
|
703
|
+
data: { provider: "object", object: "sys_approval_request" },
|
|
704
|
+
columns: ["process_name", "object_name", "record_id", "status", "current_step", "submitter_id", "updated_at"],
|
|
705
|
+
sort: [{ field: "updated_at", order: "desc" }],
|
|
706
|
+
pagination: { pageSize: 50 }
|
|
195
707
|
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
708
|
+
},
|
|
709
|
+
fields: {
|
|
710
|
+
id: Field.text({ label: "Request ID", required: true, readonly: true, group: "System" }),
|
|
711
|
+
organization_id: Field.lookup("sys_organization", {
|
|
712
|
+
label: "Organization",
|
|
713
|
+
required: false,
|
|
714
|
+
group: "System",
|
|
715
|
+
description: "Tenant that owns this approval request (propagated from submitter context)"
|
|
716
|
+
}),
|
|
717
|
+
process_name: Field.text({
|
|
718
|
+
label: "Source",
|
|
719
|
+
required: true,
|
|
720
|
+
maxLength: 100,
|
|
721
|
+
description: "Origin of the request \u2014 `flow:<flowName|nodeId>` for node-driven approvals",
|
|
722
|
+
group: "Target"
|
|
723
|
+
}),
|
|
724
|
+
object_name: Field.text({
|
|
725
|
+
label: "Object",
|
|
726
|
+
required: true,
|
|
727
|
+
maxLength: 100,
|
|
728
|
+
group: "Target"
|
|
729
|
+
}),
|
|
730
|
+
record_id: Field.text({
|
|
731
|
+
label: "Record ID",
|
|
732
|
+
required: true,
|
|
733
|
+
maxLength: 100,
|
|
734
|
+
group: "Target"
|
|
735
|
+
}),
|
|
736
|
+
submitter_id: Field.lookup("sys_user", {
|
|
737
|
+
label: "Submitter",
|
|
738
|
+
required: false,
|
|
739
|
+
group: "Target"
|
|
740
|
+
}),
|
|
741
|
+
submitter_comment: Field.textarea({
|
|
742
|
+
label: "Submitter Comment",
|
|
743
|
+
required: false,
|
|
744
|
+
group: "Target"
|
|
745
|
+
}),
|
|
746
|
+
status: Field.select(
|
|
747
|
+
["pending", "approved", "rejected", "recalled"],
|
|
748
|
+
{
|
|
749
|
+
label: "Status",
|
|
750
|
+
required: true,
|
|
751
|
+
defaultValue: "pending",
|
|
752
|
+
description: "Lifecycle state of the request",
|
|
753
|
+
group: "State"
|
|
754
|
+
}
|
|
755
|
+
),
|
|
756
|
+
current_step: Field.text({
|
|
757
|
+
label: "Current Step",
|
|
758
|
+
required: false,
|
|
759
|
+
maxLength: 100,
|
|
760
|
+
description: "Machine name of the step awaiting approval",
|
|
761
|
+
group: "State"
|
|
762
|
+
}),
|
|
763
|
+
current_step_index: Field.number({
|
|
764
|
+
label: "Current Step Index",
|
|
765
|
+
required: false,
|
|
766
|
+
defaultValue: 0,
|
|
767
|
+
group: "State"
|
|
768
|
+
}),
|
|
769
|
+
pending_approvers: Field.textarea({
|
|
770
|
+
label: "Pending Approvers",
|
|
771
|
+
required: false,
|
|
772
|
+
description: "Comma-separated user ids who can act on the current step",
|
|
773
|
+
group: "State"
|
|
774
|
+
}),
|
|
775
|
+
payload_json: Field.textarea({
|
|
776
|
+
label: "Snapshot",
|
|
777
|
+
required: false,
|
|
778
|
+
description: "Record snapshot at submission time",
|
|
779
|
+
group: "State"
|
|
780
|
+
}),
|
|
781
|
+
// ── ADR-0019: approval-as-flow-node correlation ──────────────────
|
|
782
|
+
// When a request is opened by an Approval *node* (rather than a standalone
|
|
783
|
+
// process), these tie it back to the suspended flow run so a decision can
|
|
784
|
+
// resume it. Null for legacy process-driven requests.
|
|
785
|
+
flow_run_id: Field.text({
|
|
786
|
+
label: "Flow Run",
|
|
787
|
+
required: false,
|
|
788
|
+
maxLength: 100,
|
|
789
|
+
readonly: true,
|
|
790
|
+
description: "Suspended automation run id this request gates (ADR-0019). The decision resumes it.",
|
|
791
|
+
group: "State"
|
|
792
|
+
}),
|
|
793
|
+
flow_node_id: Field.text({
|
|
794
|
+
label: "Flow Node",
|
|
795
|
+
required: false,
|
|
796
|
+
maxLength: 100,
|
|
797
|
+
readonly: true,
|
|
798
|
+
description: "Approval node id within the flow that opened this request (ADR-0019).",
|
|
799
|
+
group: "State"
|
|
800
|
+
}),
|
|
801
|
+
node_config_json: Field.textarea({
|
|
802
|
+
label: "Node Config",
|
|
803
|
+
required: false,
|
|
804
|
+
readonly: true,
|
|
805
|
+
description: "Snapshot of the Approval node config (approvers/behavior) for node-driven requests (ADR-0019).",
|
|
806
|
+
group: "State"
|
|
807
|
+
}),
|
|
808
|
+
completed_at: Field.datetime({
|
|
809
|
+
label: "Completed At",
|
|
810
|
+
required: false,
|
|
811
|
+
group: "State"
|
|
812
|
+
}),
|
|
813
|
+
created_at: Field.datetime({
|
|
814
|
+
label: "Created At",
|
|
815
|
+
required: true,
|
|
816
|
+
defaultValue: "NOW()",
|
|
817
|
+
readonly: true,
|
|
818
|
+
group: "System"
|
|
819
|
+
}),
|
|
820
|
+
updated_at: Field.datetime({ label: "Updated At", required: false, group: "System" })
|
|
821
|
+
},
|
|
822
|
+
indexes: [
|
|
823
|
+
// Look up "is there a pending request for this record?" — common
|
|
824
|
+
// guard on submit and on edit-while-locked checks.
|
|
825
|
+
{ fields: ["object_name", "record_id"] },
|
|
826
|
+
{ fields: ["status", "object_name"] },
|
|
827
|
+
// "My approvals" inbox — pending_approvers is a CSV string so this
|
|
828
|
+
// index only helps with status pre-filtering; the engine does a
|
|
829
|
+
// post-filter substring match per row.
|
|
830
|
+
{ fields: ["status", "updated_at"] },
|
|
831
|
+
{ fields: ["submitter_id", "status"] }
|
|
832
|
+
]
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
// src/sys-approval-action.object.ts
|
|
836
|
+
import { ObjectSchema as ObjectSchema2, Field as Field2 } from "@objectstack/spec/data";
|
|
837
|
+
var SysApprovalAction = ObjectSchema2.create({
|
|
838
|
+
name: "sys_approval_action",
|
|
839
|
+
label: "Approval Action",
|
|
840
|
+
pluralLabel: "Approval Actions",
|
|
841
|
+
icon: "check-circle",
|
|
842
|
+
isSystem: true,
|
|
843
|
+
managedBy: "append-only",
|
|
844
|
+
description: "Append-only audit trail for approval actions",
|
|
845
|
+
displayNameField: "id",
|
|
846
|
+
titleFormat: "{action} \xB7 {step_name}",
|
|
847
|
+
compactLayout: ["request_id", "step_name", "action", "actor_id", "created_at"],
|
|
848
|
+
listViews: {
|
|
849
|
+
recent: {
|
|
850
|
+
type: "grid",
|
|
851
|
+
name: "recent",
|
|
852
|
+
label: "Recent",
|
|
853
|
+
data: { provider: "object", object: "sys_approval_action" },
|
|
854
|
+
columns: ["created_at", "request_id", "step_name", "action", "actor_id", "comment"],
|
|
855
|
+
sort: [{ field: "created_at", order: "desc" }],
|
|
856
|
+
pagination: { pageSize: 50 },
|
|
857
|
+
emptyState: { title: "No approval actions yet", message: "Actions are logged automatically when approvals progress." }
|
|
858
|
+
},
|
|
859
|
+
by_actor: {
|
|
860
|
+
type: "grid",
|
|
861
|
+
name: "by_actor",
|
|
862
|
+
label: "By Actor",
|
|
863
|
+
data: { provider: "object", object: "sys_approval_action" },
|
|
864
|
+
columns: ["actor_id", "created_at", "request_id", "step_name", "action"],
|
|
865
|
+
sort: [{ field: "actor_id", order: "asc" }, { field: "created_at", order: "desc" }],
|
|
866
|
+
grouping: { fields: [{ field: "actor_id", order: "asc", collapsed: false }] },
|
|
867
|
+
pagination: { pageSize: 100 }
|
|
868
|
+
},
|
|
869
|
+
all_actions: {
|
|
870
|
+
type: "grid",
|
|
871
|
+
name: "all_actions",
|
|
872
|
+
label: "All",
|
|
873
|
+
data: { provider: "object", object: "sys_approval_action" },
|
|
874
|
+
columns: ["created_at", "request_id", "step_name", "action", "actor_id", "comment"],
|
|
875
|
+
sort: [{ field: "created_at", order: "desc" }],
|
|
876
|
+
pagination: { pageSize: 100 }
|
|
877
|
+
}
|
|
878
|
+
},
|
|
879
|
+
fields: {
|
|
880
|
+
id: Field2.text({ label: "Action ID", required: true, readonly: true, group: "System" }),
|
|
881
|
+
organization_id: Field2.lookup("sys_organization", {
|
|
882
|
+
label: "Organization",
|
|
883
|
+
required: false,
|
|
884
|
+
group: "System",
|
|
885
|
+
description: "Tenant that owns this action (mirrors the parent request)"
|
|
886
|
+
}),
|
|
887
|
+
request_id: Field2.lookup("sys_approval_request", {
|
|
888
|
+
label: "Request",
|
|
889
|
+
required: true,
|
|
890
|
+
group: "Target"
|
|
891
|
+
}),
|
|
892
|
+
step_name: Field2.text({
|
|
893
|
+
label: "Step",
|
|
894
|
+
required: false,
|
|
895
|
+
maxLength: 100,
|
|
896
|
+
description: "Machine name of the step at the time of the action",
|
|
897
|
+
group: "Target"
|
|
898
|
+
}),
|
|
899
|
+
step_index: Field2.number({
|
|
900
|
+
label: "Step Index",
|
|
901
|
+
required: false,
|
|
902
|
+
group: "Target"
|
|
903
|
+
}),
|
|
904
|
+
action: Field2.select(
|
|
905
|
+
["submit", "approve", "reject", "recall", "escalate"],
|
|
906
|
+
{
|
|
907
|
+
label: "Action",
|
|
908
|
+
required: true,
|
|
909
|
+
group: "Action"
|
|
910
|
+
}
|
|
911
|
+
),
|
|
912
|
+
actor_id: Field2.lookup("sys_user", {
|
|
913
|
+
label: "Actor",
|
|
914
|
+
required: false,
|
|
915
|
+
group: "Action"
|
|
916
|
+
}),
|
|
917
|
+
comment: Field2.textarea({ label: "Comment", required: false, group: "Action" }),
|
|
918
|
+
created_at: Field2.datetime({
|
|
919
|
+
label: "Created At",
|
|
920
|
+
required: true,
|
|
921
|
+
defaultValue: "NOW()",
|
|
922
|
+
readonly: true,
|
|
923
|
+
group: "System"
|
|
924
|
+
})
|
|
925
|
+
},
|
|
926
|
+
indexes: [
|
|
927
|
+
{ fields: ["request_id", "created_at"] },
|
|
928
|
+
{ fields: ["request_id", "step_index", "action"] }
|
|
929
|
+
]
|
|
930
|
+
});
|
|
202
931
|
|
|
203
932
|
// src/approval-service.ts
|
|
204
|
-
|
|
933
|
+
import {
|
|
934
|
+
APPROVAL_BRANCH_LABELS
|
|
935
|
+
} from "@objectstack/spec/automation";
|
|
936
|
+
var SYSTEM_CTX = { isSystem: true, roles: [], permissions: [] };
|
|
205
937
|
function uid(prefix) {
|
|
206
938
|
const g = globalThis;
|
|
207
939
|
if (g.crypto?.randomUUID) return `${prefix}_${g.crypto.randomUUID()}`;
|
|
@@ -223,25 +955,11 @@ function csvSplit(raw) {
|
|
|
223
955
|
if (Array.isArray(raw)) return raw.map(String).filter(Boolean);
|
|
224
956
|
return String(raw).split(",").map((s) => s.trim()).filter(Boolean);
|
|
225
957
|
}
|
|
226
|
-
function rowFromProcess(row) {
|
|
227
|
-
return {
|
|
228
|
-
id: String(row.id),
|
|
229
|
-
name: String(row.name ?? ""),
|
|
230
|
-
label: String(row.label ?? ""),
|
|
231
|
-
object_name: String(row.object_name ?? ""),
|
|
232
|
-
description: row.description ?? void 0,
|
|
233
|
-
active: row.active !== false,
|
|
234
|
-
definition: parseJson(row.definition_json, {}),
|
|
235
|
-
created_at: row.created_at ?? void 0,
|
|
236
|
-
updated_at: row.updated_at ?? void 0
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
958
|
function rowFromRequest(row) {
|
|
240
959
|
return {
|
|
241
960
|
id: String(row.id),
|
|
242
961
|
organization_id: row.organization_id ?? void 0,
|
|
243
962
|
process_name: String(row.process_name ?? ""),
|
|
244
|
-
process_hash: row.process_hash ?? void 0,
|
|
245
963
|
object_name: String(row.object_name ?? ""),
|
|
246
964
|
record_id: String(row.record_id ?? ""),
|
|
247
965
|
submitter_id: row.submitter_id ?? void 0,
|
|
@@ -251,6 +969,8 @@ function rowFromRequest(row) {
|
|
|
251
969
|
current_step_index: row.current_step_index ?? void 0,
|
|
252
970
|
pending_approvers: csvSplit(row.pending_approvers),
|
|
253
971
|
payload: parseJson(row.payload_json, void 0),
|
|
972
|
+
flow_run_id: row.flow_run_id ?? void 0,
|
|
973
|
+
flow_node_id: row.flow_node_id ?? void 0,
|
|
254
974
|
completed_at: row.completed_at ?? void 0,
|
|
255
975
|
created_at: row.created_at ?? void 0,
|
|
256
976
|
updated_at: row.updated_at ?? void 0
|
|
@@ -273,23 +993,20 @@ var ApprovalService = class {
|
|
|
273
993
|
this.engine = opts.engine;
|
|
274
994
|
this.clock = opts.clock ?? { now: () => /* @__PURE__ */ new Date() };
|
|
275
995
|
this.logger = opts.logger;
|
|
276
|
-
this.
|
|
277
|
-
this.webhookTimeoutMs = opts.webhookTimeoutMs;
|
|
278
|
-
this.onRegistryChange = opts.onRegistryChange;
|
|
279
|
-
this.metadataRepo = opts.metadataRepo;
|
|
996
|
+
this.automation = opts.automation;
|
|
280
997
|
}
|
|
281
|
-
/**
|
|
282
|
-
|
|
283
|
-
this.
|
|
998
|
+
/** Attach (or replace) the automation surface used to resume flow runs. */
|
|
999
|
+
attachAutomation(automation) {
|
|
1000
|
+
this.automation = automation;
|
|
284
1001
|
}
|
|
285
1002
|
/**
|
|
286
|
-
* Expand the approvers on
|
|
287
|
-
* tables for `team:` / `department:` / `role:` / `manager:` approver
|
|
288
|
-
* types. Falls back to a prefixed literal (`type:value`) when graph
|
|
289
|
-
*
|
|
290
|
-
*
|
|
1003
|
+
* Expand the approvers on an Approval node into user IDs by querying the
|
|
1004
|
+
* graph tables for `team:` / `department:` / `role:` / `manager:` approver
|
|
1005
|
+
* types. Falls back to a prefixed literal (`type:value`) when graph lookups
|
|
1006
|
+
* produce nothing — so existing fixtures and flows that rely on substring
|
|
1007
|
+
* matching keep working.
|
|
291
1008
|
*
|
|
292
|
-
* **Graph semantics
|
|
1009
|
+
* **Graph semantics:**
|
|
293
1010
|
* - `team` → flat members of `sys_team` (better-auth; no BFS)
|
|
294
1011
|
* - `department` → recursive BFS of `sys_department.parent_department_id`
|
|
295
1012
|
* → members of every descendant via `sys_department_member`
|
|
@@ -355,7 +1072,7 @@ var ApprovalService = class {
|
|
|
355
1072
|
filter: { team_id: teamId },
|
|
356
1073
|
fields: ["user_id"],
|
|
357
1074
|
limit: 1e4,
|
|
358
|
-
context:
|
|
1075
|
+
context: SYSTEM_CTX
|
|
359
1076
|
});
|
|
360
1077
|
} catch {
|
|
361
1078
|
rows = [];
|
|
@@ -370,7 +1087,7 @@ var ApprovalService = class {
|
|
|
370
1087
|
filter: organizationId ? { id: departmentId, organization_id: organizationId } : { id: departmentId },
|
|
371
1088
|
fields: ["id", "active"],
|
|
372
1089
|
limit: 1,
|
|
373
|
-
context:
|
|
1090
|
+
context: SYSTEM_CTX
|
|
374
1091
|
});
|
|
375
1092
|
const seedRow = Array.isArray(seed) ? seed[0] : null;
|
|
376
1093
|
if (!seedRow || seedRow.active === false) return [];
|
|
@@ -385,7 +1102,7 @@ var ApprovalService = class {
|
|
|
385
1102
|
try {
|
|
386
1103
|
const filter = { parent_department_id: parent, active: { $ne: false } };
|
|
387
1104
|
if (organizationId) filter.organization_id = organizationId;
|
|
388
|
-
kids = await this.engine.find("sys_department", { filter, fields: ["id"], limit: 1e3, context:
|
|
1105
|
+
kids = await this.engine.find("sys_department", { filter, fields: ["id"], limit: 1e3, context: SYSTEM_CTX });
|
|
389
1106
|
} catch {
|
|
390
1107
|
kids = [];
|
|
391
1108
|
}
|
|
@@ -403,7 +1120,7 @@ var ApprovalService = class {
|
|
|
403
1120
|
filter: { department_id: { $in: Array.from(seen) } },
|
|
404
1121
|
fields: ["user_id"],
|
|
405
1122
|
limit: 1e4,
|
|
406
|
-
context:
|
|
1123
|
+
context: SYSTEM_CTX
|
|
407
1124
|
});
|
|
408
1125
|
} catch {
|
|
409
1126
|
rows = [];
|
|
@@ -416,7 +1133,7 @@ var ApprovalService = class {
|
|
|
416
1133
|
if (organizationId) filter.organization_id = organizationId;
|
|
417
1134
|
let rows = [];
|
|
418
1135
|
try {
|
|
419
|
-
rows = await this.engine.find("sys_member", { filter, fields: ["user_id"], limit: 1e4, context:
|
|
1136
|
+
rows = await this.engine.find("sys_member", { filter, fields: ["user_id"], limit: 1e4, context: SYSTEM_CTX });
|
|
420
1137
|
} catch {
|
|
421
1138
|
rows = [];
|
|
422
1139
|
}
|
|
@@ -428,7 +1145,7 @@ var ApprovalService = class {
|
|
|
428
1145
|
filter: { id: userId },
|
|
429
1146
|
fields: ["id", "manager_id"],
|
|
430
1147
|
limit: 1,
|
|
431
|
-
context:
|
|
1148
|
+
context: SYSTEM_CTX
|
|
432
1149
|
});
|
|
433
1150
|
const row = Array.isArray(rows) ? rows[0] : null;
|
|
434
1151
|
return row?.manager_id ? String(row.manager_id) : null;
|
|
@@ -436,263 +1153,189 @@ var ApprovalService = class {
|
|
|
436
1153
|
return null;
|
|
437
1154
|
}
|
|
438
1155
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
if (!cb) return;
|
|
442
|
-
try {
|
|
443
|
-
await cb();
|
|
444
|
-
} catch (err) {
|
|
445
|
-
this.logger?.warn?.("[approvals] onRegistryChange handler failed", { error: err?.message });
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
/**
|
|
449
|
-
* Look up the HEAD checksum of an approval process from the metadata repo
|
|
450
|
-
* (ADR-0009). Returns null when no repo is wired, no metadata exists for
|
|
451
|
-
* the name, or the lookup fails — callers MUST treat null as "do not pin"
|
|
452
|
-
* and fall back to the projection table.
|
|
453
|
-
*/
|
|
454
|
-
async resolveProcessHash(processName, organizationId) {
|
|
455
|
-
if (!this.metadataRepo) return null;
|
|
456
|
-
if (!processName) return null;
|
|
457
|
-
const orgRef = { org: organizationId || "system", type: "approval", name: processName };
|
|
1156
|
+
/** Mirror a request status onto a business-object field, if configured. */
|
|
1157
|
+
async mirrorStatusField(object, recordId, field, status) {
|
|
458
1158
|
try {
|
|
459
|
-
|
|
460
|
-
return head?.hash ?? null;
|
|
1159
|
+
await this.engine.update(object, { id: recordId, [field]: status }, { context: SYSTEM_CTX });
|
|
461
1160
|
} catch (err) {
|
|
462
|
-
this.logger?.
|
|
463
|
-
return null;
|
|
1161
|
+
this.logger?.warn?.(`[approvals] mirrorStatusField failed: ${err?.message ?? err}`);
|
|
464
1162
|
}
|
|
465
1163
|
}
|
|
1164
|
+
// ── ADR-0019: Approval-as-flow-node ──────────────────────────
|
|
1165
|
+
//
|
|
1166
|
+
// A flow's Approval node opens a request via `openNodeRequest` (carrying its
|
|
1167
|
+
// own approvers/behavior config and the suspended run id), then suspends. A
|
|
1168
|
+
// later `decide` finalizes it and resumes the flow run down the matching
|
|
1169
|
+
// `approve`/`reject` edge. The record lock is enforced by a beforeUpdate hook
|
|
1170
|
+
// keyed on a *pending* request, so finalizing auto-releases it.
|
|
466
1171
|
/**
|
|
467
|
-
*
|
|
468
|
-
*
|
|
469
|
-
*
|
|
470
|
-
* Resolution order:
|
|
471
|
-
* 1. If `req.process_hash` AND `metadataRepo` are set, try
|
|
472
|
-
* `getByHash` — return a row whose `definition` is the pinned body.
|
|
473
|
-
* 2. Otherwise (or on lookup failure) fall back to the current
|
|
474
|
-
* projection via `getProcess(req.process_name)`.
|
|
1172
|
+
* Open a pending approval request on behalf of a flow's Approval node. The
|
|
1173
|
+
* node config (approvers / behavior / status field) is snapshotted on the row
|
|
1174
|
+
* so a decision can be made without any process to resolve against.
|
|
475
1175
|
*/
|
|
476
|
-
async
|
|
477
|
-
const hash = req.process_hash;
|
|
478
|
-
if (hash && this.metadataRepo) {
|
|
479
|
-
const orgId = req.organization_id ?? null;
|
|
480
|
-
const orgRef = { org: orgId || "system", type: "approval", name: req.process_name };
|
|
481
|
-
try {
|
|
482
|
-
const pinned = await this.metadataRepo.getByHash(orgRef, hash);
|
|
483
|
-
if (pinned?.body) {
|
|
484
|
-
const current = await this.getProcess(req.process_name, context);
|
|
485
|
-
const body = pinned.body;
|
|
486
|
-
return {
|
|
487
|
-
id: current?.id ?? `pinned_${hash.slice(7, 19)}`,
|
|
488
|
-
name: req.process_name,
|
|
489
|
-
label: body.label ?? current?.label ?? req.process_name,
|
|
490
|
-
object_name: req.object_name,
|
|
491
|
-
description: body.description ?? current?.description,
|
|
492
|
-
active: current?.active ?? true,
|
|
493
|
-
definition: body,
|
|
494
|
-
created_at: current?.created_at,
|
|
495
|
-
updated_at: current?.updated_at
|
|
496
|
-
};
|
|
497
|
-
}
|
|
498
|
-
this.logger?.warn?.("[approvals] pinned process body not found; falling back to current", {
|
|
499
|
-
request: req.id,
|
|
500
|
-
process: req.process_name,
|
|
501
|
-
hash
|
|
502
|
-
});
|
|
503
|
-
} catch (err) {
|
|
504
|
-
this.logger?.warn?.("[approvals] getByHash failed; falling back to current", {
|
|
505
|
-
request: req.id,
|
|
506
|
-
error: err?.message
|
|
507
|
-
});
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
return this.getProcess(req.process_name, context);
|
|
511
|
-
}
|
|
512
|
-
/** Mirror request status onto `process.approvalStatusField` if configured. */
|
|
513
|
-
async syncStatusField(process, request) {
|
|
514
|
-
const field = process.definition?.approvalStatusField;
|
|
515
|
-
if (!field) return;
|
|
516
|
-
try {
|
|
517
|
-
await this.engine.update(
|
|
518
|
-
process.object_name,
|
|
519
|
-
{ id: request.record_id, [field]: request.status },
|
|
520
|
-
{ context: SYSTEM_CTX2 }
|
|
521
|
-
);
|
|
522
|
-
} catch (err) {
|
|
523
|
-
this.logger?.warn?.(`[approvals] syncStatusField failed: ${err?.message ?? err}`);
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
/** Convenience wrapper that funnels every action invocation through the executor. */
|
|
527
|
-
async runActions(actions, trigger, process, request, step, actorId, comment) {
|
|
528
|
-
if (!actions || actions.length === 0) return;
|
|
529
|
-
await executeActions(actions, {
|
|
530
|
-
trigger,
|
|
531
|
-
process: { ...process, object: process.object_name },
|
|
532
|
-
request,
|
|
533
|
-
step,
|
|
534
|
-
actorId: actorId ?? null,
|
|
535
|
-
comment: comment ?? null
|
|
536
|
-
}, {
|
|
537
|
-
engine: this.engine,
|
|
538
|
-
logger: this.logger,
|
|
539
|
-
fetch: this.fetchImpl,
|
|
540
|
-
webhookTimeoutMs: this.webhookTimeoutMs
|
|
541
|
-
});
|
|
542
|
-
}
|
|
543
|
-
// ── Process definitions ──────────────────────────────────────
|
|
544
|
-
async defineProcess(input, _context) {
|
|
545
|
-
if (!input.name) throw new Error("VALIDATION_FAILED: name is required");
|
|
546
|
-
if (!input.label) throw new Error("VALIDATION_FAILED: label is required");
|
|
547
|
-
if (!input.object) throw new Error("VALIDATION_FAILED: object is required");
|
|
548
|
-
if (!input.definition) throw new Error("VALIDATION_FAILED: definition is required");
|
|
549
|
-
const parsed = ApprovalProcessSchema.safeParse(input.definition);
|
|
550
|
-
if (!parsed.success) {
|
|
551
|
-
const msg = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
552
|
-
throw new Error(`VALIDATION_FAILED: ${msg}`);
|
|
553
|
-
}
|
|
554
|
-
const now = this.clock.now().toISOString();
|
|
555
|
-
const payload = {
|
|
556
|
-
name: input.name,
|
|
557
|
-
label: input.label,
|
|
558
|
-
object_name: input.object,
|
|
559
|
-
description: input.description ?? null,
|
|
560
|
-
active: input.active !== false,
|
|
561
|
-
definition_json: JSON.stringify(parsed.data),
|
|
562
|
-
updated_at: now
|
|
563
|
-
};
|
|
564
|
-
const existing = await this.engine.find("sys_approval_process", {
|
|
565
|
-
where: { name: input.name },
|
|
566
|
-
limit: 1,
|
|
567
|
-
context: SYSTEM_CTX2
|
|
568
|
-
});
|
|
569
|
-
if (Array.isArray(existing) && existing[0]) {
|
|
570
|
-
const id2 = existing[0].id;
|
|
571
|
-
await this.engine.update("sys_approval_process", { id: id2, ...payload }, { context: SYSTEM_CTX2 });
|
|
572
|
-
const row2 = rowFromProcess({ ...existing[0], ...payload, id: id2 });
|
|
573
|
-
await this.notifyRegistryChanged();
|
|
574
|
-
return row2;
|
|
575
|
-
}
|
|
576
|
-
const id = input.id ?? uid("apv");
|
|
577
|
-
const row = { id, ...payload, created_at: now };
|
|
578
|
-
await this.engine.insert("sys_approval_process", row, { context: SYSTEM_CTX2 });
|
|
579
|
-
const out = rowFromProcess(row);
|
|
580
|
-
await this.notifyRegistryChanged();
|
|
581
|
-
return out;
|
|
582
|
-
}
|
|
583
|
-
async listProcesses(filter, _context) {
|
|
584
|
-
const f = {};
|
|
585
|
-
if (filter?.object) f.object_name = filter.object;
|
|
586
|
-
if (filter?.activeOnly) f.active = true;
|
|
587
|
-
const rows = await this.engine.find("sys_approval_process", {
|
|
588
|
-
where: f,
|
|
589
|
-
limit: 500,
|
|
590
|
-
orderBy: [{ field: "updated_at", direction: "desc" }],
|
|
591
|
-
context: SYSTEM_CTX2
|
|
592
|
-
});
|
|
593
|
-
return Array.isArray(rows) ? rows.map(rowFromProcess) : [];
|
|
594
|
-
}
|
|
595
|
-
async getProcess(idOrName, _context) {
|
|
596
|
-
if (!idOrName) return null;
|
|
597
|
-
let rows = await this.engine.find("sys_approval_process", {
|
|
598
|
-
where: { id: idOrName },
|
|
599
|
-
limit: 1,
|
|
600
|
-
context: SYSTEM_CTX2
|
|
601
|
-
});
|
|
602
|
-
if (!Array.isArray(rows) || !rows[0]) {
|
|
603
|
-
rows = await this.engine.find("sys_approval_process", {
|
|
604
|
-
where: { name: idOrName },
|
|
605
|
-
limit: 1,
|
|
606
|
-
context: SYSTEM_CTX2
|
|
607
|
-
});
|
|
608
|
-
}
|
|
609
|
-
return Array.isArray(rows) && rows[0] ? rowFromProcess(rows[0]) : null;
|
|
610
|
-
}
|
|
611
|
-
async deleteProcess(idOrName, context) {
|
|
612
|
-
if (!idOrName) throw new Error("VALIDATION_FAILED: idOrName is required");
|
|
613
|
-
const proc = await this.getProcess(idOrName, context);
|
|
614
|
-
if (!proc) return;
|
|
615
|
-
await this.engine.delete("sys_approval_process", { where: { id: proc.id }, context: SYSTEM_CTX2 });
|
|
616
|
-
await this.notifyRegistryChanged();
|
|
617
|
-
}
|
|
618
|
-
// ── Requests ─────────────────────────────────────────────────
|
|
619
|
-
async submit(input, context) {
|
|
1176
|
+
async openNodeRequest(input, context) {
|
|
620
1177
|
if (!input.object) throw new Error("VALIDATION_FAILED: object is required");
|
|
621
1178
|
if (!input.recordId) throw new Error("VALIDATION_FAILED: recordId is required");
|
|
622
|
-
|
|
623
|
-
if (input.processName) {
|
|
624
|
-
process = await this.getProcess(input.processName, context);
|
|
625
|
-
if (process && !process.active) {
|
|
626
|
-
throw new Error(`NO_ACTIVE_PROCESS: process '${input.processName}' is not active`);
|
|
627
|
-
}
|
|
628
|
-
} else {
|
|
629
|
-
const list = await this.listProcesses({ object: input.object, activeOnly: true }, context);
|
|
630
|
-
process = list[0] ?? null;
|
|
631
|
-
}
|
|
632
|
-
if (!process) {
|
|
633
|
-
throw new Error(`NO_ACTIVE_PROCESS: no active approval process for object '${input.object}'`);
|
|
634
|
-
}
|
|
1179
|
+
if (!input.runId) throw new Error("VALIDATION_FAILED: runId is required");
|
|
635
1180
|
const existing = await this.engine.find("sys_approval_request", {
|
|
636
1181
|
where: { object_name: input.object, record_id: input.recordId, status: "pending" },
|
|
637
1182
|
limit: 1,
|
|
638
|
-
context:
|
|
1183
|
+
context: SYSTEM_CTX
|
|
639
1184
|
});
|
|
640
1185
|
if (Array.isArray(existing) && existing[0]) {
|
|
641
1186
|
throw new Error(`DUPLICATE_REQUEST: a pending approval already exists for ${input.object}/${input.recordId}`);
|
|
642
1187
|
}
|
|
643
|
-
const
|
|
644
|
-
|
|
645
|
-
throw new Error("VALIDATION_FAILED: process definition has no steps");
|
|
646
|
-
}
|
|
647
|
-
const step0 = steps[0];
|
|
648
|
-
const ctxOrg = context?.organizationId ?? context?.tenantId ?? null;
|
|
649
|
-
const approvers = await this.expandApprovers(step0, input.payload, ctxOrg);
|
|
1188
|
+
const ctxOrg = context?.organizationId ?? context?.tenantId ?? input.organizationId ?? null;
|
|
1189
|
+
const approvers = await this.expandApprovers({ approvers: input.config.approvers }, input.record, ctxOrg);
|
|
650
1190
|
const now = this.clock.now().toISOString();
|
|
651
1191
|
const id = uid("areq");
|
|
652
|
-
const
|
|
1192
|
+
const processName = `flow:${input.flowName ?? input.nodeId}`;
|
|
653
1193
|
const row = {
|
|
654
1194
|
id,
|
|
655
|
-
process_name:
|
|
656
|
-
process_hash: processHash,
|
|
1195
|
+
process_name: processName,
|
|
657
1196
|
object_name: input.object,
|
|
658
1197
|
record_id: input.recordId,
|
|
659
1198
|
submitter_id: input.submitterId ?? context.userId ?? null,
|
|
660
|
-
submitter_comment: input.comment ?? null,
|
|
661
1199
|
status: "pending",
|
|
662
|
-
current_step:
|
|
1200
|
+
current_step: input.nodeId,
|
|
663
1201
|
current_step_index: 0,
|
|
664
1202
|
pending_approvers: approvers.join(","),
|
|
665
|
-
payload_json: input.
|
|
1203
|
+
payload_json: input.record != null ? JSON.stringify(input.record) : null,
|
|
1204
|
+
flow_run_id: input.runId,
|
|
1205
|
+
flow_node_id: input.nodeId,
|
|
1206
|
+
node_config_json: JSON.stringify(input.config),
|
|
666
1207
|
organization_id: ctxOrg,
|
|
667
1208
|
created_at: now,
|
|
668
1209
|
updated_at: now
|
|
669
1210
|
};
|
|
670
|
-
await this.engine.insert("sys_approval_request", row, { context:
|
|
1211
|
+
await this.engine.insert("sys_approval_request", row, { context: SYSTEM_CTX });
|
|
671
1212
|
await this.engine.insert("sys_approval_action", {
|
|
672
1213
|
id: uid("aact"),
|
|
673
1214
|
request_id: id,
|
|
674
1215
|
organization_id: ctxOrg,
|
|
675
|
-
step_name:
|
|
1216
|
+
step_name: input.nodeId,
|
|
676
1217
|
step_index: 0,
|
|
677
1218
|
action: "submit",
|
|
678
1219
|
actor_id: input.submitterId ?? context.userId ?? null,
|
|
1220
|
+
comment: null,
|
|
1221
|
+
created_at: now
|
|
1222
|
+
}, { context: SYSTEM_CTX });
|
|
1223
|
+
if (input.config.approvalStatusField) {
|
|
1224
|
+
await this.mirrorStatusField(input.object, input.recordId, input.config.approvalStatusField, "pending");
|
|
1225
|
+
}
|
|
1226
|
+
return rowFromRequest(row);
|
|
1227
|
+
}
|
|
1228
|
+
/**
|
|
1229
|
+
* Record a decision on a node-driven request. Honours the node's `unanimous`
|
|
1230
|
+
* behavior (holds until every approver has approved). When the request
|
|
1231
|
+
* finalizes, returns the suspended run id + node id so the caller (or
|
|
1232
|
+
* {@link ApprovalService.decide}) can resume the flow down the matching
|
|
1233
|
+
* branch.
|
|
1234
|
+
*/
|
|
1235
|
+
async decideNode(requestId, input, context) {
|
|
1236
|
+
if (!requestId) throw new Error("VALIDATION_FAILED: requestId is required");
|
|
1237
|
+
if (!input?.actorId) throw new Error("VALIDATION_FAILED: actorId is required");
|
|
1238
|
+
if (input.decision !== "approve" && input.decision !== "reject") {
|
|
1239
|
+
throw new Error("VALIDATION_FAILED: decision must be approve|reject");
|
|
1240
|
+
}
|
|
1241
|
+
const rawRows = await this.engine.find("sys_approval_request", {
|
|
1242
|
+
where: { id: requestId },
|
|
1243
|
+
limit: 1,
|
|
1244
|
+
context: SYSTEM_CTX
|
|
1245
|
+
});
|
|
1246
|
+
const raw = Array.isArray(rawRows) ? rawRows[0] : null;
|
|
1247
|
+
if (!raw) throw new Error(`REQUEST_NOT_FOUND: ${requestId}`);
|
|
1248
|
+
if (raw.status !== "pending") throw new Error(`INVALID_STATE: request is ${raw.status}`);
|
|
1249
|
+
const pendingApprovers = csvSplit(raw.pending_approvers);
|
|
1250
|
+
if (!context.isSystem && !pendingApprovers.includes(input.actorId)) {
|
|
1251
|
+
throw new Error(`FORBIDDEN: actor '${input.actorId}' is not a pending approver`);
|
|
1252
|
+
}
|
|
1253
|
+
const config = parseJson(raw.node_config_json, { approvers: [], behavior: "first_response" });
|
|
1254
|
+
const org = raw.organization_id ?? null;
|
|
1255
|
+
const nodeId = raw.flow_node_id ?? raw.current_step ?? null;
|
|
1256
|
+
const runId = raw.flow_run_id ?? null;
|
|
1257
|
+
const now = this.clock.now().toISOString();
|
|
1258
|
+
await this.engine.insert("sys_approval_action", {
|
|
1259
|
+
id: uid("aact"),
|
|
1260
|
+
request_id: requestId,
|
|
1261
|
+
organization_id: org,
|
|
1262
|
+
step_name: nodeId,
|
|
1263
|
+
step_index: 0,
|
|
1264
|
+
action: input.decision,
|
|
1265
|
+
actor_id: input.actorId,
|
|
679
1266
|
comment: input.comment ?? null,
|
|
680
1267
|
created_at: now
|
|
681
|
-
}, { context:
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
1268
|
+
}, { context: SYSTEM_CTX });
|
|
1269
|
+
if (input.decision === "approve" && config.behavior === "unanimous") {
|
|
1270
|
+
const original = await this.expandApprovers(
|
|
1271
|
+
{ approvers: config.approvers },
|
|
1272
|
+
parseJson(raw.payload_json, void 0),
|
|
1273
|
+
org
|
|
1274
|
+
);
|
|
1275
|
+
const acts = await this.engine.find("sys_approval_action", {
|
|
1276
|
+
where: { request_id: requestId, step_index: 0, action: "approve" },
|
|
1277
|
+
limit: 500,
|
|
1278
|
+
context: SYSTEM_CTX
|
|
1279
|
+
});
|
|
1280
|
+
const approved = new Set((acts ?? []).map((a) => String(a.actor_id ?? "")).filter(Boolean));
|
|
1281
|
+
const stillPending = original.filter((a) => !approved.has(a));
|
|
1282
|
+
if (stillPending.length > 0) {
|
|
1283
|
+
await this.engine.update("sys_approval_request", {
|
|
1284
|
+
id: requestId,
|
|
1285
|
+
pending_approvers: stillPending.join(","),
|
|
1286
|
+
updated_at: now
|
|
1287
|
+
}, { context: SYSTEM_CTX });
|
|
1288
|
+
const fresh2 = await this.getRequest(requestId, context);
|
|
1289
|
+
return { request: fresh2, runId, nodeId, finalized: false, decision: input.decision };
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
const finalStatus = input.decision === "approve" ? "approved" : "rejected";
|
|
1293
|
+
await this.engine.update("sys_approval_request", {
|
|
1294
|
+
id: requestId,
|
|
1295
|
+
status: finalStatus,
|
|
1296
|
+
pending_approvers: null,
|
|
1297
|
+
completed_at: now,
|
|
1298
|
+
updated_at: now
|
|
1299
|
+
}, { context: SYSTEM_CTX });
|
|
1300
|
+
if (config.approvalStatusField) {
|
|
1301
|
+
await this.mirrorStatusField(raw.object_name, raw.record_id, config.approvalStatusField, finalStatus);
|
|
1302
|
+
}
|
|
1303
|
+
const fresh = await this.getRequest(requestId, context);
|
|
1304
|
+
return { request: fresh, runId, nodeId, finalized: true, decision: input.decision };
|
|
695
1305
|
}
|
|
1306
|
+
/**
|
|
1307
|
+
* Public contract entrypoint (ADR-0019). Records a decision on a node-driven
|
|
1308
|
+
* request via {@link ApprovalService.decideNode} and, when it finalizes,
|
|
1309
|
+
* resumes the owning flow run down the matching `approve` / `reject` edge.
|
|
1310
|
+
*/
|
|
1311
|
+
async decide(requestId, input, context) {
|
|
1312
|
+
const result = await this.decideNode(requestId, input, context);
|
|
1313
|
+
let resumed = false;
|
|
1314
|
+
if (result.finalized && result.runId && typeof this.automation?.resume === "function") {
|
|
1315
|
+
const branchLabel = result.decision === "approve" ? APPROVAL_BRANCH_LABELS.approve : APPROVAL_BRANCH_LABELS.reject;
|
|
1316
|
+
try {
|
|
1317
|
+
await this.automation.resume(result.runId, {
|
|
1318
|
+
branchLabel,
|
|
1319
|
+
output: { decision: result.decision, requestId }
|
|
1320
|
+
});
|
|
1321
|
+
resumed = true;
|
|
1322
|
+
} catch (err) {
|
|
1323
|
+
this.logger?.warn?.("[approvals] resume after decision failed", {
|
|
1324
|
+
request: requestId,
|
|
1325
|
+
run: result.runId,
|
|
1326
|
+
error: err?.message ?? String(err)
|
|
1327
|
+
});
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
return {
|
|
1331
|
+
request: result.request,
|
|
1332
|
+
finalized: result.finalized,
|
|
1333
|
+
decision: result.decision,
|
|
1334
|
+
runId: result.runId,
|
|
1335
|
+
resumed
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
// ── Read API ─────────────────────────────────────────────────
|
|
696
1339
|
async listRequests(filter, context) {
|
|
697
1340
|
const f = {};
|
|
698
1341
|
if (filter?.object) f.object_name = filter.object;
|
|
@@ -707,7 +1350,7 @@ var ApprovalService = class {
|
|
|
707
1350
|
where: f,
|
|
708
1351
|
limit: 500,
|
|
709
1352
|
orderBy: [{ field: "updated_at", direction: "desc" }],
|
|
710
|
-
context:
|
|
1353
|
+
context: SYSTEM_CTX
|
|
711
1354
|
});
|
|
712
1355
|
let list = Array.isArray(rows) ? rows.map(rowFromRequest) : [];
|
|
713
1356
|
if (statusFilter) list = list.filter((r) => statusFilter.includes(r.status));
|
|
@@ -725,169 +1368,10 @@ var ApprovalService = class {
|
|
|
725
1368
|
const rows = await this.engine.find("sys_approval_request", {
|
|
726
1369
|
where,
|
|
727
1370
|
limit: 1,
|
|
728
|
-
context:
|
|
1371
|
+
context: SYSTEM_CTX
|
|
729
1372
|
});
|
|
730
1373
|
return Array.isArray(rows) && rows[0] ? rowFromRequest(rows[0]) : null;
|
|
731
1374
|
}
|
|
732
|
-
async approve(requestId, input, context) {
|
|
733
|
-
const req = await this.getRequest(requestId, context);
|
|
734
|
-
if (!req) throw new Error(`REQUEST_NOT_FOUND: ${requestId}`);
|
|
735
|
-
if (req.status !== "pending") throw new Error(`INVALID_STATE: request is ${req.status}`);
|
|
736
|
-
if (!input?.actorId) throw new Error("VALIDATION_FAILED: actorId is required");
|
|
737
|
-
if (!context.isSystem && !(req.pending_approvers ?? []).includes(input.actorId)) {
|
|
738
|
-
throw new Error(`FORBIDDEN: actor '${input.actorId}' is not a pending approver`);
|
|
739
|
-
}
|
|
740
|
-
const process = await this.loadProcessForRequest(req, context);
|
|
741
|
-
if (!process) throw new Error(`PROCESS_NOT_FOUND: ${req.process_name}`);
|
|
742
|
-
const steps = process.definition?.steps ?? [];
|
|
743
|
-
const stepIndex = req.current_step_index ?? 0;
|
|
744
|
-
const step = steps[stepIndex];
|
|
745
|
-
if (!step) throw new Error(`INVALID_STATE: step index ${stepIndex} out of range`);
|
|
746
|
-
const now = this.clock.now().toISOString();
|
|
747
|
-
await this.engine.insert("sys_approval_action", {
|
|
748
|
-
id: uid("aact"),
|
|
749
|
-
request_id: req.id,
|
|
750
|
-
organization_id: req.organization_id ?? null,
|
|
751
|
-
step_name: step.name,
|
|
752
|
-
step_index: stepIndex,
|
|
753
|
-
action: "approve",
|
|
754
|
-
actor_id: input.actorId,
|
|
755
|
-
comment: input.comment ?? null,
|
|
756
|
-
created_at: now
|
|
757
|
-
}, { context: SYSTEM_CTX2 });
|
|
758
|
-
if (step.behavior === "unanimous") {
|
|
759
|
-
const original = await this.expandApprovers(step, req.payload, req.organization_id ?? null);
|
|
760
|
-
const acts = await this.engine.find("sys_approval_action", {
|
|
761
|
-
where: { request_id: req.id, step_index: stepIndex, action: "approve" },
|
|
762
|
-
limit: 500,
|
|
763
|
-
context: SYSTEM_CTX2
|
|
764
|
-
});
|
|
765
|
-
const approved = new Set((acts ?? []).map((a) => String(a.actor_id ?? "")).filter(Boolean));
|
|
766
|
-
const stillPending = original.filter((a) => !approved.has(a));
|
|
767
|
-
if (stillPending.length > 0) {
|
|
768
|
-
await this.engine.update("sys_approval_request", {
|
|
769
|
-
id: req.id,
|
|
770
|
-
pending_approvers: stillPending.join(","),
|
|
771
|
-
updated_at: now
|
|
772
|
-
}, { context: SYSTEM_CTX2 });
|
|
773
|
-
const fresh2 = await this.getRequest(req.id, context);
|
|
774
|
-
return { request: fresh2, finalized: false };
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
if (stepIndex + 1 >= steps.length) {
|
|
778
|
-
await this.engine.update("sys_approval_request", {
|
|
779
|
-
id: req.id,
|
|
780
|
-
status: "approved",
|
|
781
|
-
pending_approvers: null,
|
|
782
|
-
completed_at: now,
|
|
783
|
-
updated_at: now
|
|
784
|
-
}, { context: SYSTEM_CTX2 });
|
|
785
|
-
const fresh2 = await this.getRequest(req.id, context);
|
|
786
|
-
await this.runActions(step?.onApprove, "step_approve", process, fresh2, step, input.actorId, input.comment);
|
|
787
|
-
await this.syncStatusField(process, fresh2);
|
|
788
|
-
await this.runActions(process.definition?.onFinalApprove, "final_approve", process, fresh2, step, input.actorId, input.comment);
|
|
789
|
-
return { request: fresh2, finalized: true };
|
|
790
|
-
}
|
|
791
|
-
const nextStep = steps[stepIndex + 1];
|
|
792
|
-
const nextApprovers = await this.expandApprovers(nextStep, req.payload, req.organization_id ?? null);
|
|
793
|
-
await this.engine.update("sys_approval_request", {
|
|
794
|
-
id: req.id,
|
|
795
|
-
current_step: nextStep.name,
|
|
796
|
-
current_step_index: stepIndex + 1,
|
|
797
|
-
pending_approvers: nextApprovers.join(","),
|
|
798
|
-
updated_at: now
|
|
799
|
-
}, { context: SYSTEM_CTX2 });
|
|
800
|
-
const fresh = await this.getRequest(req.id, context);
|
|
801
|
-
await this.runActions(step?.onApprove, "step_approve", process, fresh, step, input.actorId, input.comment);
|
|
802
|
-
return { request: fresh, finalized: false };
|
|
803
|
-
}
|
|
804
|
-
async reject(requestId, input, context) {
|
|
805
|
-
const req = await this.getRequest(requestId, context);
|
|
806
|
-
if (!req) throw new Error(`REQUEST_NOT_FOUND: ${requestId}`);
|
|
807
|
-
if (req.status !== "pending") throw new Error(`INVALID_STATE: request is ${req.status}`);
|
|
808
|
-
if (!input?.actorId) throw new Error("VALIDATION_FAILED: actorId is required");
|
|
809
|
-
if (!context.isSystem && !(req.pending_approvers ?? []).includes(input.actorId)) {
|
|
810
|
-
throw new Error(`FORBIDDEN: actor '${input.actorId}' is not a pending approver`);
|
|
811
|
-
}
|
|
812
|
-
const process = await this.loadProcessForRequest(req, context);
|
|
813
|
-
if (!process) throw new Error(`PROCESS_NOT_FOUND: ${req.process_name}`);
|
|
814
|
-
const steps = process.definition?.steps ?? [];
|
|
815
|
-
const stepIndex = req.current_step_index ?? 0;
|
|
816
|
-
const step = steps[stepIndex];
|
|
817
|
-
const now = this.clock.now().toISOString();
|
|
818
|
-
await this.engine.insert("sys_approval_action", {
|
|
819
|
-
id: uid("aact"),
|
|
820
|
-
request_id: req.id,
|
|
821
|
-
organization_id: req.organization_id ?? null,
|
|
822
|
-
step_name: step?.name,
|
|
823
|
-
step_index: stepIndex,
|
|
824
|
-
action: "reject",
|
|
825
|
-
actor_id: input.actorId,
|
|
826
|
-
comment: input.comment ?? null,
|
|
827
|
-
created_at: now
|
|
828
|
-
}, { context: SYSTEM_CTX2 });
|
|
829
|
-
if (step?.rejectionBehavior === "back_to_previous" && stepIndex > 0) {
|
|
830
|
-
const prev = steps[stepIndex - 1];
|
|
831
|
-
const prevApprovers = await this.expandApprovers(prev, req.payload, req.organization_id ?? null);
|
|
832
|
-
await this.engine.update("sys_approval_request", {
|
|
833
|
-
id: req.id,
|
|
834
|
-
current_step: prev.name,
|
|
835
|
-
current_step_index: stepIndex - 1,
|
|
836
|
-
pending_approvers: prevApprovers.join(","),
|
|
837
|
-
updated_at: now
|
|
838
|
-
}, { context: SYSTEM_CTX2 });
|
|
839
|
-
const fresh2 = await this.getRequest(req.id, context);
|
|
840
|
-
await this.runActions(step?.onReject, "step_reject", process, fresh2, step, input.actorId, input.comment);
|
|
841
|
-
return { request: fresh2, finalized: false };
|
|
842
|
-
}
|
|
843
|
-
await this.engine.update("sys_approval_request", {
|
|
844
|
-
id: req.id,
|
|
845
|
-
status: "rejected",
|
|
846
|
-
pending_approvers: null,
|
|
847
|
-
completed_at: now,
|
|
848
|
-
updated_at: now
|
|
849
|
-
}, { context: SYSTEM_CTX2 });
|
|
850
|
-
const fresh = await this.getRequest(req.id, context);
|
|
851
|
-
await this.runActions(step?.onReject, "step_reject", process, fresh, step, input.actorId, input.comment);
|
|
852
|
-
await this.syncStatusField(process, fresh);
|
|
853
|
-
await this.runActions(process.definition?.onFinalReject, "final_reject", process, fresh, step, input.actorId, input.comment);
|
|
854
|
-
return { request: fresh, finalized: true };
|
|
855
|
-
}
|
|
856
|
-
async recall(requestId, input, context) {
|
|
857
|
-
const req = await this.getRequest(requestId, context);
|
|
858
|
-
if (!req) throw new Error(`REQUEST_NOT_FOUND: ${requestId}`);
|
|
859
|
-
if (req.status !== "pending") throw new Error(`INVALID_STATE: request is ${req.status}`);
|
|
860
|
-
if (!input?.actorId) throw new Error("VALIDATION_FAILED: actorId is required");
|
|
861
|
-
if (!context.isSystem && req.submitter_id && req.submitter_id !== input.actorId) {
|
|
862
|
-
throw new Error(`FORBIDDEN: only the submitter can recall this request`);
|
|
863
|
-
}
|
|
864
|
-
const now = this.clock.now().toISOString();
|
|
865
|
-
await this.engine.insert("sys_approval_action", {
|
|
866
|
-
id: uid("aact"),
|
|
867
|
-
request_id: req.id,
|
|
868
|
-
organization_id: req.organization_id ?? null,
|
|
869
|
-
step_name: req.current_step,
|
|
870
|
-
step_index: req.current_step_index,
|
|
871
|
-
action: "recall",
|
|
872
|
-
actor_id: input.actorId,
|
|
873
|
-
comment: input.comment ?? null,
|
|
874
|
-
created_at: now
|
|
875
|
-
}, { context: SYSTEM_CTX2 });
|
|
876
|
-
await this.engine.update("sys_approval_request", {
|
|
877
|
-
id: req.id,
|
|
878
|
-
status: "recalled",
|
|
879
|
-
pending_approvers: null,
|
|
880
|
-
completed_at: now,
|
|
881
|
-
updated_at: now
|
|
882
|
-
}, { context: SYSTEM_CTX2 });
|
|
883
|
-
const fresh = await this.getRequest(req.id, context);
|
|
884
|
-
const process = await this.loadProcessForRequest(req, context);
|
|
885
|
-
if (process) {
|
|
886
|
-
await this.syncStatusField(process, fresh);
|
|
887
|
-
await this.runActions(process.definition?.onRecall, "recall", process, fresh, void 0, input.actorId, input.comment);
|
|
888
|
-
}
|
|
889
|
-
return { request: fresh, finalized: true };
|
|
890
|
-
}
|
|
891
1375
|
async listActions(requestId, context) {
|
|
892
1376
|
if (!requestId) return [];
|
|
893
1377
|
const req = await this.getRequest(requestId, context);
|
|
@@ -896,164 +1380,136 @@ var ApprovalService = class {
|
|
|
896
1380
|
where: { request_id: requestId },
|
|
897
1381
|
limit: 500,
|
|
898
1382
|
orderBy: [{ field: "created_at", direction: "asc" }],
|
|
899
|
-
context:
|
|
1383
|
+
context: SYSTEM_CTX
|
|
900
1384
|
});
|
|
901
1385
|
return Array.isArray(rows) ? rows.map(rowFromAction) : [];
|
|
902
1386
|
}
|
|
903
1387
|
};
|
|
904
1388
|
|
|
905
|
-
// src/approvals-plugin.ts
|
|
906
|
-
import {
|
|
907
|
-
SysApprovalProcess,
|
|
908
|
-
SysApprovalRequest,
|
|
909
|
-
SysApprovalAction
|
|
910
|
-
} from "@objectstack/platform-objects/audit";
|
|
911
|
-
|
|
912
1389
|
// src/lifecycle-hooks.ts
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
expr = criteria;
|
|
923
|
-
} else {
|
|
924
|
-
return true;
|
|
925
|
-
}
|
|
926
|
-
if (!expr.source || !expr.source.trim()) return true;
|
|
927
|
-
const r = ExpressionEngine.evaluate(expr, { record });
|
|
928
|
-
if (!r.ok) {
|
|
929
|
-
logger?.warn?.("[approvals] entryCriteria evaluation failed; skipping auto-submit", {
|
|
930
|
-
source: expr.source,
|
|
931
|
-
error: r.error.message
|
|
932
|
-
});
|
|
933
|
-
return false;
|
|
1390
|
+
var APPROVALS_HOOK_PACKAGE = "plugin-approvals:lock";
|
|
1391
|
+
function parseJson2(raw, fallback) {
|
|
1392
|
+
if (raw == null || raw === "") return fallback;
|
|
1393
|
+
if (typeof raw === "string") {
|
|
1394
|
+
try {
|
|
1395
|
+
return JSON.parse(raw);
|
|
1396
|
+
} catch {
|
|
1397
|
+
return fallback;
|
|
1398
|
+
}
|
|
934
1399
|
}
|
|
935
|
-
return
|
|
1400
|
+
return raw;
|
|
936
1401
|
}
|
|
937
|
-
async function
|
|
1402
|
+
async function pendingRequestFor(engine, objectName, recordId) {
|
|
938
1403
|
try {
|
|
939
1404
|
const rows = await engine.find("sys_approval_request", {
|
|
940
1405
|
where: { object_name: objectName, record_id: String(recordId), status: "pending" },
|
|
941
1406
|
limit: 1
|
|
942
1407
|
});
|
|
943
|
-
return Array.isArray(rows) && rows
|
|
1408
|
+
return Array.isArray(rows) && rows[0] ? rows[0] : null;
|
|
944
1409
|
} catch {
|
|
945
|
-
return
|
|
1410
|
+
return null;
|
|
946
1411
|
}
|
|
947
1412
|
}
|
|
948
|
-
function
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
if (!
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
try {
|
|
973
|
-
const result = ctx?.result ?? {};
|
|
974
|
-
const id = String(ctx?.input?.id ?? result?.id ?? "");
|
|
975
|
-
if (!id) return;
|
|
976
|
-
const record = {
|
|
977
|
-
...ctx?.previous ?? {},
|
|
978
|
-
...result?.id ? result : {},
|
|
979
|
-
...ctx?.input?.data ?? {},
|
|
980
|
-
id
|
|
981
|
-
};
|
|
982
|
-
for (const proc of procs) {
|
|
983
|
-
await tryAutoSubmit(engine, service, proc, objectName, id, record, ctx, logger);
|
|
984
|
-
}
|
|
985
|
-
} catch (err) {
|
|
986
|
-
logger?.warn?.("[approvals] afterUpdate auto-trigger failed", { error: err?.message });
|
|
987
|
-
}
|
|
988
|
-
}, { object: objectName, packageId: APPROVALS_HOOK_PACKAGE, priority: 200 });
|
|
989
|
-
const lockProcs = procs.filter((p) => p.definition?.lockRecord !== false);
|
|
990
|
-
if (lockProcs.length === 0) continue;
|
|
991
|
-
engine.registerHook("beforeUpdate", async (ctx) => {
|
|
992
|
-
const id = String(ctx?.input?.id ?? "");
|
|
993
|
-
if (!id) return;
|
|
994
|
-
const data = ctx?.input?.data ?? {};
|
|
995
|
-
const changedFields = Object.keys(data).filter((k) => k !== "id" && k !== "updated_at");
|
|
996
|
-
if (changedFields.length === 0) return;
|
|
997
|
-
if (ctx?.session?.isSystem) return;
|
|
998
|
-
const mirrorFields = /* @__PURE__ */ new Set();
|
|
999
|
-
for (const p of lockProcs) {
|
|
1000
|
-
const f = p.definition?.approvalStatusField;
|
|
1001
|
-
if (typeof f === "string" && f) mirrorFields.add(f);
|
|
1002
|
-
}
|
|
1003
|
-
const onlyMirror = changedFields.every((f) => mirrorFields.has(f));
|
|
1004
|
-
if (onlyMirror) return;
|
|
1005
|
-
const roles = ctx?.session?.roles ?? [];
|
|
1006
|
-
if (Array.isArray(roles) && roles.includes("admin")) return;
|
|
1007
|
-
const pending = await hasPendingRequest(engine, objectName, id);
|
|
1008
|
-
if (!pending) return;
|
|
1009
|
-
const err = new Error("RECORD_LOCKED: record is locked while an approval is in progress");
|
|
1010
|
-
err.code = "RECORD_LOCKED";
|
|
1011
|
-
err.statusCode = 409;
|
|
1012
|
-
throw err;
|
|
1013
|
-
}, { object: objectName, packageId: APPROVALS_HOOK_PACKAGE, priority: 50 });
|
|
1014
|
-
}
|
|
1015
|
-
logger?.info?.("[approvals] lifecycle hooks bound", {
|
|
1016
|
-
objects: Array.from(byObject.keys()),
|
|
1017
|
-
processCount: processes.length
|
|
1018
|
-
});
|
|
1413
|
+
function bindApprovalLockHook(engine, logger) {
|
|
1414
|
+
engine.registerHook("beforeUpdate", async (ctx) => {
|
|
1415
|
+
const id = String(ctx?.input?.id ?? "");
|
|
1416
|
+
if (!id) return;
|
|
1417
|
+
const object = ctx?.object ?? ctx?.objectName;
|
|
1418
|
+
if (!object || String(object).startsWith("sys_approval")) return;
|
|
1419
|
+
const data = ctx?.input?.data ?? {};
|
|
1420
|
+
const changedFields = Object.keys(data).filter((k) => k !== "id" && k !== "updated_at");
|
|
1421
|
+
if (changedFields.length === 0) return;
|
|
1422
|
+
if (ctx?.session?.isSystem) return;
|
|
1423
|
+
const roles = ctx?.session?.roles ?? [];
|
|
1424
|
+
if (Array.isArray(roles) && roles.includes("admin")) return;
|
|
1425
|
+
const pending = await pendingRequestFor(engine, object, id);
|
|
1426
|
+
if (!pending) return;
|
|
1427
|
+
const config = parseJson2(pending.node_config_json, {});
|
|
1428
|
+
if (config?.lockRecord === false) return;
|
|
1429
|
+
const mirror = config?.approvalStatusField;
|
|
1430
|
+
if (typeof mirror === "string" && mirror && changedFields.every((f) => f === mirror)) return;
|
|
1431
|
+
const err = new Error("RECORD_LOCKED: record is locked while an approval is in progress");
|
|
1432
|
+
err.code = "RECORD_LOCKED";
|
|
1433
|
+
err.statusCode = 409;
|
|
1434
|
+
throw err;
|
|
1435
|
+
}, { packageId: APPROVALS_HOOK_PACKAGE, priority: 50 });
|
|
1436
|
+
logger?.info?.("[approvals] record-lock hook bound");
|
|
1019
1437
|
}
|
|
1020
1438
|
function unbindAllHooks(engine) {
|
|
1021
1439
|
return engine.unregisterHooksByPackage(APPROVALS_HOOK_PACKAGE);
|
|
1022
1440
|
}
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1441
|
+
|
|
1442
|
+
// src/approval-node.ts
|
|
1443
|
+
import {
|
|
1444
|
+
defineActionDescriptor,
|
|
1445
|
+
ApprovalNodeConfigSchema,
|
|
1446
|
+
getApprovalNodeConfigJsonSchema,
|
|
1447
|
+
APPROVAL_NODE_TYPE
|
|
1448
|
+
} from "@objectstack/spec/automation";
|
|
1449
|
+
var SYSTEM_CTX2 = { isSystem: true, roles: [], permissions: [] };
|
|
1450
|
+
function registerApprovalNode(automation, service, logger) {
|
|
1451
|
+
automation.registerNodeExecutor({
|
|
1452
|
+
type: APPROVAL_NODE_TYPE,
|
|
1453
|
+
descriptor: defineActionDescriptor({
|
|
1454
|
+
type: APPROVAL_NODE_TYPE,
|
|
1455
|
+
version: "1.0.0",
|
|
1456
|
+
name: "Approval",
|
|
1457
|
+
description: "Route a record for human approval; suspends the flow until a decision, then continues down the approve / reject branch.",
|
|
1458
|
+
icon: "check-circle",
|
|
1459
|
+
category: "human",
|
|
1460
|
+
paradigms: ["flow"],
|
|
1461
|
+
source: "plugin",
|
|
1462
|
+
// Human decision: the run suspends here awaiting an external reply.
|
|
1463
|
+
supportsPause: true,
|
|
1464
|
+
isAsync: true,
|
|
1465
|
+
// Publish the node's config contract (ADR-0018 §configSchema) so the
|
|
1466
|
+
// Studio flow designer renders the Approval property form from the engine
|
|
1467
|
+
// rather than a hardcoded client form — the engine owns the shape.
|
|
1468
|
+
configSchema: getApprovalNodeConfigJsonSchema()
|
|
1469
|
+
}),
|
|
1470
|
+
async execute(node, variables, context) {
|
|
1471
|
+
const parsed = ApprovalNodeConfigSchema.safeParse(node.config ?? {});
|
|
1472
|
+
if (!parsed.success) {
|
|
1473
|
+
const msg = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
1474
|
+
return { success: false, error: `Approval node '${node.id}' has invalid config: ${msg}` };
|
|
1475
|
+
}
|
|
1476
|
+
const config = parsed.data;
|
|
1477
|
+
const runId = variables.get("$runId");
|
|
1478
|
+
const record = variables.get("$record") ?? context?.record ?? {};
|
|
1479
|
+
const object = context?.object ?? record?.object_name;
|
|
1480
|
+
const recordId = record?.id;
|
|
1481
|
+
if (!runId) return { success: false, error: `Approval node '${node.id}': missing $runId` };
|
|
1482
|
+
if (!object) return { success: false, error: `Approval node '${node.id}': no target object in context` };
|
|
1483
|
+
if (!recordId) return { success: false, error: `Approval node '${node.id}': no record id in $record` };
|
|
1484
|
+
try {
|
|
1485
|
+
const request = await service.openNodeRequest({
|
|
1486
|
+
object,
|
|
1487
|
+
recordId: String(recordId),
|
|
1488
|
+
runId: String(runId),
|
|
1489
|
+
nodeId: node.id,
|
|
1490
|
+
config,
|
|
1491
|
+
flowName: context?.flowName,
|
|
1492
|
+
submitterId: context?.userId ?? null,
|
|
1493
|
+
record,
|
|
1494
|
+
organizationId: context?.organizationId ?? context?.tenantId ?? null
|
|
1495
|
+
}, {
|
|
1496
|
+
...SYSTEM_CTX2,
|
|
1497
|
+
userId: context?.userId,
|
|
1498
|
+
organizationId: context?.organizationId,
|
|
1499
|
+
tenantId: context?.tenantId
|
|
1500
|
+
});
|
|
1501
|
+
logger?.info?.("[approvals] approval node suspended run", {
|
|
1502
|
+
node: node.id,
|
|
1503
|
+
request: request.id,
|
|
1504
|
+
run: String(runId)
|
|
1505
|
+
});
|
|
1506
|
+
return { success: true, suspend: true, correlation: request.id };
|
|
1507
|
+
} catch (err) {
|
|
1508
|
+
return { success: false, error: `Approval node '${node.id}': ${err?.message ?? String(err)}` };
|
|
1509
|
+
}
|
|
1033
1510
|
}
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
await service.submit({
|
|
1037
|
-
object: objectName,
|
|
1038
|
-
recordId,
|
|
1039
|
-
processName: process.name,
|
|
1040
|
-
payload: record,
|
|
1041
|
-
submitterId
|
|
1042
|
-
}, { ...SYSTEM_CTX3, userId: submitterId ?? void 0, organizationId: submitterOrg ?? void 0, tenantId: submitterOrg ?? void 0 });
|
|
1043
|
-
logger?.info?.("[approvals] auto-submitted approval", {
|
|
1044
|
-
process: process.name,
|
|
1045
|
-
object: objectName,
|
|
1046
|
-
record: recordId
|
|
1047
|
-
});
|
|
1048
|
-
} catch (err) {
|
|
1049
|
-
if (err?.code === "DUPLICATE_REQUEST") return;
|
|
1050
|
-
logger?.warn?.("[approvals] auto-submit failed", {
|
|
1051
|
-
process: process.name,
|
|
1052
|
-
object: objectName,
|
|
1053
|
-
record: recordId,
|
|
1054
|
-
error: err?.message ?? String(err)
|
|
1055
|
-
});
|
|
1056
|
-
}
|
|
1511
|
+
});
|
|
1512
|
+
logger?.info?.("[approvals] approval node executor registered");
|
|
1057
1513
|
}
|
|
1058
1514
|
|
|
1059
1515
|
// src/approvals-plugin.ts
|
|
@@ -1074,8 +1530,36 @@ var ApprovalsServicePlugin = class {
|
|
|
1074
1530
|
scope: "system",
|
|
1075
1531
|
defaultDatasource: "cloud",
|
|
1076
1532
|
namespace: "sys",
|
|
1077
|
-
objects: [
|
|
1533
|
+
objects: [SysApprovalRequest, SysApprovalAction],
|
|
1534
|
+
// ADR-0029 D7 — contribute the Approvals entries into the Setup app's
|
|
1535
|
+
// `group_approvals` slot. This plugin owns these objects (K2.b), so it
|
|
1536
|
+
// ships their menu too; when the plugin isn't installed the slot is empty.
|
|
1537
|
+
navigationContributions: [
|
|
1538
|
+
{
|
|
1539
|
+
app: "setup",
|
|
1540
|
+
group: "group_approvals",
|
|
1541
|
+
priority: 100,
|
|
1542
|
+
items: [
|
|
1543
|
+
{ id: "nav_approval_requests", type: "object", label: "Requests", objectName: "sys_approval_request", icon: "inbox", requiresObject: "sys_approval_request" },
|
|
1544
|
+
{ id: "nav_approval_actions", type: "object", label: "Action History", objectName: "sys_approval_action", icon: "history", requiresObject: "sys_approval_action" }
|
|
1545
|
+
]
|
|
1546
|
+
}
|
|
1547
|
+
]
|
|
1078
1548
|
});
|
|
1549
|
+
if (typeof ctx.hook === "function") {
|
|
1550
|
+
ctx.hook("kernel:ready", async () => {
|
|
1551
|
+
try {
|
|
1552
|
+
const i18n = ctx.getService("i18n");
|
|
1553
|
+
if (i18n && typeof i18n.loadTranslations === "function") {
|
|
1554
|
+
const { ApprovalsTranslations: ApprovalsTranslations2 } = await Promise.resolve().then(() => (init_translations(), translations_exports));
|
|
1555
|
+
for (const [locale, data] of Object.entries(ApprovalsTranslations2)) {
|
|
1556
|
+
i18n.loadTranslations(locale, data);
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
} catch {
|
|
1560
|
+
}
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1079
1563
|
ctx.logger.info("ApprovalsServicePlugin: schemas registered");
|
|
1080
1564
|
}
|
|
1081
1565
|
async start(ctx) {
|
|
@@ -1094,47 +1578,28 @@ var ApprovalsServicePlugin = class {
|
|
|
1094
1578
|
return;
|
|
1095
1579
|
}
|
|
1096
1580
|
this.engine = engine;
|
|
1097
|
-
this.logger = ctx.logger;
|
|
1098
|
-
let metadataRepo;
|
|
1099
|
-
try {
|
|
1100
|
-
const meta = ctx.getService("metadata");
|
|
1101
|
-
metadataRepo = meta?.getRepository?.();
|
|
1102
|
-
} catch {
|
|
1103
|
-
}
|
|
1104
1581
|
this.service = new ApprovalService({
|
|
1105
1582
|
engine,
|
|
1106
|
-
logger: ctx.logger
|
|
1107
|
-
metadataRepo
|
|
1583
|
+
logger: ctx.logger
|
|
1108
1584
|
});
|
|
1109
|
-
if (metadataRepo) {
|
|
1110
|
-
ctx.logger.info("ApprovalsServicePlugin: execution pinning enabled (ADR-0009)");
|
|
1111
|
-
}
|
|
1112
1585
|
if (!this.options.disableAutoHooks) {
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
await this.rebindHooks();
|
|
1119
|
-
});
|
|
1120
|
-
} catch {
|
|
1121
|
-
await this.rebindHooks();
|
|
1122
|
-
}
|
|
1123
|
-
} else {
|
|
1124
|
-
await this.rebindHooks();
|
|
1586
|
+
try {
|
|
1587
|
+
unbindAllHooks(engine);
|
|
1588
|
+
bindApprovalLockHook(engine, ctx.logger);
|
|
1589
|
+
} catch (err) {
|
|
1590
|
+
ctx.logger.warn?.("[approvals] failed to bind record-lock hook", { error: err?.message });
|
|
1125
1591
|
}
|
|
1126
1592
|
}
|
|
1127
1593
|
ctx.registerService("approvals", this.service);
|
|
1128
1594
|
ctx.logger.info("ApprovalsServicePlugin: service registered");
|
|
1129
|
-
}
|
|
1130
|
-
async rebindHooks() {
|
|
1131
|
-
if (!this.engine || !this.service) return;
|
|
1132
1595
|
try {
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1596
|
+
const automation = ctx.getService("automation");
|
|
1597
|
+
if (automation && typeof automation.registerNodeExecutor === "function") {
|
|
1598
|
+
this.service.attachAutomation(automation);
|
|
1599
|
+
registerApprovalNode(automation, this.service, ctx.logger);
|
|
1600
|
+
}
|
|
1601
|
+
} catch {
|
|
1602
|
+
ctx.logger.info("ApprovalsServicePlugin: no automation engine \u2014 approval node not registered");
|
|
1138
1603
|
}
|
|
1139
1604
|
}
|
|
1140
1605
|
async stop(_ctx) {
|
|
@@ -1149,8 +1614,8 @@ var ApprovalsServicePlugin = class {
|
|
|
1149
1614
|
export {
|
|
1150
1615
|
ApprovalService,
|
|
1151
1616
|
ApprovalsServicePlugin,
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1617
|
+
SysApprovalAction,
|
|
1618
|
+
SysApprovalRequest,
|
|
1619
|
+
registerApprovalNode
|
|
1155
1620
|
};
|
|
1156
1621
|
//# sourceMappingURL=index.mjs.map
|