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