hierarchical-approval 2.0.0 → 2.2.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +60 -0
  3. package/dist/{ApprovalEngine-B8bPZq01.d.ts → ApprovalEngine-B3ChZxS7.d.ts} +109 -8
  4. package/dist/{ApprovalEngine-C7p9cOA3.d.cts → ApprovalEngine-CbFBAMlO.d.cts} +109 -8
  5. package/dist/{IAuditAdapter-UJNdJanq.d.cts → IAuditAdapter-BHVKMMv6.d.cts} +1 -1
  6. package/dist/{IAuditAdapter-DDV4Rf9F.d.ts → IAuditAdapter-C4OASS6u.d.ts} +1 -1
  7. package/dist/{IAuthorizationPolicy-CFJ-gXKl.d.ts → IAuthorizationPolicy-BA_m2Dg9.d.ts} +1 -1
  8. package/dist/{IAuthorizationPolicy-CN6LAaKg.d.cts → IAuthorizationPolicy-CtLkwQ8I.d.cts} +1 -1
  9. package/dist/{INotificationAdapter-Dv_MuYwS.d.ts → INotificationAdapter-BdgfUrIn.d.ts} +12 -2
  10. package/dist/{INotificationAdapter-Dy2d0JKy.d.cts → INotificationAdapter-JJAiMyMd.d.cts} +12 -2
  11. package/dist/{IOperationMiddleware-DWYXVuJD.d.ts → IOperationMiddleware-BGYrAqep.d.ts} +1 -1
  12. package/dist/{IOperationMiddleware-DGG-guxK.d.cts → IOperationMiddleware-D0ksiFM3.d.cts} +1 -1
  13. package/dist/{IStorageAdapter-Bgd6p8XL.d.cts → IStorageAdapter-Bk7ybd3z.d.cts} +1 -1
  14. package/dist/{IStorageAdapter-B8aRYeGI.d.ts → IStorageAdapter-DjRvHUF0.d.ts} +1 -1
  15. package/dist/adapters/MemoryAdapter.d.cts +2 -2
  16. package/dist/adapters/MemoryAdapter.d.ts +2 -2
  17. package/dist/adapters/PostgresAdapter.d.cts +2 -2
  18. package/dist/adapters/PostgresAdapter.d.ts +2 -2
  19. package/dist/index.cjs +385 -13
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.d.cts +8 -8
  22. package/dist/index.d.ts +8 -8
  23. package/dist/index.js +385 -14
  24. package/dist/index.js.map +1 -1
  25. package/dist/{instance-BE0uJmg3.d.cts → instance-DUJY_Axf.d.cts} +41 -3
  26. package/dist/{instance-BE0uJmg3.d.ts → instance-DUJY_Axf.d.ts} +41 -3
  27. package/dist/nestjs.cjs +384 -13
  28. package/dist/nestjs.cjs.map +1 -1
  29. package/dist/nestjs.d.cts +7 -7
  30. package/dist/nestjs.d.ts +7 -7
  31. package/dist/nestjs.js +384 -13
  32. package/dist/nestjs.js.map +1 -1
  33. package/dist/plugins/audit.d.cts +2 -2
  34. package/dist/plugins/audit.d.ts +2 -2
  35. package/dist/plugins/notify.d.cts +2 -2
  36. package/dist/plugins/notify.d.ts +2 -2
  37. package/dist/plugins/resilience.d.cts +3 -3
  38. package/dist/plugins/resilience.d.ts +3 -3
  39. package/dist/plugins/tracing.d.cts +2 -2
  40. package/dist/plugins/tracing.d.ts +2 -2
  41. package/dist/plugins/webhook.d.cts +2 -2
  42. package/dist/plugins/webhook.d.ts +2 -2
  43. package/dist/testing.cjs +384 -13
  44. package/dist/testing.cjs.map +1 -1
  45. package/dist/testing.d.cts +7 -7
  46. package/dist/testing.d.ts +7 -7
  47. package/dist/testing.js +384 -13
  48. package/dist/testing.js.map +1 -1
  49. package/package.json +1 -1
package/dist/index.d.cts CHANGED
@@ -1,18 +1,18 @@
1
- export { A as AddCommentOptions, a as ApprovalEngine, b as ApprovalEngineOptions, c as ApprovalStatistics, d as ApproveOptions, e as ApproverResolverFn, f as ApproverWorkload, B as BulkResult, g as BusinessCalendar, C as CanApproveResult, h as CancelOptions, i as ConditionOperatorFn, j as CycleTimeStats, D as DelegateOptions, E as EscalateOptions, H as HealthResult, I as IdGeneratorFn, k as IdempotencyKeyFn, O as OrgProvider, l as OutOfOfficeProvider, m as OverrideOptions, P as PreviewChainLevel, n as PreviewResult, o as ProvideInfoOptions, R as ReassignOptions, p as RejectOptions, q as RequestInfoOptions, r as ResubmitOptions, s as RetryPolicy, S as SubmitOptions, T as TransferResult, U as UpdateDataOptions, V as ValidationResult, W as WeekendCalendarOptions, t as defaultIdGenerator, u as toComparableNumber, v as validateConditionExpression, w as weekendCalendar } from './ApprovalEngine-C7p9cOA3.cjs';
2
- import { I as IStorageAdapter } from './IStorageAdapter-Bgd6p8XL.cjs';
3
- export { c as CursorPaginatedResult, C as CursorPaginationOpts, b as InstanceFilter, a as PaginatedResult, P as PaginationOpts } from './IStorageAdapter-Bgd6p8XL.cjs';
1
+ export { A as AddCommentOptions, a as ApprovalEngine, b as ApprovalEngineOptions, c as ApprovalStatistics, d as ApproveOptions, e as ApproverResolverFn, f as ApproverWorkload, B as BulkResult, g as BusinessCalendar, C as CanApproveResult, h as CancelOptions, i as ConditionOperatorFn, j as CycleTimeStats, D as DelegateOptions, E as EscalateOptions, H as HealthResult, I as IdGeneratorFn, k as IdempotencyKeyFn, l as ImportResult, O as OrgProvider, m as OutOfOfficeProvider, n as OverrideOptions, P as PreviewChainLevel, o as PreviewResult, p as ProvideInfoOptions, R as ReassignOptions, q as RejectOptions, r as RequestInfoOptions, s as ResubmitOptions, t as RetryPolicy, S as SubmitOptions, T as TEMPLATE_BUNDLE_VERSION, u as TemplateBundle, v as TransferResult, U as UpdateDataOptions, V as ValidationResult, W as WeekendCalendarOptions, w as defaultIdGenerator, x as toComparableNumber, y as validateConditionExpression, z as weekendCalendar } from './ApprovalEngine-CbFBAMlO.cjs';
2
+ import { I as IStorageAdapter } from './IStorageAdapter-Bk7ybd3z.cjs';
3
+ export { c as CursorPaginatedResult, C as CursorPaginationOpts, b as InstanceFilter, a as PaginatedResult, P as PaginationOpts } from './IStorageAdapter-Bk7ybd3z.cjs';
4
4
  import { L as Logger } from './Logger-BplhlU7l.cjs';
5
5
  export { n as noopLogger } from './Logger-BplhlU7l.cjs';
6
6
  import { C as Clock } from './Clock-3FnOczFJ.cjs';
7
7
  export { s as systemClock } from './Clock-3FnOczFJ.cjs';
8
- export { A as AuthorizationContext, I as IAuthorizationPolicy } from './IAuthorizationPolicy-CN6LAaKg.cjs';
9
- export { A as ApprovalConflictError, a as ApprovalError, b as ApprovalForbiddenError, c as ApprovalNotFoundError, d as ApprovalTemplateNotFoundError, e as ApprovalValidationError, I as IOperationMiddleware, O as OperationContext } from './IOperationMiddleware-DGG-guxK.cjs';
8
+ export { A as AuthorizationContext, I as IAuthorizationPolicy } from './IAuthorizationPolicy-CtLkwQ8I.cjs';
9
+ export { A as ApprovalConflictError, a as ApprovalError, b as ApprovalForbiddenError, c as ApprovalNotFoundError, d as ApprovalTemplateNotFoundError, e as ApprovalValidationError, I as IOperationMiddleware, O as OperationContext } from './IOperationMiddleware-D0ksiFM3.cjs';
10
10
  export { MemoryAdapter } from './adapters/MemoryAdapter.cjs';
11
- export { A as ApprovalEvent, a as ApprovalEventMap, b as ApprovalEventName, c as ApprovedEvent, d as AttachmentEvent, C as CancelledEvent, D as DataUpdatedEvent, e as DelegatedEvent, E as EscalatedEvent, f as ExpiredEvent, H as HistoryEntry, I as INotificationAdapter, g as InfoProvidedEvent, h as InfoRequestedEvent, L as LevelAdvancedEvent, N as NotificationEvent, O as OverriddenEvent, R as ReassignedEvent, i as RejectedEvent, j as ReminderEvent, k as ResubmittedEvent, S as SlaBreachedEvent, l as SubmittedEvent } from './INotificationAdapter-Dy2d0JKy.cjs';
12
- export { I as IAuditAdapter } from './IAuditAdapter-UJNdJanq.cjs';
11
+ export { A as ApprovalEvent, a as ApprovalEventMap, b as ApprovalEventName, c as ApprovedEvent, d as AttachmentEvent, C as CancelledEvent, D as DataUpdatedEvent, e as DelegatedEvent, E as EscalatedEvent, f as ExpiredEvent, H as HistoryEntry, I as INotificationAdapter, g as InfoProvidedEvent, h as InfoRequestedEvent, L as LevelAdvancedEvent, N as NotificationEvent, O as OverriddenEvent, R as ReassignedEvent, i as RejectedEvent, j as ReminderEvent, k as ResubmittedEvent, S as SlaBreachedEvent, l as SubWorkflowEvent, m as SubmittedEvent } from './INotificationAdapter-JJAiMyMd.cjs';
12
+ export { I as IAuditAdapter } from './IAuditAdapter-BHVKMMv6.cjs';
13
13
  export { I as IMetricsAdapter, M as MetricName } from './IMetricsAdapter-DWq8IFaf.cjs';
14
14
  export { I as ISchedulerAdapter } from './ISchedulerAdapter-DKv_QjVN.cjs';
15
- export { a as ApprovalInstance, c as ApprovalLevelConfig, d as ApprovalLevelInstance, e as ApprovalMode, f as ApprovalStatus, A as ApprovalTemplate, g as ApprovalTemplateConfig, h as ApproverConfig, i as Attachment, j as AuditAction, k as AuditContext, b as AuditEntry, C as Condition, l as ConditionExpression, m as ConditionGroup, n as ConditionOperator, o as ConditionRule, E as EscalationConfig, I as InfoRequest, L as LevelStatus, R as ResolvedApprover, p as ResolverFn, T as TemplateSnapshot } from './instance-BE0uJmg3.cjs';
15
+ export { a as ApprovalInstance, c as ApprovalLevelConfig, d as ApprovalLevelInstance, e as ApprovalMode, f as ApprovalStatus, A as ApprovalTemplate, g as ApprovalTemplateConfig, h as ApproverConfig, i as Attachment, j as AuditAction, k as AuditContext, b as AuditEntry, C as Condition, l as ConditionExpression, m as ConditionGroup, n as ConditionOperator, o as ConditionRule, E as EscalationConfig, I as InfoRequest, L as LevelStatus, R as ResolvedApprover, p as ResolverFn, S as SubWorkflowConfig, T as TemplateSnapshot } from './instance-DUJY_Axf.cjs';
16
16
  import 'zod';
17
17
 
18
18
  interface EscalationSchedulerOpts {
package/dist/index.d.ts CHANGED
@@ -1,18 +1,18 @@
1
- export { A as AddCommentOptions, a as ApprovalEngine, b as ApprovalEngineOptions, c as ApprovalStatistics, d as ApproveOptions, e as ApproverResolverFn, f as ApproverWorkload, B as BulkResult, g as BusinessCalendar, C as CanApproveResult, h as CancelOptions, i as ConditionOperatorFn, j as CycleTimeStats, D as DelegateOptions, E as EscalateOptions, H as HealthResult, I as IdGeneratorFn, k as IdempotencyKeyFn, O as OrgProvider, l as OutOfOfficeProvider, m as OverrideOptions, P as PreviewChainLevel, n as PreviewResult, o as ProvideInfoOptions, R as ReassignOptions, p as RejectOptions, q as RequestInfoOptions, r as ResubmitOptions, s as RetryPolicy, S as SubmitOptions, T as TransferResult, U as UpdateDataOptions, V as ValidationResult, W as WeekendCalendarOptions, t as defaultIdGenerator, u as toComparableNumber, v as validateConditionExpression, w as weekendCalendar } from './ApprovalEngine-B8bPZq01.js';
2
- import { I as IStorageAdapter } from './IStorageAdapter-B8aRYeGI.js';
3
- export { c as CursorPaginatedResult, C as CursorPaginationOpts, b as InstanceFilter, a as PaginatedResult, P as PaginationOpts } from './IStorageAdapter-B8aRYeGI.js';
1
+ export { A as AddCommentOptions, a as ApprovalEngine, b as ApprovalEngineOptions, c as ApprovalStatistics, d as ApproveOptions, e as ApproverResolverFn, f as ApproverWorkload, B as BulkResult, g as BusinessCalendar, C as CanApproveResult, h as CancelOptions, i as ConditionOperatorFn, j as CycleTimeStats, D as DelegateOptions, E as EscalateOptions, H as HealthResult, I as IdGeneratorFn, k as IdempotencyKeyFn, l as ImportResult, O as OrgProvider, m as OutOfOfficeProvider, n as OverrideOptions, P as PreviewChainLevel, o as PreviewResult, p as ProvideInfoOptions, R as ReassignOptions, q as RejectOptions, r as RequestInfoOptions, s as ResubmitOptions, t as RetryPolicy, S as SubmitOptions, T as TEMPLATE_BUNDLE_VERSION, u as TemplateBundle, v as TransferResult, U as UpdateDataOptions, V as ValidationResult, W as WeekendCalendarOptions, w as defaultIdGenerator, x as toComparableNumber, y as validateConditionExpression, z as weekendCalendar } from './ApprovalEngine-B3ChZxS7.js';
2
+ import { I as IStorageAdapter } from './IStorageAdapter-DjRvHUF0.js';
3
+ export { c as CursorPaginatedResult, C as CursorPaginationOpts, b as InstanceFilter, a as PaginatedResult, P as PaginationOpts } from './IStorageAdapter-DjRvHUF0.js';
4
4
  import { L as Logger } from './Logger-BplhlU7l.js';
5
5
  export { n as noopLogger } from './Logger-BplhlU7l.js';
6
6
  import { C as Clock } from './Clock-3FnOczFJ.js';
7
7
  export { s as systemClock } from './Clock-3FnOczFJ.js';
8
- export { A as AuthorizationContext, I as IAuthorizationPolicy } from './IAuthorizationPolicy-CFJ-gXKl.js';
9
- export { A as ApprovalConflictError, a as ApprovalError, b as ApprovalForbiddenError, c as ApprovalNotFoundError, d as ApprovalTemplateNotFoundError, e as ApprovalValidationError, I as IOperationMiddleware, O as OperationContext } from './IOperationMiddleware-DWYXVuJD.js';
8
+ export { A as AuthorizationContext, I as IAuthorizationPolicy } from './IAuthorizationPolicy-BA_m2Dg9.js';
9
+ export { A as ApprovalConflictError, a as ApprovalError, b as ApprovalForbiddenError, c as ApprovalNotFoundError, d as ApprovalTemplateNotFoundError, e as ApprovalValidationError, I as IOperationMiddleware, O as OperationContext } from './IOperationMiddleware-BGYrAqep.js';
10
10
  export { MemoryAdapter } from './adapters/MemoryAdapter.js';
11
- export { A as ApprovalEvent, a as ApprovalEventMap, b as ApprovalEventName, c as ApprovedEvent, d as AttachmentEvent, C as CancelledEvent, D as DataUpdatedEvent, e as DelegatedEvent, E as EscalatedEvent, f as ExpiredEvent, H as HistoryEntry, I as INotificationAdapter, g as InfoProvidedEvent, h as InfoRequestedEvent, L as LevelAdvancedEvent, N as NotificationEvent, O as OverriddenEvent, R as ReassignedEvent, i as RejectedEvent, j as ReminderEvent, k as ResubmittedEvent, S as SlaBreachedEvent, l as SubmittedEvent } from './INotificationAdapter-Dv_MuYwS.js';
12
- export { I as IAuditAdapter } from './IAuditAdapter-DDV4Rf9F.js';
11
+ export { A as ApprovalEvent, a as ApprovalEventMap, b as ApprovalEventName, c as ApprovedEvent, d as AttachmentEvent, C as CancelledEvent, D as DataUpdatedEvent, e as DelegatedEvent, E as EscalatedEvent, f as ExpiredEvent, H as HistoryEntry, I as INotificationAdapter, g as InfoProvidedEvent, h as InfoRequestedEvent, L as LevelAdvancedEvent, N as NotificationEvent, O as OverriddenEvent, R as ReassignedEvent, i as RejectedEvent, j as ReminderEvent, k as ResubmittedEvent, S as SlaBreachedEvent, l as SubWorkflowEvent, m as SubmittedEvent } from './INotificationAdapter-BdgfUrIn.js';
12
+ export { I as IAuditAdapter } from './IAuditAdapter-C4OASS6u.js';
13
13
  export { I as IMetricsAdapter, M as MetricName } from './IMetricsAdapter-DWq8IFaf.js';
14
14
  export { I as ISchedulerAdapter } from './ISchedulerAdapter-DKv_QjVN.js';
15
- export { a as ApprovalInstance, c as ApprovalLevelConfig, d as ApprovalLevelInstance, e as ApprovalMode, f as ApprovalStatus, A as ApprovalTemplate, g as ApprovalTemplateConfig, h as ApproverConfig, i as Attachment, j as AuditAction, k as AuditContext, b as AuditEntry, C as Condition, l as ConditionExpression, m as ConditionGroup, n as ConditionOperator, o as ConditionRule, E as EscalationConfig, I as InfoRequest, L as LevelStatus, R as ResolvedApprover, p as ResolverFn, T as TemplateSnapshot } from './instance-BE0uJmg3.js';
15
+ export { a as ApprovalInstance, c as ApprovalLevelConfig, d as ApprovalLevelInstance, e as ApprovalMode, f as ApprovalStatus, A as ApprovalTemplate, g as ApprovalTemplateConfig, h as ApproverConfig, i as Attachment, j as AuditAction, k as AuditContext, b as AuditEntry, C as Condition, l as ConditionExpression, m as ConditionGroup, n as ConditionOperator, o as ConditionRule, E as EscalationConfig, I as InfoRequest, L as LevelStatus, R as ResolvedApprover, p as ResolverFn, S as SubWorkflowConfig, T as TemplateSnapshot } from './instance-DUJY_Axf.js';
16
16
  import 'zod';
17
17
 
18
18
  interface EscalationSchedulerOpts {
package/dist/index.js CHANGED
@@ -908,6 +908,7 @@ function computeTimingStats(samples) {
908
908
  }
909
909
 
910
910
  // src/engine/ApprovalEngine.ts
911
+ var MAX_SUBWORKFLOW_DEPTH = 5;
911
912
  var DEFAULT_MAX_REMINDERS = 3;
912
913
  var DEFAULT_MAX_ATTEMPTS = 3;
913
914
  var DEFAULT_BASE_DELAY_MS = 50;
@@ -919,6 +920,7 @@ var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
919
920
  ]);
920
921
  var CYCLE_TIME_STATUSES = ["approved", "rejected", "cancelled"];
921
922
  var CYCLE_TIME_FETCH_BATCH_SIZE = 500;
923
+ var TEMPLATE_BUNDLE_VERSION = 1;
922
924
  var ApprovalEngine = class _ApprovalEngine {
923
925
  constructor(opts) {
924
926
  this.opts = opts;
@@ -1012,12 +1014,32 @@ var ApprovalEngine = class _ApprovalEngine {
1012
1014
  });
1013
1015
  }
1014
1016
  levelNums.add(l.level);
1015
- if (!l.approvers || l.approvers.length === 0) {
1017
+ if (!l.subWorkflow && (!l.approvers || l.approvers.length === 0)) {
1016
1018
  errors.push({
1017
1019
  field: `levels[${i}].approvers`,
1018
1020
  message: `Level ${l.level} must have at least one approver.`
1019
1021
  });
1020
1022
  }
1023
+ if (l.subWorkflow) {
1024
+ if (!l.subWorkflow.templateName) {
1025
+ errors.push({
1026
+ field: `levels[${i}].subWorkflow.templateName`,
1027
+ message: `Level ${l.level} declares a subWorkflow without a templateName.`
1028
+ });
1029
+ }
1030
+ if (l.subWorkflow.templateName === config.name) {
1031
+ errors.push({
1032
+ field: `levels[${i}].subWorkflow.templateName`,
1033
+ message: `Level ${l.level} would spawn a sub-workflow of its own template ("${config.name}"), which cannot terminate.`
1034
+ });
1035
+ }
1036
+ if (l.approvers && l.approvers.length > 0) {
1037
+ errors.push({
1038
+ field: `levels[${i}].approvers`,
1039
+ message: `Level ${l.level} sets both approvers and subWorkflow; a sub-workflow level is decided by its child approval, so its approvers would never be asked.`
1040
+ });
1041
+ }
1042
+ }
1021
1043
  if (l.reminderAfterDays !== void 0 && l.reminderAfterDays <= 0) {
1022
1044
  errors.push({
1023
1045
  field: `levels[${i}].reminderAfterDays`,
@@ -1160,7 +1182,7 @@ var ApprovalEngine = class _ApprovalEngine {
1160
1182
  return this.registry.list();
1161
1183
  }
1162
1184
  // ─── Lifecycle ────────────────────────────────────────────────────────────
1163
- async submit(raw, auditCtx) {
1185
+ async submit(raw, auditCtx, link) {
1164
1186
  const opts = parseOrThrow(() => SubmitOptionsSchema.parse(raw));
1165
1187
  const startMs = this.clock.now().getTime();
1166
1188
  const template = await this.registry.get(opts.templateName);
@@ -1220,6 +1242,7 @@ var ApprovalEngine = class _ApprovalEngine {
1220
1242
  weights: cfg.weights,
1221
1243
  escalationAfterDays: cfg.escalationAfterDays,
1222
1244
  escalationDueAt: inFirstGroup && cfg.escalationAfterDays ? this.deadlineFrom(now, cfg.escalationAfterDays) : void 0,
1245
+ subWorkflowTemplate: cfg.subWorkflow?.templateName,
1223
1246
  reminderAfterDays: cfg.reminderAfterDays,
1224
1247
  reminderEveryDays: cfg.reminderEveryDays,
1225
1248
  maxReminders: cfg.maxReminders,
@@ -1228,6 +1251,10 @@ var ApprovalEngine = class _ApprovalEngine {
1228
1251
  };
1229
1252
  });
1230
1253
  for (const lvl of levels.filter((l) => l.status === "pending")) {
1254
+ if (lvl.subWorkflowTemplate) {
1255
+ lvl.approverIds = [];
1256
+ continue;
1257
+ }
1231
1258
  lvl.approverIds = await this.resolver.resolveApprovers(
1232
1259
  lvl.approverConfigs,
1233
1260
  opts.submittedBy,
@@ -1248,6 +1275,9 @@ var ApprovalEngine = class _ApprovalEngine {
1248
1275
  const instance = {
1249
1276
  id: instanceId,
1250
1277
  tenantId: this.tenantId,
1278
+ parentInstanceId: link?.parentInstanceId,
1279
+ parentLevel: link?.parentLevel,
1280
+ subWorkflowDepth: link?.depth,
1251
1281
  templateId: template.id,
1252
1282
  templateName: template.name,
1253
1283
  documentId: opts.documentId,
@@ -1312,12 +1342,13 @@ var ApprovalEngine = class _ApprovalEngine {
1312
1342
  { operation: "submit", actorId: opts.submittedBy, tenantId: this.tenantId, input: opts },
1313
1343
  instance
1314
1344
  );
1345
+ await this.startSubWorkflows(instance);
1315
1346
  return instance;
1316
1347
  }
1317
1348
  async approve(instanceId, raw, auditCtx) {
1318
1349
  const opts = parseOrThrow(() => ApproveOptionsSchema.parse(raw));
1319
1350
  const startMs = this.clock.now().getTime();
1320
- return this.withOptimisticRetry(instanceId, async (instance) => {
1351
+ const decided = await this.withOptimisticRetry(instanceId, async (instance) => {
1321
1352
  assertStatus(instance, "pending");
1322
1353
  if (opts.approverId === instance.submittedBy) {
1323
1354
  throw new ApprovalForbiddenError(
@@ -1530,10 +1561,12 @@ var ApprovalEngine = class _ApprovalEngine {
1530
1561
  }
1531
1562
  return instance;
1532
1563
  });
1564
+ await this.afterDecision(decided);
1565
+ return decided;
1533
1566
  }
1534
1567
  async reject(instanceId, raw, auditCtx) {
1535
1568
  const opts = parseOrThrow(() => RejectOptionsSchema.parse(raw));
1536
- return this.withOptimisticRetry(instanceId, async (instance) => {
1569
+ const decided = await this.withOptimisticRetry(instanceId, async (instance) => {
1537
1570
  assertStatus(instance, "pending");
1538
1571
  if (opts.approverId === instance.submittedBy) {
1539
1572
  throw new ApprovalForbiddenError(
@@ -1652,6 +1685,8 @@ var ApprovalEngine = class _ApprovalEngine {
1652
1685
  }
1653
1686
  return instance;
1654
1687
  });
1688
+ await this.afterDecision(decided);
1689
+ return decided;
1655
1690
  }
1656
1691
  async delegate(instanceId, raw, auditCtx) {
1657
1692
  const opts = parseOrThrow(() => DelegateOptionsSchema.parse(raw));
@@ -1829,7 +1864,8 @@ var ApprovalEngine = class _ApprovalEngine {
1829
1864
  }
1830
1865
  async cancel(instanceId, raw, auditCtx) {
1831
1866
  const opts = parseOrThrow(() => CancelOptionsSchema.parse(raw));
1832
- return this.withOptimisticRetry(instanceId, async (instance) => {
1867
+ let cancelled = null;
1868
+ const result = await this.withOptimisticRetry(instanceId, async (instance) => {
1833
1869
  if (instance.status === "approved" || instance.status === "rejected") {
1834
1870
  throw new ApprovalError(`Cannot cancel a "${instance.status}" approval.`, "CANNOT_CANCEL");
1835
1871
  }
@@ -1883,8 +1919,11 @@ var ApprovalEngine = class _ApprovalEngine {
1883
1919
  },
1884
1920
  instance
1885
1921
  );
1922
+ cancelled = instance;
1886
1923
  return instance;
1887
1924
  });
1925
+ if (cancelled) await this.propagateToParent(cancelled);
1926
+ return result;
1888
1927
  }
1889
1928
  async escalate(instanceId, raw, auditCtx) {
1890
1929
  parseOrThrow(() => EscalateOptionsSchema.parse(raw));
@@ -2963,6 +3002,118 @@ var ApprovalEngine = class _ApprovalEngine {
2963
3002
  oldestAgeMs: Number.isFinite(row.oldest) ? now.getTime() - row.oldest : 0
2964
3003
  })).sort((a, b) => b.pending - a.pending || a.approverId.localeCompare(b.approverId));
2965
3004
  }
3005
+ /**
3006
+ * Export templates as a portable bundle.
3007
+ *
3008
+ * Approval configuration is written once and then has to travel — authored in
3009
+ * a sandbox, reviewed, promoted to production. Reading `listTemplates()` and
3010
+ * re-posting the rows carried each environment's own `id`, `tenantId` and
3011
+ * version lineage with it, which either collided on arrival or silently
3012
+ * claimed a history the target never had. This strips all of it.
3013
+ *
3014
+ * @param names - Templates to include; omit for all of them.
3015
+ */
3016
+ async exportTemplates(names) {
3017
+ const all = await this.registry.list();
3018
+ const wanted = names ? all.filter((t) => names.includes(t.name)) : all;
3019
+ if (names) {
3020
+ const missing = names.filter((n) => !all.some((t) => t.name === n));
3021
+ if (missing.length > 0) {
3022
+ throw new ApprovalTemplateNotFoundError(missing.join(", "));
3023
+ }
3024
+ }
3025
+ return {
3026
+ bundleVersion: TEMPLATE_BUNDLE_VERSION,
3027
+ exportedAt: this.clock.now(),
3028
+ templates: wanted.map((t) => {
3029
+ const {
3030
+ id: _id,
3031
+ tenantId: _tenantId,
3032
+ createdAt: _createdAt,
3033
+ version: _version,
3034
+ previousVersionId: _previousVersionId,
3035
+ ...config
3036
+ } = t;
3037
+ return config;
3038
+ })
3039
+ };
3040
+ }
3041
+ /**
3042
+ * Import a bundle produced by {@link exportTemplates}.
3043
+ *
3044
+ * **Every template is validated before any is written.** A bundle that is
3045
+ * half-applied is worse than one rejected outright: the tenant is left in a
3046
+ * state matching neither environment, and the operator has no way to tell
3047
+ * which half landed. Per-template failures during the write phase are still
3048
+ * reported individually, since a storage error can occur after validation
3049
+ * passes.
3050
+ *
3051
+ * @param bundle - The bundle to apply.
3052
+ * @param opts - `mode: 'create'` (default) refuses to touch existing
3053
+ * templates; `'upsert'` updates them. `dryRun` reports without writing.
3054
+ */
3055
+ async importTemplates(bundle, opts = {}) {
3056
+ const mode = opts.mode ?? "create";
3057
+ const dryRun = opts.dryRun ?? false;
3058
+ if (bundle.bundleVersion !== TEMPLATE_BUNDLE_VERSION) {
3059
+ throw new ApprovalValidationError(
3060
+ `Unsupported template bundle version ${bundle.bundleVersion}; this engine reads version ${TEMPLATE_BUNDLE_VERSION}.`
3061
+ );
3062
+ }
3063
+ if (!Array.isArray(bundle.templates) || bundle.templates.length === 0) {
3064
+ throw new ApprovalValidationError("Template bundle contains no templates.");
3065
+ }
3066
+ const duplicates = bundle.templates.map((t) => t.name).filter((name, i, all) => all.indexOf(name) !== i);
3067
+ if (duplicates.length > 0) {
3068
+ throw new ApprovalValidationError(
3069
+ `Template bundle contains duplicate names: ${[...new Set(duplicates)].join(", ")}.`
3070
+ );
3071
+ }
3072
+ const invalid = [];
3073
+ for (const config of bundle.templates) {
3074
+ const result2 = this.validateTemplate(config);
3075
+ if (!result2.valid) {
3076
+ invalid.push({
3077
+ name: config.name,
3078
+ message: result2.errors[0]?.message ?? "unknown validation error"
3079
+ });
3080
+ }
3081
+ }
3082
+ if (invalid.length > 0) {
3083
+ throw new ApprovalValidationError(
3084
+ `Template bundle failed validation and was not applied: ${invalid.map((e) => `${e.name}: ${e.message}`).join("; ")}`
3085
+ );
3086
+ }
3087
+ const result = { created: [], updated: [], skipped: [], errors: [], dryRun };
3088
+ for (const config of bundle.templates) {
3089
+ const existing = await this.opts.adapter.getTemplate(this.tenantId, config.name);
3090
+ try {
3091
+ if (existing && mode === "create") {
3092
+ result.skipped.push(config.name);
3093
+ continue;
3094
+ }
3095
+ if (existing) {
3096
+ if (!dryRun) await this.registry.update(config);
3097
+ result.updated.push(config.name);
3098
+ } else {
3099
+ if (!dryRun) await this.registry.define(config);
3100
+ result.created.push(config.name);
3101
+ }
3102
+ } catch (err) {
3103
+ result.errors.push({ name: config.name, message: err.message });
3104
+ }
3105
+ }
3106
+ this.logger.info("importTemplates: bundle applied", {
3107
+ tenantId: this.tenantId,
3108
+ mode,
3109
+ dryRun,
3110
+ created: result.created.length,
3111
+ updated: result.updated.length,
3112
+ skipped: result.skipped.length,
3113
+ errors: result.errors.length
3114
+ });
3115
+ return result;
3116
+ }
2966
3117
  async getStatistics(filter = {}) {
2967
3118
  const statuses = [
2968
3119
  "pending",
@@ -3401,14 +3552,18 @@ var ApprovalEngine = class _ApprovalEngine {
3401
3552
  /** Resolve approvers for a group, set its deadlines, and mark it pending. */
3402
3553
  async activateGroup(instance, group, now) {
3403
3554
  for (const lvl of group) {
3404
- lvl.approverIds = await this.resolver.resolveApprovers(
3405
- lvl.approverConfigs,
3406
- instance.submittedBy,
3407
- instance.data,
3408
- this.opts.orgProvider,
3409
- this.opts.outOfOfficeProvider,
3410
- now
3411
- );
3555
+ if (lvl.subWorkflowTemplate) {
3556
+ lvl.approverIds = [];
3557
+ } else {
3558
+ lvl.approverIds = await this.resolver.resolveApprovers(
3559
+ lvl.approverConfigs,
3560
+ instance.submittedBy,
3561
+ instance.data,
3562
+ this.opts.orgProvider,
3563
+ this.opts.outOfOfficeProvider,
3564
+ now
3565
+ );
3566
+ }
3412
3567
  if (lvl.escalationAfterDays) {
3413
3568
  lvl.escalationDueAt = this.deadlineFrom(now, lvl.escalationAfterDays);
3414
3569
  }
@@ -3495,6 +3650,222 @@ var ApprovalEngine = class _ApprovalEngine {
3495
3650
  this.logger.error("reminder: failed to send", err, { tenantId: this.tenantId, instanceId });
3496
3651
  }
3497
3652
  }
3653
+ /**
3654
+ * Start child approvals for any open level that delegates to a sub-workflow.
3655
+ *
3656
+ * Run after the parent has been persisted, never inside the same optimistic
3657
+ * write: the child's own `submit()` performs its own reads and writes, and
3658
+ * nesting them under the parent's compare-and-set would make a slow child
3659
+ * template a source of spurious version conflicts on the parent.
3660
+ */
3661
+ async startSubWorkflows(instance) {
3662
+ const pendingSubs = instance.levels.filter(
3663
+ (l) => l.status === "pending" && l.subWorkflowTemplate && !l.childInstanceId
3664
+ );
3665
+ if (pendingSubs.length === 0) return;
3666
+ const depth = (instance.subWorkflowDepth ?? 0) + 1;
3667
+ if (depth > MAX_SUBWORKFLOW_DEPTH) {
3668
+ throw new ApprovalValidationError(
3669
+ `Sub-workflow nesting exceeded ${MAX_SUBWORKFLOW_DEPTH} levels at template "${instance.templateName}". Check for a template that reaches itself.`
3670
+ );
3671
+ }
3672
+ for (const level of pendingSubs) {
3673
+ const templateName = level.subWorkflowTemplate;
3674
+ const childTemplate = await this.registry.get(templateName);
3675
+ const child = await this.submit(
3676
+ {
3677
+ templateName,
3678
+ // Unique per parent level, so a resubmitted parent does not collide
3679
+ // with the child it spawned last time.
3680
+ documentId: `${instance.documentId}#L${level.level}`,
3681
+ documentType: childTemplate.documentType,
3682
+ submittedBy: instance.submittedBy,
3683
+ data: instance.data,
3684
+ metadata: { ...instance.metadata, parentInstanceId: instance.id }
3685
+ },
3686
+ void 0,
3687
+ { parentInstanceId: instance.id, parentLevel: level.level, depth }
3688
+ );
3689
+ await this.withOptimisticRetry(instance.id, async (parent) => {
3690
+ const lvl = parent.levels.find((l) => l.level === level.level);
3691
+ if (!lvl || lvl.childInstanceId) return parent;
3692
+ lvl.childInstanceId = child.id;
3693
+ parent.updatedAt = this.clock.now();
3694
+ await this.opts.adapter.updateInstance(parent, parent.version);
3695
+ return parent;
3696
+ });
3697
+ const payload = {
3698
+ instanceId: instance.id,
3699
+ documentId: instance.documentId,
3700
+ documentType: instance.documentType,
3701
+ timestamp: this.clock.now(),
3702
+ level: level.level,
3703
+ childInstanceId: child.id,
3704
+ childTemplateName: templateName
3705
+ };
3706
+ this.bus.emit("approval:subworkflow_started", payload);
3707
+ await this.notifyAdapters("approval:subworkflow_started", instance, payload);
3708
+ this.logger.info("subWorkflow: child approval started", {
3709
+ tenantId: this.tenantId,
3710
+ instanceId: instance.id,
3711
+ level: level.level,
3712
+ childInstanceId: child.id
3713
+ });
3714
+ }
3715
+ }
3716
+ /**
3717
+ * Return a finished child's outcome to the parent level that is waiting on it.
3718
+ *
3719
+ * An approved child approves its parent level and lets the chain advance; any
3720
+ * other terminal outcome — rejected, cancelled, expired — rejects the parent,
3721
+ * because the approval the parent was waiting for did not happen. Collapsing
3722
+ * those into one rejection is deliberate: a parent that treated a cancelled
3723
+ * child as "carry on" would advance past a gate nobody cleared.
3724
+ */
3725
+ async propagateToParent(child) {
3726
+ if (!child.parentInstanceId || child.parentLevel === void 0) return;
3727
+ const outcome = child.status;
3728
+ const parentId = child.parentInstanceId;
3729
+ const parentLevelNumber = child.parentLevel;
3730
+ try {
3731
+ const parent = await this.opts.adapter.getInstance(this.tenantId, parentId);
3732
+ if (!parent || parent.status !== "pending") return;
3733
+ const payload = {
3734
+ instanceId: parentId,
3735
+ documentId: parent.documentId,
3736
+ documentType: parent.documentType,
3737
+ timestamp: this.clock.now(),
3738
+ level: parentLevelNumber,
3739
+ childInstanceId: child.id,
3740
+ childTemplateName: child.templateName,
3741
+ outcome
3742
+ };
3743
+ if (outcome === "approved") {
3744
+ await this.completeSubWorkflowLevel(parentId, parentLevelNumber, child);
3745
+ } else {
3746
+ await this.rejectFromSubWorkflow(parentId, parentLevelNumber, child);
3747
+ }
3748
+ const refreshed = await this.opts.adapter.getInstance(this.tenantId, parentId);
3749
+ this.bus.emit("approval:subworkflow_completed", payload);
3750
+ if (refreshed) {
3751
+ await this.notifyAdapters("approval:subworkflow_completed", refreshed, payload);
3752
+ }
3753
+ } catch (err) {
3754
+ this.logger.error("subWorkflow: failed to propagate outcome to parent", err, {
3755
+ tenantId: this.tenantId,
3756
+ childInstanceId: child.id,
3757
+ parentInstanceId: parentId
3758
+ });
3759
+ }
3760
+ }
3761
+ /** Mark a sub-workflow level approved and advance the parent chain. */
3762
+ async completeSubWorkflowLevel(parentId, levelNumber, child) {
3763
+ let advanced = null;
3764
+ await this.withOptimisticRetry(parentId, async (parent) => {
3765
+ const level = parent.levels.find((l) => l.level === levelNumber);
3766
+ if (!level || level.status !== "pending") return parent;
3767
+ const now = this.clock.now();
3768
+ level.status = "approved";
3769
+ level.reminderDueAt = void 0;
3770
+ level.escalationDueAt = void 0;
3771
+ const auditEntry = {
3772
+ action: "subworkflow_completed",
3773
+ actorId: "system",
3774
+ level: levelNumber,
3775
+ timestamp: now,
3776
+ newValue: { childInstanceId: child.id, outcome: "approved" }
3777
+ };
3778
+ parent.auditLog.push(auditEntry);
3779
+ parent.updatedAt = now;
3780
+ const siblingsOpen = this.groupMembers(parent, level).some(
3781
+ (l) => l.status === "pending" || l.status === "waiting"
3782
+ );
3783
+ if (!siblingsOpen) {
3784
+ const nextGroup = this.findNextGroup(parent);
3785
+ if (nextGroup.length === 0) {
3786
+ parent.status = "approved";
3787
+ } else {
3788
+ await this.activateGroup(parent, nextGroup, now);
3789
+ }
3790
+ }
3791
+ await this.opts.adapter.updateInstance(parent, parent.version);
3792
+ await this.opts.adapter.appendAuditEntry(this.tenantId, parentId, auditEntry);
3793
+ await this.runExternalAudit(parent, auditEntry);
3794
+ advanced = parent;
3795
+ return parent;
3796
+ });
3797
+ if (advanced) {
3798
+ const parent = advanced;
3799
+ if (parent.status === "approved") {
3800
+ this.bus.emit("approval:completed", parent);
3801
+ await this.notifyAdapters("approval:completed", parent, parent);
3802
+ await this.propagateToParent(parent);
3803
+ } else {
3804
+ await this.startSubWorkflows(parent);
3805
+ }
3806
+ }
3807
+ }
3808
+ /** Reject a parent because the child approval it was waiting on did not succeed. */
3809
+ async rejectFromSubWorkflow(parentId, levelNumber, child) {
3810
+ let rejected = null;
3811
+ await this.withOptimisticRetry(parentId, async (parent) => {
3812
+ const level = parent.levels.find((l) => l.level === levelNumber);
3813
+ if (!level || level.status !== "pending") return parent;
3814
+ const now = this.clock.now();
3815
+ level.status = "rejected";
3816
+ level.reminderDueAt = void 0;
3817
+ level.escalationDueAt = void 0;
3818
+ parent.status = "rejected";
3819
+ parent.updatedAt = now;
3820
+ const auditEntry = {
3821
+ action: "subworkflow_completed",
3822
+ actorId: "system",
3823
+ level: levelNumber,
3824
+ timestamp: now,
3825
+ reason: `Child approval ${child.id} ended as "${child.status}".`,
3826
+ newValue: { childInstanceId: child.id, outcome: child.status }
3827
+ };
3828
+ parent.auditLog.push(auditEntry);
3829
+ await this.opts.adapter.updateInstance(parent, parent.version);
3830
+ await this.opts.adapter.appendAuditEntry(this.tenantId, parentId, auditEntry);
3831
+ await this.runExternalAudit(parent, auditEntry);
3832
+ rejected = parent;
3833
+ return parent;
3834
+ });
3835
+ if (rejected) {
3836
+ const parent = rejected;
3837
+ const payload = {
3838
+ instanceId: parent.id,
3839
+ documentId: parent.documentId,
3840
+ documentType: parent.documentType,
3841
+ timestamp: this.clock.now(),
3842
+ approverId: "system",
3843
+ level: levelNumber,
3844
+ reason: `Child approval ${child.id} ended as "${child.status}".`,
3845
+ returnTo: null
3846
+ };
3847
+ this.bus.emit("approval:rejected", payload);
3848
+ await this.notifyAdapters("approval:rejected", parent, payload);
3849
+ await this.propagateToParent(parent);
3850
+ }
3851
+ }
3852
+ /**
3853
+ * Work that must happen after a decision is durably recorded, not inside it.
3854
+ *
3855
+ * Both branches touch other instances — a newly opened level may spawn a
3856
+ * child approval, and a finished instance may be a child that owes its
3857
+ * outcome to a parent. Doing either inside the deciding instance's
3858
+ * compare-and-set would nest writes under a version guard that knows nothing
3859
+ * about them, so a slow child template would surface as a spurious conflict
3860
+ * on the decision the user just made.
3861
+ */
3862
+ async afterDecision(instance) {
3863
+ if (TERMINAL_STATUSES.has(instance.status)) {
3864
+ await this.propagateToParent(instance);
3865
+ return;
3866
+ }
3867
+ await this.startSubWorkflows(instance);
3868
+ }
3498
3869
  findNextLevel(instance) {
3499
3870
  return instance.levels.find((l) => l.level > instance.currentLevel && l.status === "waiting") ?? null;
3500
3871
  }
@@ -3833,6 +4204,6 @@ function weekendCalendar(options = {}) {
3833
4204
  };
3834
4205
  }
3835
4206
 
3836
- export { ApprovalConflictError, ApprovalEngine, ApprovalError, ApprovalForbiddenError, ApprovalNotFoundError, ApprovalTemplateNotFoundError, ApprovalValidationError, EscalationScheduler, MemoryAdapter, defaultIdGenerator, noopLogger, systemClock, toComparableNumber, validateConditionExpression, weekendCalendar };
4207
+ export { ApprovalConflictError, ApprovalEngine, ApprovalError, ApprovalForbiddenError, ApprovalNotFoundError, ApprovalTemplateNotFoundError, ApprovalValidationError, EscalationScheduler, MemoryAdapter, TEMPLATE_BUNDLE_VERSION, defaultIdGenerator, noopLogger, systemClock, toComparableNumber, validateConditionExpression, weekendCalendar };
3837
4208
  //# sourceMappingURL=index.js.map
3838
4209
  //# sourceMappingURL=index.js.map