@objectstack/plugin-approvals 12.6.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/dist/index.mjs CHANGED
@@ -1007,7 +1007,7 @@ var ESCALATION_JOB_NAME = "approvals-sla-escalation";
1007
1007
  var ESCALATION_SCAN_INTERVAL_MS = 5 * 60 * 1e3;
1008
1008
  var SLA_ACTOR_ID = "system:sla";
1009
1009
  var ACTION_TOKEN_TTL_MS = 72 * 60 * 60 * 1e3;
1010
- var SYSTEM_CTX = { isSystem: true, roles: [], permissions: [] };
1010
+ var SYSTEM_CTX = { isSystem: true, positions: [], permissions: [] };
1011
1011
  function uid(prefix) {
1012
1012
  const g = globalThis;
1013
1013
  if (g.crypto?.randomUUID) return `${prefix}_${g.crypto.randomUUID()}`;
@@ -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:` / `role:` / `manager:` approver
1134
- * types. Falls back to a prefixed literal (`type:value`) when graph lookups
1135
- * produce nothing — so existing fixtures and flows that rely on substring
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
- * - `role` users with `sys_member.role = value` in tenant
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" && escalateTo) {
2240
+ if (action === "reassign" && escalatees.length) {
2192
2241
  await this.engine.update("sys_approval_request", {
2193
2242
  id: raw.id,
2194
- pending_approvers: escalateTo,
2243
+ pending_approvers: escalatees.join(","),
2195
2244
  updated_at: now
2196
2245
  }, { context: SYSTEM_CTX });
2197
- await this.syncApproverIndex(raw.id, [escalateTo], raw.organization_id ?? null, now);
2246
+ await this.syncApproverIndex(raw.id, escalatees, raw.organization_id ?? null, now);
2198
2247
  await this.notify({
2199
2248
  topic: "approval.escalated",
2200
- audience: [escalateTo],
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, ...escalateTo ? [escalateTo] : []],
2267
+ audience: [...pending, ...escalatees],
2219
2268
  actorId: SLA_ACTOR_ID,
2220
2269
  source: { object: "sys_approval_request", id: raw.id },
2221
2270
  payload: {
@@ -2920,7 +2969,7 @@ import {
2920
2969
  getApprovalNodeConfigJsonSchema,
2921
2970
  APPROVAL_NODE_TYPE
2922
2971
  } from "@objectstack/spec/automation";
2923
- var SYSTEM_CTX2 = { isSystem: true, roles: [], permissions: [] };
2972
+ var SYSTEM_CTX2 = { isSystem: true, positions: [], permissions: [] };
2924
2973
  function registerApprovalNode(automation, service, logger) {
2925
2974
  automation.registerNodeExecutor({
2926
2975
  type: APPROVAL_NODE_TYPE,