@objectstack/plugin-approvals 13.0.0 → 14.3.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 +97 -0
- package/dist/index.d.mts +21 -7
- package/dist/index.d.ts +21 -7
- package/dist/index.js +60 -11
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +60 -11
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -7
- package/src/approval-service.test.ts +93 -0
- package/src/approval-service.ts +59 -13
package/dist/index.mjs
CHANGED
|
@@ -1130,16 +1130,20 @@ var _ApprovalService = class _ApprovalService {
|
|
|
1130
1130
|
}
|
|
1131
1131
|
/**
|
|
1132
1132
|
* Expand the approvers on an Approval node into user IDs by querying the
|
|
1133
|
-
* graph tables for `team:` / `department:` / `
|
|
1134
|
-
* types. Falls back to a prefixed literal
|
|
1135
|
-
* produce nothing — so existing fixtures
|
|
1136
|
-
* matching keep working.
|
|
1133
|
+
* graph tables for `team:` / `department:` / `position:` / `role:` /
|
|
1134
|
+
* `manager:` approver types. Falls back to a prefixed literal
|
|
1135
|
+
* (`type:value`) when graph lookups produce nothing — so existing fixtures
|
|
1136
|
+
* and flows that rely on substring matching keep working.
|
|
1137
1137
|
*
|
|
1138
1138
|
* **Graph semantics:**
|
|
1139
1139
|
* - `team` → flat members of `sys_team` (better-auth; no BFS)
|
|
1140
1140
|
* - `department` → recursive BFS of `sys_business_unit.parent_business_unit_id`
|
|
1141
1141
|
* → members of every descendant via `sys_business_unit_member`
|
|
1142
|
-
* - `
|
|
1142
|
+
* - `position` → holders via `sys_user_position` ∪ `sys_member.role`
|
|
1143
|
+
* transition source (ADR-0090 D3 / ADR-0057 D4)
|
|
1144
|
+
* - `role` → users with `sys_member.role = value` in tenant — the
|
|
1145
|
+
* better-auth MEMBERSHIP TIER (owner/admin/member), not a
|
|
1146
|
+
* position; author `position` for org positions
|
|
1143
1147
|
* - `manager` → `sys_user.manager_id` of `record[value] ?? record.owner_id`
|
|
1144
1148
|
* - `field` → literal user id stored in `record[value]`
|
|
1145
1149
|
* - `user` → literal value
|
|
@@ -1164,12 +1168,18 @@ var _ApprovalService = class _ApprovalService {
|
|
|
1164
1168
|
for (const u of users) out.push(u);
|
|
1165
1169
|
continue;
|
|
1166
1170
|
}
|
|
1167
|
-
} else if (a.type === "business_unit" || a.type === "bu") {
|
|
1171
|
+
} else if (a.type === "department" || a.type === "business_unit" || a.type === "bu") {
|
|
1168
1172
|
const users = await this.expandBusinessUnitUsers(String(a.value), organizationId);
|
|
1169
1173
|
if (users.length) {
|
|
1170
1174
|
for (const u of users) out.push(u);
|
|
1171
1175
|
continue;
|
|
1172
1176
|
}
|
|
1177
|
+
} else if (a.type === "position") {
|
|
1178
|
+
const users = await this.expandPositionUsers(String(a.value), organizationId);
|
|
1179
|
+
if (users.length) {
|
|
1180
|
+
for (const u of users) out.push(u);
|
|
1181
|
+
continue;
|
|
1182
|
+
}
|
|
1173
1183
|
} else if (a.type === "role") {
|
|
1174
1184
|
const users = await this.expandRoleUsers(String(a.value), organizationId);
|
|
1175
1185
|
if (users.length) {
|
|
@@ -1256,6 +1266,36 @@ var _ApprovalService = class _ApprovalService {
|
|
|
1256
1266
|
}
|
|
1257
1267
|
return Array.from(new Set((rows ?? []).map((r) => String(r.user_id ?? "")).filter(Boolean)));
|
|
1258
1268
|
}
|
|
1269
|
+
/**
|
|
1270
|
+
* Position holders (ADR-0090 D3): `sys_user_position` is the platform-owned
|
|
1271
|
+
* assignment table, keyed by the position's machine name (ADR-0057 D4),
|
|
1272
|
+
* unioned with the better-auth membership string (`sys_member.role`) as a
|
|
1273
|
+
* transition source — the same semantics as `PositionGraphService` in
|
|
1274
|
+
* `plugin-sharing`, so an approval routes to exactly the users the sharing
|
|
1275
|
+
* engine would expand for the same position.
|
|
1276
|
+
*/
|
|
1277
|
+
async expandPositionUsers(positionName, organizationId) {
|
|
1278
|
+
if (!positionName) return [];
|
|
1279
|
+
const users = /* @__PURE__ */ new Set();
|
|
1280
|
+
const filter = { position: positionName };
|
|
1281
|
+
if (organizationId) filter.organization_id = organizationId;
|
|
1282
|
+
try {
|
|
1283
|
+
const rows = await this.engine.find("sys_user_position", {
|
|
1284
|
+
filter,
|
|
1285
|
+
fields: ["user_id"],
|
|
1286
|
+
limit: 1e4,
|
|
1287
|
+
context: SYSTEM_CTX
|
|
1288
|
+
});
|
|
1289
|
+
for (const r of rows ?? []) {
|
|
1290
|
+
const uid2 = String(r.user_id ?? "");
|
|
1291
|
+
if (uid2) users.add(uid2);
|
|
1292
|
+
}
|
|
1293
|
+
} catch {
|
|
1294
|
+
}
|
|
1295
|
+
for (const uid2 of await this.expandRoleUsers(positionName, organizationId)) users.add(uid2);
|
|
1296
|
+
return Array.from(users);
|
|
1297
|
+
}
|
|
1298
|
+
/** better-auth org-membership tier (`sys_member.role`) — NOT positions. */
|
|
1259
1299
|
async expandRoleUsers(roleName, organizationId) {
|
|
1260
1300
|
if (!roleName) return [];
|
|
1261
1301
|
const filter = { role: roleName };
|
|
@@ -2177,6 +2217,15 @@ var _ApprovalService = class _ApprovalService {
|
|
|
2177
2217
|
const escalateTo = typeof esc2.escalateTo === "string" && esc2.escalateTo.trim() ? esc2.escalateTo.trim() : void 0;
|
|
2178
2218
|
const now = this.clock.now().toISOString();
|
|
2179
2219
|
const pending = csvSplit(raw.pending_approvers);
|
|
2220
|
+
let escalatees = [];
|
|
2221
|
+
if (escalateTo) {
|
|
2222
|
+
try {
|
|
2223
|
+
escalatees = await this.expandPositionUsers(escalateTo, raw.organization_id ?? null);
|
|
2224
|
+
} catch {
|
|
2225
|
+
escalatees = [];
|
|
2226
|
+
}
|
|
2227
|
+
if (!escalatees.length) escalatees = [escalateTo];
|
|
2228
|
+
}
|
|
2180
2229
|
await this.engine.insert("sys_approval_action", {
|
|
2181
2230
|
id: uid("aact"),
|
|
2182
2231
|
request_id: raw.id,
|
|
@@ -2188,16 +2237,16 @@ var _ApprovalService = class _ApprovalService {
|
|
|
2188
2237
|
comment: `${action}${escalateTo ? ` \u2192 ${escalateTo}` : ""}`,
|
|
2189
2238
|
created_at: now
|
|
2190
2239
|
}, { context: SYSTEM_CTX });
|
|
2191
|
-
if (action === "reassign" &&
|
|
2240
|
+
if (action === "reassign" && escalatees.length) {
|
|
2192
2241
|
await this.engine.update("sys_approval_request", {
|
|
2193
2242
|
id: raw.id,
|
|
2194
|
-
pending_approvers:
|
|
2243
|
+
pending_approvers: escalatees.join(","),
|
|
2195
2244
|
updated_at: now
|
|
2196
2245
|
}, { context: SYSTEM_CTX });
|
|
2197
|
-
await this.syncApproverIndex(raw.id,
|
|
2246
|
+
await this.syncApproverIndex(raw.id, escalatees, raw.organization_id ?? null, now);
|
|
2198
2247
|
await this.notify({
|
|
2199
2248
|
topic: "approval.escalated",
|
|
2200
|
-
audience:
|
|
2249
|
+
audience: escalatees,
|
|
2201
2250
|
actorId: SLA_ACTOR_ID,
|
|
2202
2251
|
source: { object: "sys_approval_request", id: raw.id },
|
|
2203
2252
|
payload: {
|
|
@@ -2215,7 +2264,7 @@ var _ApprovalService = class _ApprovalService {
|
|
|
2215
2264
|
} else {
|
|
2216
2265
|
await this.notify({
|
|
2217
2266
|
topic: "approval.sla_breached",
|
|
2218
|
-
audience: [...pending, ...
|
|
2267
|
+
audience: [...pending, ...escalatees],
|
|
2219
2268
|
actorId: SLA_ACTOR_ID,
|
|
2220
2269
|
source: { object: "sys_approval_request", id: raw.id },
|
|
2221
2270
|
payload: {
|