-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathapproval-service.ts
More file actions
173 lines (159 loc) · 6.43 KB
/
Copy pathapproval-service.ts
File metadata and controls
173 lines (159 loc) · 6.43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
/**
* @objectstack/spec/contracts/approval-service
*
* Cross-package contract for the approval runtime. The default
* implementation lives in `@objectstack/plugin-approvals` and is registered
* as the `approvals` service.
*
* ADR-0019: approval is no longer a standalone engine. An approval is a
* **flow node** (`type: 'approval'`) — the flow opens a request on the node
* and suspends; a human decision finalises it and resumes the flow down the
* matching `approve` / `reject` edge. This service owns the runtime state
* (`sys_approval_request` / `sys_approval_action`, approver resolution, record
* lock, status mirror) and the decision API. There is no standalone process
* authoring type, submit, or step machinery anymore.
*/
import type { SharingExecutionContext } from './sharing-service.js';
/** Lifecycle state of an approval request. */
export type ApprovalStatus = 'pending' | 'approved' | 'rejected' | 'recalled';
/** Live request row. */
export interface ApprovalRequestRow {
id: string;
/** Origin of the request — `flow:<flowName|nodeId>` for node-driven approvals. */
process_name: string;
object_name: string;
record_id: string;
submitter_id?: string;
submitter_comment?: string;
status: ApprovalStatus;
/** The flow node id that opened the request (mirrors `flow_node_id`). */
current_step?: string;
current_step_index?: number;
pending_approvers?: string[];
payload?: unknown;
/** ADR-0019 correlation: the suspended flow run this request belongs to. */
flow_run_id?: string;
flow_node_id?: string;
completed_at?: string;
created_at?: string;
updated_at?: string;
/**
* When the request was opened. Alias of `created_at` — the row is created
* at submission time. Kept as its own field so inbox clients have a stable
* name that survives any future split between row-creation and submission.
*/
submitted_at?: string;
// ── Display enrichment (inbox-facing; resolved by the service) ─────
/** Human label of the originating flow (e.g. "Project Budget Approval"). */
process_label?: string;
/** Human label of the approval step / node (e.g. "Manager Review"). */
step_label?: string;
/** Display name of the target record (its name/title field), when resolvable. */
record_title?: string;
/** Display name of the submitter (`sys_user.name`), when resolvable. */
submitter_name?: string;
/** Schema label of the target object (e.g. "Project" for `showcase_project`). */
object_label?: string;
/**
* Display names for user-id entries in `pending_approvers`
* (id → `sys_user.name`). Emails and `role:<r>` entries are not mapped —
* they are already human-readable.
*/
pending_approver_names?: Record<string, string>;
/**
* Display values for lookup fields in `payload` (field key → referenced
* record's display name), so inbox summaries never show foreign-key ids.
*/
payload_display?: Record<string, string>;
}
/** Audit row. */
export interface ApprovalActionRow {
id: string;
request_id: string;
step_name?: string;
step_index?: number;
action: 'submit' | 'approve' | 'reject' | 'recall' | 'escalate';
actor_id?: string;
comment?: string;
created_at?: string;
/** Display name of the actor (`sys_user.name`), when resolvable. */
actor_name?: string;
}
/** Input for a decision on an approval request. */
export interface ApprovalDecisionInput {
decision: 'approve' | 'reject';
actorId: string;
comment?: string;
}
/** Input for recalling (withdrawing) a pending request. */
export interface ApprovalRecallInput {
/** Must be the request's submitter (or a system context). */
actorId: string;
comment?: string;
}
/** Result of a recall. */
export interface ApprovalRecallResult {
request: ApprovalRequestRow;
/** The suspended flow run this request gated, if any. */
runId?: string | null;
/**
* True when the owning flow run was resumed (down the `reject` branch with
* `output.decision = 'recall'`) so it doesn't stay suspended forever. The
* engine has no run-cancel primitive yet; the reject edge is the closest
* "did not pass" semantics.
*/
resumed?: boolean;
}
/** Result of a decision that resumes the owning flow when finalised. */
export interface ApprovalDecisionResult {
request: ApprovalRequestRow;
/** True when this call moved the request to a terminal state. */
finalized: boolean;
decision: 'approve' | 'reject';
/** The suspended flow run that was (or will be) resumed, if any. */
runId?: string | null;
/** True when the owning flow run was resumed as a result of this decision. */
resumed?: boolean;
}
/**
* Public contract — the node-era approval runtime.
*/
export interface IApprovalService {
/**
* "My approvals" inbox. Supports filtering by status, target object,
* record id, or by the user expected to act next.
*/
listRequests(
filter: {
object?: string;
recordId?: string;
status?: ApprovalStatus | ApprovalStatus[];
/**
* Match requests where ANY of these identities is a pending approver.
* Accepts a single id or a list (a user typically has several
* identities: their user id, email, and `role:<r>` entries). Passing
* the list lets a caller resolve "my pending approvals" in ONE request
* instead of one request per identity.
*/
approverId?: string | string[];
submitterId?: string;
} | undefined,
context: SharingExecutionContext,
): Promise<ApprovalRequestRow[]>;
getRequest(requestId: string, context: SharingExecutionContext): Promise<ApprovalRequestRow | null>;
/**
* Record a decision on a node-driven request. Honours the node's
* `unanimous` behaviour, finalises the request when satisfied, and resumes
* the owning flow run down the matching `approve` / `reject` edge.
*/
decide(requestId: string, input: ApprovalDecisionInput, context: SharingExecutionContext): Promise<ApprovalDecisionResult>;
/**
* Withdraw a pending request. Only the submitter (or a system context) may
* recall. Finalises the request as `recalled` and resumes the owning flow
* run down the `reject` branch with `output.decision = 'recall'`.
*/
recall(requestId: string, input: ApprovalRecallInput, context: SharingExecutionContext): Promise<ApprovalRecallResult>;
/** Audit trail for a request. */
listActions(requestId: string, context: SharingExecutionContext): Promise<ApprovalActionRow[]>;
}