@objectstack/plugin-approvals 8.0.1 → 9.0.1
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 +25 -0
- package/dist/index.d.mts +33 -9
- package/dist/index.d.ts +33 -9
- package/dist/index.js +193 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +193 -5
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -8
- package/src/approval-node.test.ts +14 -0
- package/src/approval-node.ts +9 -1
- package/src/approval-service.test.ts +92 -0
- package/src/approval-service.ts +203 -2
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @objectstack/plugin-approvals@
|
|
2
|
+
> @objectstack/plugin-approvals@9.0.1 build /home/runner/work/framework/framework/packages/plugins/plugin-approvals
|
|
3
3
|
> tsup --config ../../../tsup.config.ts
|
|
4
4
|
|
|
5
5
|
[34mCLI[39m Building entry: src/index.ts
|
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
[34mCLI[39m Cleaning output folder
|
|
11
11
|
[34mESM[39m Build start
|
|
12
12
|
[34mCJS[39m Build start
|
|
13
|
-
[32mCJS[39m [1mdist/index.js [22m[
|
|
14
|
-
[32mCJS[39m [1mdist/index.js.map [22m[
|
|
15
|
-
[32mCJS[39m ⚡️ Build success in
|
|
16
|
-
[32mESM[39m [1mdist/index.mjs [22m[
|
|
17
|
-
[32mESM[39m [1mdist/index.mjs.map [22m[
|
|
18
|
-
[32mESM[39m ⚡️ Build success in
|
|
13
|
+
[32mCJS[39m [1mdist/index.js [22m[32m64.93 KB[39m
|
|
14
|
+
[32mCJS[39m [1mdist/index.js.map [22m[32m113.45 KB[39m
|
|
15
|
+
[32mCJS[39m ⚡️ Build success in 189ms
|
|
16
|
+
[32mESM[39m [1mdist/index.mjs [22m[32m63.58 KB[39m
|
|
17
|
+
[32mESM[39m [1mdist/index.mjs.map [22m[32m112.27 KB[39m
|
|
18
|
+
[32mESM[39m ⚡️ Build success in 190ms
|
|
19
19
|
[34mDTS[39m Build start
|
|
20
|
-
[32mDTS[39m ⚡️ Build success in
|
|
21
|
-
[32mDTS[39m [1mdist/index.d.mts [22m[
|
|
22
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[
|
|
20
|
+
[32mDTS[39m ⚡️ Build success in 24542ms
|
|
21
|
+
[32mDTS[39m [1mdist/index.d.mts [22m[32m334.31 KB[39m
|
|
22
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m334.31 KB[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# @objectstack/plugin-approvals
|
|
2
2
|
|
|
3
|
+
## 9.0.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [1817845]
|
|
8
|
+
- @objectstack/spec@9.0.1
|
|
9
|
+
- @objectstack/core@9.0.1
|
|
10
|
+
- @objectstack/formula@9.0.1
|
|
11
|
+
- @objectstack/metadata-core@9.0.1
|
|
12
|
+
- @objectstack/platform-objects@9.0.1
|
|
13
|
+
|
|
14
|
+
## 9.0.0
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- Updated dependencies [4c3f693]
|
|
19
|
+
- Updated dependencies [0bf39f1]
|
|
20
|
+
- Updated dependencies [f533f42]
|
|
21
|
+
- Updated dependencies [1c83ee8]
|
|
22
|
+
- @objectstack/spec@9.0.0
|
|
23
|
+
- @objectstack/core@9.0.0
|
|
24
|
+
- @objectstack/formula@9.0.0
|
|
25
|
+
- @objectstack/metadata-core@9.0.0
|
|
26
|
+
- @objectstack/platform-objects@9.0.0
|
|
27
|
+
|
|
3
28
|
## 8.0.1
|
|
4
29
|
|
|
5
30
|
### Patch Changes
|
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as _objectstack_spec_data from '@objectstack/spec/data';
|
|
2
2
|
import { ApprovalNodeConfig } from '@objectstack/spec/automation';
|
|
3
|
-
import { IApprovalService, SharingExecutionContext, ApprovalRequestRow, ApprovalDecisionInput, ApprovalDecisionResult, ApprovalStatus, ApprovalActionRow } from '@objectstack/spec/contracts';
|
|
3
|
+
import { IApprovalService, SharingExecutionContext, ApprovalRequestRow, ApprovalDecisionInput, ApprovalDecisionResult, ApprovalRecallInput, ApprovalRecallResult, ApprovalStatus, ApprovalActionRow } from '@objectstack/spec/contracts';
|
|
4
4
|
export { ApprovalActionRow, ApprovalDecisionInput, ApprovalDecisionResult, ApprovalRequestRow, ApprovalStatus, IApprovalService } from '@objectstack/spec/contracts';
|
|
5
5
|
import { Plugin, PluginContext } from '@objectstack/core';
|
|
6
6
|
|
|
@@ -507,10 +507,9 @@ declare const SysApprovalRequest: Omit<{
|
|
|
507
507
|
} | undefined;
|
|
508
508
|
chart?: {
|
|
509
509
|
chartType: "bar" | "line" | "pie" | "area" | "scatter";
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
groupByField?: string | undefined;
|
|
510
|
+
dataset: string;
|
|
511
|
+
values: string[];
|
|
512
|
+
dimensions?: string[] | undefined;
|
|
514
513
|
} | undefined;
|
|
515
514
|
description?: string | undefined;
|
|
516
515
|
sharing?: {
|
|
@@ -5164,10 +5163,9 @@ declare const SysApprovalAction: Omit<{
|
|
|
5164
5163
|
} | undefined;
|
|
5165
5164
|
chart?: {
|
|
5166
5165
|
chartType: "bar" | "line" | "pie" | "area" | "scatter";
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
groupByField?: string | undefined;
|
|
5166
|
+
dataset: string;
|
|
5167
|
+
values: string[];
|
|
5168
|
+
dimensions?: string[] | undefined;
|
|
5171
5169
|
} | undefined;
|
|
5172
5170
|
description?: string | undefined;
|
|
5173
5171
|
sharing?: {
|
|
@@ -7504,6 +7502,10 @@ declare class ApprovalService implements IApprovalService {
|
|
|
7504
7502
|
nodeId: string;
|
|
7505
7503
|
config: ApprovalNodeConfig;
|
|
7506
7504
|
flowName?: string;
|
|
7505
|
+
/** Authored flow label, snapshotted for inbox display. */
|
|
7506
|
+
flowLabel?: string;
|
|
7507
|
+
/** Authored node label, snapshotted for inbox display. */
|
|
7508
|
+
nodeLabel?: string;
|
|
7507
7509
|
submitterId?: string | null;
|
|
7508
7510
|
record?: any;
|
|
7509
7511
|
organizationId?: string | null;
|
|
@@ -7532,6 +7534,28 @@ declare class ApprovalService implements IApprovalService {
|
|
|
7532
7534
|
* resumes the owning flow run down the matching `approve` / `reject` edge.
|
|
7533
7535
|
*/
|
|
7534
7536
|
decide(requestId: string, input: ApprovalDecisionInput, context: SharingExecutionContext): Promise<ApprovalDecisionResult>;
|
|
7537
|
+
/**
|
|
7538
|
+
* Withdraw a pending request (submitter only). Finalises the row as
|
|
7539
|
+
* `recalled`, releases the record lock (keyed on pending status), mirrors
|
|
7540
|
+
* the status field when configured, and resumes the owning flow run down
|
|
7541
|
+
* the `reject` branch with `output.decision = 'recall'` — the engine has no
|
|
7542
|
+
* run-cancel primitive, and leaving the run suspended forever would leak it.
|
|
7543
|
+
*/
|
|
7544
|
+
recall(requestId: string, input: ApprovalRecallInput, context: SharingExecutionContext): Promise<ApprovalRecallResult>;
|
|
7545
|
+
/**
|
|
7546
|
+
* Resolve the schema-declared display field for an object, when the engine
|
|
7547
|
+
* exposes schema metadata (`getSchema`). Falls back to common title-ish
|
|
7548
|
+
* field names so plain `ApprovalEngine` fakes still enrich sensibly.
|
|
7549
|
+
*/
|
|
7550
|
+
private resolveDisplayField;
|
|
7551
|
+
private static pickTitle;
|
|
7552
|
+
/**
|
|
7553
|
+
* Attach inbox display fields (`record_title`, `submitter_name`) to rows.
|
|
7554
|
+
* Batched: one query per distinct target object plus one `sys_user` lookup.
|
|
7555
|
+
* Best-effort — a deleted record falls back to the payload snapshot, and a
|
|
7556
|
+
* lookup failure leaves the field unset rather than failing the list.
|
|
7557
|
+
*/
|
|
7558
|
+
private enrichRows;
|
|
7535
7559
|
listRequests(filter: {
|
|
7536
7560
|
object?: string;
|
|
7537
7561
|
recordId?: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as _objectstack_spec_data from '@objectstack/spec/data';
|
|
2
2
|
import { ApprovalNodeConfig } from '@objectstack/spec/automation';
|
|
3
|
-
import { IApprovalService, SharingExecutionContext, ApprovalRequestRow, ApprovalDecisionInput, ApprovalDecisionResult, ApprovalStatus, ApprovalActionRow } from '@objectstack/spec/contracts';
|
|
3
|
+
import { IApprovalService, SharingExecutionContext, ApprovalRequestRow, ApprovalDecisionInput, ApprovalDecisionResult, ApprovalRecallInput, ApprovalRecallResult, ApprovalStatus, ApprovalActionRow } from '@objectstack/spec/contracts';
|
|
4
4
|
export { ApprovalActionRow, ApprovalDecisionInput, ApprovalDecisionResult, ApprovalRequestRow, ApprovalStatus, IApprovalService } from '@objectstack/spec/contracts';
|
|
5
5
|
import { Plugin, PluginContext } from '@objectstack/core';
|
|
6
6
|
|
|
@@ -507,10 +507,9 @@ declare const SysApprovalRequest: Omit<{
|
|
|
507
507
|
} | undefined;
|
|
508
508
|
chart?: {
|
|
509
509
|
chartType: "bar" | "line" | "pie" | "area" | "scatter";
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
groupByField?: string | undefined;
|
|
510
|
+
dataset: string;
|
|
511
|
+
values: string[];
|
|
512
|
+
dimensions?: string[] | undefined;
|
|
514
513
|
} | undefined;
|
|
515
514
|
description?: string | undefined;
|
|
516
515
|
sharing?: {
|
|
@@ -5164,10 +5163,9 @@ declare const SysApprovalAction: Omit<{
|
|
|
5164
5163
|
} | undefined;
|
|
5165
5164
|
chart?: {
|
|
5166
5165
|
chartType: "bar" | "line" | "pie" | "area" | "scatter";
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
groupByField?: string | undefined;
|
|
5166
|
+
dataset: string;
|
|
5167
|
+
values: string[];
|
|
5168
|
+
dimensions?: string[] | undefined;
|
|
5171
5169
|
} | undefined;
|
|
5172
5170
|
description?: string | undefined;
|
|
5173
5171
|
sharing?: {
|
|
@@ -7504,6 +7502,10 @@ declare class ApprovalService implements IApprovalService {
|
|
|
7504
7502
|
nodeId: string;
|
|
7505
7503
|
config: ApprovalNodeConfig;
|
|
7506
7504
|
flowName?: string;
|
|
7505
|
+
/** Authored flow label, snapshotted for inbox display. */
|
|
7506
|
+
flowLabel?: string;
|
|
7507
|
+
/** Authored node label, snapshotted for inbox display. */
|
|
7508
|
+
nodeLabel?: string;
|
|
7507
7509
|
submitterId?: string | null;
|
|
7508
7510
|
record?: any;
|
|
7509
7511
|
organizationId?: string | null;
|
|
@@ -7532,6 +7534,28 @@ declare class ApprovalService implements IApprovalService {
|
|
|
7532
7534
|
* resumes the owning flow run down the matching `approve` / `reject` edge.
|
|
7533
7535
|
*/
|
|
7534
7536
|
decide(requestId: string, input: ApprovalDecisionInput, context: SharingExecutionContext): Promise<ApprovalDecisionResult>;
|
|
7537
|
+
/**
|
|
7538
|
+
* Withdraw a pending request (submitter only). Finalises the row as
|
|
7539
|
+
* `recalled`, releases the record lock (keyed on pending status), mirrors
|
|
7540
|
+
* the status field when configured, and resumes the owning flow run down
|
|
7541
|
+
* the `reject` branch with `output.decision = 'recall'` — the engine has no
|
|
7542
|
+
* run-cancel primitive, and leaving the run suspended forever would leak it.
|
|
7543
|
+
*/
|
|
7544
|
+
recall(requestId: string, input: ApprovalRecallInput, context: SharingExecutionContext): Promise<ApprovalRecallResult>;
|
|
7545
|
+
/**
|
|
7546
|
+
* Resolve the schema-declared display field for an object, when the engine
|
|
7547
|
+
* exposes schema metadata (`getSchema`). Falls back to common title-ish
|
|
7548
|
+
* field names so plain `ApprovalEngine` fakes still enrich sensibly.
|
|
7549
|
+
*/
|
|
7550
|
+
private resolveDisplayField;
|
|
7551
|
+
private static pickTitle;
|
|
7552
|
+
/**
|
|
7553
|
+
* Attach inbox display fields (`record_title`, `submitter_name`) to rows.
|
|
7554
|
+
* Batched: one query per distinct target object plus one `sys_user` lookup.
|
|
7555
|
+
* Best-effort — a deleted record falls back to the payload snapshot, and a
|
|
7556
|
+
* lookup failure leaves the field unset rather than failing the list.
|
|
7557
|
+
*/
|
|
7558
|
+
private enrichRows;
|
|
7535
7559
|
listRequests(filter: {
|
|
7536
7560
|
object?: string;
|
|
7537
7561
|
recordId?: string;
|
package/dist/index.js
CHANGED
|
@@ -976,7 +976,14 @@ function csvSplit(raw) {
|
|
|
976
976
|
if (Array.isArray(raw)) return raw.map(String).filter(Boolean);
|
|
977
977
|
return String(raw).split(",").map((s) => s.trim()).filter(Boolean);
|
|
978
978
|
}
|
|
979
|
+
function prettifyMachineName(raw) {
|
|
980
|
+
if (!raw) return void 0;
|
|
981
|
+
const base = String(raw).replace(/^flow:/, "").trim();
|
|
982
|
+
if (!base) return void 0;
|
|
983
|
+
return base.split(/[_\-\s]+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
984
|
+
}
|
|
979
985
|
function rowFromRequest(row) {
|
|
986
|
+
const cfg = parseJson(row.node_config_json, void 0);
|
|
980
987
|
return {
|
|
981
988
|
id: String(row.id),
|
|
982
989
|
organization_id: row.organization_id ?? void 0,
|
|
@@ -994,7 +1001,11 @@ function rowFromRequest(row) {
|
|
|
994
1001
|
flow_node_id: row.flow_node_id ?? void 0,
|
|
995
1002
|
completed_at: row.completed_at ?? void 0,
|
|
996
1003
|
created_at: row.created_at ?? void 0,
|
|
997
|
-
updated_at: row.updated_at ?? void 0
|
|
1004
|
+
updated_at: row.updated_at ?? void 0,
|
|
1005
|
+
// The row is created at submission time; expose the stable inbox-facing name.
|
|
1006
|
+
submitted_at: row.created_at ?? void 0,
|
|
1007
|
+
process_label: cfg?.__flowLabel ?? prettifyMachineName(row.process_name),
|
|
1008
|
+
step_label: cfg?.__nodeLabel ?? prettifyMachineName(row.current_step)
|
|
998
1009
|
};
|
|
999
1010
|
}
|
|
1000
1011
|
function rowFromAction(row) {
|
|
@@ -1009,7 +1020,7 @@ function rowFromAction(row) {
|
|
|
1009
1020
|
created_at: row.created_at ?? void 0
|
|
1010
1021
|
};
|
|
1011
1022
|
}
|
|
1012
|
-
var ApprovalService = class {
|
|
1023
|
+
var ApprovalService = class _ApprovalService {
|
|
1013
1024
|
constructor(opts) {
|
|
1014
1025
|
this.engine = opts.engine;
|
|
1015
1026
|
this.clock = opts.clock ?? { now: () => /* @__PURE__ */ new Date() };
|
|
@@ -1211,6 +1222,9 @@ var ApprovalService = class {
|
|
|
1211
1222
|
const now = this.clock.now().toISOString();
|
|
1212
1223
|
const id = uid("areq");
|
|
1213
1224
|
const processName = `flow:${input.flowName ?? input.nodeId}`;
|
|
1225
|
+
const configSnapshot = { ...input.config };
|
|
1226
|
+
if (input.flowLabel) configSnapshot.__flowLabel = input.flowLabel;
|
|
1227
|
+
if (input.nodeLabel) configSnapshot.__nodeLabel = input.nodeLabel;
|
|
1214
1228
|
const row = {
|
|
1215
1229
|
id,
|
|
1216
1230
|
process_name: processName,
|
|
@@ -1224,7 +1238,7 @@ var ApprovalService = class {
|
|
|
1224
1238
|
payload_json: input.record != null ? JSON.stringify(input.record) : null,
|
|
1225
1239
|
flow_run_id: input.runId,
|
|
1226
1240
|
flow_node_id: input.nodeId,
|
|
1227
|
-
node_config_json: JSON.stringify(
|
|
1241
|
+
node_config_json: JSON.stringify(configSnapshot),
|
|
1228
1242
|
organization_id: ctxOrg,
|
|
1229
1243
|
created_at: now,
|
|
1230
1244
|
updated_at: now
|
|
@@ -1356,6 +1370,172 @@ var ApprovalService = class {
|
|
|
1356
1370
|
resumed
|
|
1357
1371
|
};
|
|
1358
1372
|
}
|
|
1373
|
+
/**
|
|
1374
|
+
* Withdraw a pending request (submitter only). Finalises the row as
|
|
1375
|
+
* `recalled`, releases the record lock (keyed on pending status), mirrors
|
|
1376
|
+
* the status field when configured, and resumes the owning flow run down
|
|
1377
|
+
* the `reject` branch with `output.decision = 'recall'` — the engine has no
|
|
1378
|
+
* run-cancel primitive, and leaving the run suspended forever would leak it.
|
|
1379
|
+
*/
|
|
1380
|
+
async recall(requestId, input, context) {
|
|
1381
|
+
if (!requestId) throw new Error("VALIDATION_FAILED: requestId is required");
|
|
1382
|
+
if (!input?.actorId) throw new Error("VALIDATION_FAILED: actorId is required");
|
|
1383
|
+
const rawRows = await this.engine.find("sys_approval_request", {
|
|
1384
|
+
where: { id: requestId },
|
|
1385
|
+
limit: 1,
|
|
1386
|
+
context: SYSTEM_CTX
|
|
1387
|
+
});
|
|
1388
|
+
const raw = Array.isArray(rawRows) ? rawRows[0] : null;
|
|
1389
|
+
if (!raw) throw new Error(`REQUEST_NOT_FOUND: ${requestId}`);
|
|
1390
|
+
if (raw.status !== "pending") throw new Error(`INVALID_STATE: request is ${raw.status}`);
|
|
1391
|
+
if (!context.isSystem && raw.submitter_id && String(raw.submitter_id) !== String(input.actorId)) {
|
|
1392
|
+
throw new Error(`FORBIDDEN: only the submitter may recall this request`);
|
|
1393
|
+
}
|
|
1394
|
+
const config = parseJson(raw.node_config_json, { approvers: [], behavior: "first_response" });
|
|
1395
|
+
const org = raw.organization_id ?? null;
|
|
1396
|
+
const nodeId = raw.flow_node_id ?? raw.current_step ?? null;
|
|
1397
|
+
const runId = raw.flow_run_id ?? null;
|
|
1398
|
+
const now = this.clock.now().toISOString();
|
|
1399
|
+
await this.engine.insert("sys_approval_action", {
|
|
1400
|
+
id: uid("aact"),
|
|
1401
|
+
request_id: requestId,
|
|
1402
|
+
organization_id: org,
|
|
1403
|
+
step_name: nodeId,
|
|
1404
|
+
step_index: 0,
|
|
1405
|
+
action: "recall",
|
|
1406
|
+
actor_id: input.actorId,
|
|
1407
|
+
comment: input.comment ?? null,
|
|
1408
|
+
created_at: now
|
|
1409
|
+
}, { context: SYSTEM_CTX });
|
|
1410
|
+
await this.engine.update("sys_approval_request", {
|
|
1411
|
+
id: requestId,
|
|
1412
|
+
status: "recalled",
|
|
1413
|
+
pending_approvers: null,
|
|
1414
|
+
completed_at: now,
|
|
1415
|
+
updated_at: now
|
|
1416
|
+
}, { context: SYSTEM_CTX });
|
|
1417
|
+
if (config.approvalStatusField) {
|
|
1418
|
+
await this.mirrorStatusField(raw.object_name, raw.record_id, config.approvalStatusField, "recalled");
|
|
1419
|
+
}
|
|
1420
|
+
let resumed = false;
|
|
1421
|
+
if (runId && typeof this.automation?.resume === "function") {
|
|
1422
|
+
try {
|
|
1423
|
+
await this.automation.resume(runId, {
|
|
1424
|
+
branchLabel: import_automation.APPROVAL_BRANCH_LABELS.reject,
|
|
1425
|
+
output: { decision: "recall", requestId }
|
|
1426
|
+
});
|
|
1427
|
+
resumed = true;
|
|
1428
|
+
} catch (err) {
|
|
1429
|
+
this.logger?.warn?.("[approvals] resume after recall failed", {
|
|
1430
|
+
request: requestId,
|
|
1431
|
+
run: runId,
|
|
1432
|
+
error: err?.message ?? String(err)
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
const fresh = await this.getRequest(requestId, context);
|
|
1437
|
+
return { request: fresh, runId, resumed };
|
|
1438
|
+
}
|
|
1439
|
+
// ── Display enrichment ───────────────────────────────────────
|
|
1440
|
+
/**
|
|
1441
|
+
* Resolve the schema-declared display field for an object, when the engine
|
|
1442
|
+
* exposes schema metadata (`getSchema`). Falls back to common title-ish
|
|
1443
|
+
* field names so plain `ApprovalEngine` fakes still enrich sensibly.
|
|
1444
|
+
*/
|
|
1445
|
+
resolveDisplayField(object) {
|
|
1446
|
+
try {
|
|
1447
|
+
const schema = this.engine.getSchema?.(object);
|
|
1448
|
+
const fields = schema?.fields ?? {};
|
|
1449
|
+
const declared = schema?.displayNameField;
|
|
1450
|
+
if (declared && declared !== "id" && fields[declared]) return declared;
|
|
1451
|
+
for (const cand of ["name", "title", "subject", "label"]) {
|
|
1452
|
+
if (fields[cand]) return cand;
|
|
1453
|
+
}
|
|
1454
|
+
} catch {
|
|
1455
|
+
}
|
|
1456
|
+
return void 0;
|
|
1457
|
+
}
|
|
1458
|
+
static pickTitle(rec, displayField) {
|
|
1459
|
+
const candidates = displayField ? [displayField, "name", "title", "subject", "label"] : ["name", "title", "subject", "label"];
|
|
1460
|
+
for (const f of candidates) {
|
|
1461
|
+
const v = rec?.[f];
|
|
1462
|
+
if (v != null && String(v).trim() && f !== "id") return String(v);
|
|
1463
|
+
}
|
|
1464
|
+
return void 0;
|
|
1465
|
+
}
|
|
1466
|
+
/**
|
|
1467
|
+
* Attach inbox display fields (`record_title`, `submitter_name`) to rows.
|
|
1468
|
+
* Batched: one query per distinct target object plus one `sys_user` lookup.
|
|
1469
|
+
* Best-effort — a deleted record falls back to the payload snapshot, and a
|
|
1470
|
+
* lookup failure leaves the field unset rather than failing the list.
|
|
1471
|
+
*/
|
|
1472
|
+
async enrichRows(rows) {
|
|
1473
|
+
if (!rows.length) return;
|
|
1474
|
+
const byObject = /* @__PURE__ */ new Map();
|
|
1475
|
+
for (const r of rows) {
|
|
1476
|
+
if (!r.object_name || !r.record_id) continue;
|
|
1477
|
+
let set = byObject.get(r.object_name);
|
|
1478
|
+
if (!set) {
|
|
1479
|
+
set = /* @__PURE__ */ new Set();
|
|
1480
|
+
byObject.set(r.object_name, set);
|
|
1481
|
+
}
|
|
1482
|
+
set.add(r.record_id);
|
|
1483
|
+
}
|
|
1484
|
+
const titles = /* @__PURE__ */ new Map();
|
|
1485
|
+
for (const [object, idSet] of byObject) {
|
|
1486
|
+
const ids = Array.from(idSet);
|
|
1487
|
+
const displayField = this.resolveDisplayField(object);
|
|
1488
|
+
try {
|
|
1489
|
+
const recs = await this.engine.find(object, {
|
|
1490
|
+
where: { id: { $in: ids } },
|
|
1491
|
+
limit: ids.length,
|
|
1492
|
+
context: SYSTEM_CTX
|
|
1493
|
+
});
|
|
1494
|
+
for (const rec of recs ?? []) {
|
|
1495
|
+
const title = _ApprovalService.pickTitle(rec, displayField);
|
|
1496
|
+
if (rec?.id && title) titles.set(`${object}\0${rec.id}`, title);
|
|
1497
|
+
}
|
|
1498
|
+
} catch {
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
const submitters = Array.from(new Set(rows.map((r) => r.submitter_id).filter(Boolean)));
|
|
1502
|
+
const names = /* @__PURE__ */ new Map();
|
|
1503
|
+
if (submitters.length) {
|
|
1504
|
+
try {
|
|
1505
|
+
const users = await this.engine.find("sys_user", {
|
|
1506
|
+
where: { id: { $in: submitters } },
|
|
1507
|
+
fields: ["id", "name", "email"],
|
|
1508
|
+
limit: submitters.length,
|
|
1509
|
+
context: SYSTEM_CTX
|
|
1510
|
+
});
|
|
1511
|
+
for (const u of users ?? []) {
|
|
1512
|
+
if (u?.id && (u.name || u.email)) names.set(String(u.id), String(u.name ?? u.email));
|
|
1513
|
+
}
|
|
1514
|
+
} catch {
|
|
1515
|
+
}
|
|
1516
|
+
const unresolvedEmails = submitters.filter((s) => !names.has(s) && s.includes("@"));
|
|
1517
|
+
if (unresolvedEmails.length) {
|
|
1518
|
+
try {
|
|
1519
|
+
const users = await this.engine.find("sys_user", {
|
|
1520
|
+
where: { email: { $in: unresolvedEmails } },
|
|
1521
|
+
fields: ["email", "name"],
|
|
1522
|
+
limit: unresolvedEmails.length,
|
|
1523
|
+
context: SYSTEM_CTX
|
|
1524
|
+
});
|
|
1525
|
+
for (const u of users ?? []) {
|
|
1526
|
+
if (u?.email && u.name) names.set(String(u.email), String(u.name));
|
|
1527
|
+
}
|
|
1528
|
+
} catch {
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
for (const r of rows) {
|
|
1533
|
+
const title = titles.get(`${r.object_name}\0${r.record_id}`) ?? _ApprovalService.pickTitle(r.payload, void 0);
|
|
1534
|
+
if (title) r.record_title = title;
|
|
1535
|
+
const name = r.submitter_id ? names.get(String(r.submitter_id)) : void 0;
|
|
1536
|
+
if (name) r.submitter_name = name;
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1359
1539
|
// ── Read API ─────────────────────────────────────────────────
|
|
1360
1540
|
async listRequests(filter, context) {
|
|
1361
1541
|
const f = {};
|
|
@@ -1384,6 +1564,7 @@ var ApprovalService = class {
|
|
|
1384
1564
|
});
|
|
1385
1565
|
}
|
|
1386
1566
|
}
|
|
1567
|
+
await this.enrichRows(list);
|
|
1387
1568
|
return list;
|
|
1388
1569
|
}
|
|
1389
1570
|
async getRequest(requestId, context) {
|
|
@@ -1396,7 +1577,10 @@ var ApprovalService = class {
|
|
|
1396
1577
|
limit: 1,
|
|
1397
1578
|
context: SYSTEM_CTX
|
|
1398
1579
|
});
|
|
1399
|
-
|
|
1580
|
+
if (!Array.isArray(rows) || !rows[0]) return null;
|
|
1581
|
+
const row = rowFromRequest(rows[0]);
|
|
1582
|
+
await this.enrichRows([row]);
|
|
1583
|
+
return row;
|
|
1400
1584
|
}
|
|
1401
1585
|
async listActions(requestId, context) {
|
|
1402
1586
|
if (!requestId) return [];
|
|
@@ -1502,6 +1686,8 @@ function registerApprovalNode(automation, service, logger) {
|
|
|
1502
1686
|
if (!runId) return { success: false, error: `Approval node '${node.id}': missing $runId` };
|
|
1503
1687
|
if (!object) return { success: false, error: `Approval node '${node.id}': no target object in context` };
|
|
1504
1688
|
if (!recordId) return { success: false, error: `Approval node '${node.id}': no record id in $record` };
|
|
1689
|
+
const flowName = variables.get("$flowName") ?? context?.flowName;
|
|
1690
|
+
const flowLabel = variables.get("$flowLabel");
|
|
1505
1691
|
try {
|
|
1506
1692
|
const request = await service.openNodeRequest({
|
|
1507
1693
|
object,
|
|
@@ -1509,7 +1695,9 @@ function registerApprovalNode(automation, service, logger) {
|
|
|
1509
1695
|
runId: String(runId),
|
|
1510
1696
|
nodeId: node.id,
|
|
1511
1697
|
config,
|
|
1512
|
-
flowName
|
|
1698
|
+
flowName,
|
|
1699
|
+
flowLabel,
|
|
1700
|
+
nodeLabel: typeof node.label === "string" ? node.label : void 0,
|
|
1513
1701
|
submitterId: context?.userId ?? null,
|
|
1514
1702
|
record,
|
|
1515
1703
|
organizationId: context?.organizationId ?? context?.tenantId ?? null
|