hierarchical-approval 1.3.0 → 1.5.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 (65) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/README.md +57 -0
  3. package/dist/{ApprovalEngine-Dhu5mlCG.d.ts → ApprovalEngine-CyZJDbjP.d.ts} +54 -8
  4. package/dist/{ApprovalEngine-CfySjeuy.d.cts → ApprovalEngine-SI-IRUMY.d.cts} +54 -8
  5. package/dist/{IAuditAdapter-B2I3CjaJ.d.cts → IAuditAdapter-Cr0dLoVP.d.cts} +1 -1
  6. package/dist/{IAuditAdapter-GixC9zS1.d.ts → IAuditAdapter-wfOcN3Zf.d.ts} +1 -1
  7. package/dist/{IAuthorizationPolicy-BHDZK9QL.d.cts → IAuthorizationPolicy-BHybAsq5.d.cts} +2 -2
  8. package/dist/{IAuthorizationPolicy-4ge9Gnxj.d.ts → IAuthorizationPolicy-CdvoZsGC.d.ts} +2 -2
  9. package/dist/{IMetricsAdapter-QvyaKQCc.d.cts → IMetricsAdapter-JZVK3QUq.d.cts} +1 -1
  10. package/dist/{IMetricsAdapter-QvyaKQCc.d.ts → IMetricsAdapter-JZVK3QUq.d.ts} +1 -1
  11. package/dist/{INotificationAdapter-BmSeB7FZ.d.ts → INotificationAdapter-Cn-LNPtD.d.cts} +22 -2
  12. package/dist/{INotificationAdapter-BKMeGmaG.d.cts → INotificationAdapter-Cyo0cvXU.d.ts} +22 -2
  13. package/dist/{IOperationMiddleware-BLw9Oc-S.d.cts → IOperationMiddleware-DrJy9UtL.d.cts} +1 -1
  14. package/dist/{IOperationMiddleware-BW9t8wPn.d.ts → IOperationMiddleware-rx7XAPrW.d.ts} +1 -1
  15. package/dist/{IStorageAdapter-CKV_FZFV.d.cts → IStorageAdapter-CXRyeUOK.d.ts} +11 -1
  16. package/dist/{IStorageAdapter-DQJmR6Mt.d.ts → IStorageAdapter-Dz8jrIAK.d.cts} +11 -1
  17. package/dist/adapters/MemoryAdapter.cjs +24 -0
  18. package/dist/adapters/MemoryAdapter.cjs.map +1 -1
  19. package/dist/adapters/MemoryAdapter.d.cts +2 -2
  20. package/dist/adapters/MemoryAdapter.d.ts +2 -2
  21. package/dist/adapters/MemoryAdapter.js +24 -0
  22. package/dist/adapters/MemoryAdapter.js.map +1 -1
  23. package/dist/adapters/PostgresAdapter.cjs +20 -0
  24. package/dist/adapters/PostgresAdapter.cjs.map +1 -1
  25. package/dist/adapters/PostgresAdapter.d.cts +2 -2
  26. package/dist/adapters/PostgresAdapter.d.ts +2 -2
  27. package/dist/adapters/PostgresAdapter.js +20 -0
  28. package/dist/adapters/PostgresAdapter.js.map +1 -1
  29. package/dist/index.cjs +237 -0
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.d.cts +9 -9
  32. package/dist/index.d.ts +9 -9
  33. package/dist/index.js +237 -0
  34. package/dist/index.js.map +1 -1
  35. package/dist/{instance-BsM0DUPf.d.cts → instance-SA0BU-Gb.d.cts} +20 -2
  36. package/dist/{instance-BsM0DUPf.d.ts → instance-SA0BU-Gb.d.ts} +20 -2
  37. package/dist/nestjs.cjs +213 -0
  38. package/dist/nestjs.cjs.map +1 -1
  39. package/dist/nestjs.d.cts +8 -8
  40. package/dist/nestjs.d.ts +8 -8
  41. package/dist/nestjs.js +213 -0
  42. package/dist/nestjs.js.map +1 -1
  43. package/dist/plugins/audit.d.cts +2 -2
  44. package/dist/plugins/audit.d.ts +2 -2
  45. package/dist/plugins/metrics.cjs +2 -0
  46. package/dist/plugins/metrics.cjs.map +1 -1
  47. package/dist/plugins/metrics.d.cts +1 -1
  48. package/dist/plugins/metrics.d.ts +1 -1
  49. package/dist/plugins/metrics.js +2 -0
  50. package/dist/plugins/metrics.js.map +1 -1
  51. package/dist/plugins/notify.d.cts +2 -2
  52. package/dist/plugins/notify.d.ts +2 -2
  53. package/dist/plugins/resilience.d.cts +3 -3
  54. package/dist/plugins/resilience.d.ts +3 -3
  55. package/dist/plugins/tracing.d.cts +2 -2
  56. package/dist/plugins/tracing.d.ts +2 -2
  57. package/dist/plugins/webhook.d.cts +2 -2
  58. package/dist/plugins/webhook.d.ts +2 -2
  59. package/dist/testing.cjs +237 -0
  60. package/dist/testing.cjs.map +1 -1
  61. package/dist/testing.d.cts +8 -8
  62. package/dist/testing.d.ts +8 -8
  63. package/dist/testing.js +237 -0
  64. package/dist/testing.js.map +1 -1
  65. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,75 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
  _Nothing yet._
9
9
 
10
+ ## [1.5.0] - 2026-09-04
11
+
12
+ ### Added — request for information
13
+
14
+ - **`requestInfo()` / `provideInfo()` let an approver ask the submitter a
15
+ question without rejecting.** Approvers routinely need one fact before they
16
+ can decide, and there was no way to say so: rejecting throws away every
17
+ approval already collected and forces a resubmit, while chasing the question
18
+ by email leaves the request sitting and quietly burns the SLA the approver is
19
+ measured on.
20
+
21
+ ```ts
22
+ await engine.requestInfo(id, { approverId: 'mgr-1', question: 'Which cost centre?' });
23
+ await engine.provideInfo(id, { respondedBy: 'buyer-1', response: 'CC-42' });
24
+ ```
25
+
26
+ The instance stays `pending` and keeps its approvers — this is a question, not
27
+ a decision.
28
+
29
+ - **Deadlines are paused for the duration of the hold.** The scheduler skips a
30
+ held instance entirely, so escalation, reminders, SLA breach and expiry cannot
31
+ fire while the submitter owes an answer; on answer, every deadline that was
32
+ set is pushed out by exactly how long the question was open. An approver gets
33
+ back the time they had before asking rather than being penalised for asking.
34
+ A deadline that was never configured is not invented by being held.
35
+
36
+ - **Wiring:** emits `approval:info_requested` (addressed to the submitter) and
37
+ `approval:info_provided` (addressed to the waiting approvers, carrying
38
+ `heldForMs`); records `info_requested` / `info_provided` audit entries;
39
+ increments `approval.info_requested` / `approval.info_provided`; and adds both
40
+ operations to the authorization-policy set. Only one question may be open at a
41
+ time.
42
+
43
+ New exports: `InfoRequest`, `InfoRequestedEvent`, `InfoProvidedEvent`,
44
+ `RequestInfoOptions`, `ProvideInfoOptions`. `ApprovalInstance` gains
45
+ `infoRequest`.
46
+
47
+ ## [1.4.0] - 2026-09-04
48
+
49
+ ### Added — filter instances by document data
50
+
51
+ - **`InstanceFilter` gains `data`.** Filters covered status, document type,
52
+ submitter, template and date, but nothing about the document itself, so
53
+ "every pending purchase order for vendor ACME" meant paging the whole tenant
54
+ and filtering in application code — which scales badly and pushes the same
55
+ logic into every caller.
56
+
57
+ ```ts
58
+ await engine.queryInstances({
59
+ status: 'pending',
60
+ data: { 'vendor.id': 'v-1', region: 'EU' },
61
+ });
62
+ ```
63
+
64
+ Keys are dot-paths, values compare by deep equality (so object and array
65
+ values match structurally), and all pairs must match. Available on
66
+ `queryInstances()`, `queryInstancesByCursor()` and `getStatistics()`.
67
+
68
+ - **Paths resolve over own properties only**, mirroring how conditions read
69
+ field paths — a filter and a condition written against the same path agree on
70
+ what it means, and an inherited prototype member can never make an instance
71
+ match a query.
72
+
73
+ - **On PostgreSQL this compiles to a parameterised JSONB path lookup**
74
+ (`data #> $n::text[] = $n+1::jsonb`). Path segments travel as a parameter and
75
+ are never interpolated into SQL. Comparison is against JSONB rather than
76
+ serialised text, so structural matching holds on both adapters. Index hot
77
+ paths with `CREATE INDEX ON approval_instances ((data #> '{vendor,id}'))`.
78
+
10
79
  ## [1.3.0] - 2026-09-04
11
80
 
12
81
  ### Added — out-of-office cover
package/README.md CHANGED
@@ -399,6 +399,63 @@ engine.registerConditionOperator('between', (actual, expected) => {
399
399
  });
400
400
  ```
401
401
 
402
+ ### Asking the submitter a question
403
+
404
+ Approvers routinely need one fact before they can decide. `requestInfo()` puts
405
+ the approval on hold without rejecting it:
406
+
407
+ ```ts
408
+ await engine.requestInfo(instance.id, {
409
+ approverId: 'mgr-1',
410
+ question: 'Which cost centre should this book to?',
411
+ });
412
+
413
+ // ...later
414
+ await engine.provideInfo(instance.id, {
415
+ respondedBy: 'buyer-1',
416
+ response: 'CC-42',
417
+ });
418
+ ```
419
+
420
+ The instance stays `pending` and keeps its approvers — this is a question, not a
421
+ decision. What changes is the clock: **escalation, SLA, reminder and expiry
422
+ deadlines stop advancing while the question is open**, and on answer every one
423
+ is pushed out by exactly how long the hold lasted. An approver gets back the
424
+ time they had before asking, rather than being penalised for asking.
425
+
426
+ Emits `approval:info_requested` (addressed to the submitter) and
427
+ `approval:info_provided` (addressed to the waiting approvers, carrying
428
+ `heldForMs`), and records `info_requested` / `info_provided` audit entries.
429
+
430
+ Only one question can be open at a time; answer it before asking another.
431
+
432
+ ### Filtering by document data
433
+
434
+ Every query filter accepts `data`, matching against the document body itself —
435
+ "every purchase order for vendor ACME" without fetching pages and filtering in
436
+ application code:
437
+
438
+ ```ts
439
+ const { items } = await engine.queryInstances({
440
+ status: 'pending',
441
+ data: { 'vendor.id': 'v-1', region: 'EU' }, // dot-paths; all pairs must match
442
+ });
443
+ ```
444
+
445
+ Values compare by deep equality, so an object or array value matches
446
+ structurally. Keys resolve over **own** properties only, mirroring how
447
+ conditions read field paths, so an inherited prototype member can never make an
448
+ instance match. Works with `queryInstances()`, `queryInstancesByCursor()` and
449
+ `getStatistics()`.
450
+
451
+ On PostgreSQL this becomes a parameterised JSONB path lookup — the path travels
452
+ as a parameter and is never interpolated into SQL. Index the paths you filter on
453
+ often:
454
+
455
+ ```sql
456
+ CREATE INDEX ON approval_instances ((data #> '{vendor,id}'));
457
+ ```
458
+
402
459
  ### Out-of-office cover
403
460
 
404
461
  Approvers go on leave, and a chain that waits on an absent person stalls.
@@ -1,14 +1,14 @@
1
- import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from './IStorageAdapter-DQJmR6Mt.js';
2
- import { k as ConditionExpression, o as ResolverFn, g as ApprovalTemplateConfig, A as ApprovalTemplate, j as AuditContext, a as ApprovalInstance, e as ApprovalMode, b as AuditEntry } from './instance-BsM0DUPf.js';
3
- import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-BmSeB7FZ.js';
1
+ import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from './IStorageAdapter-CXRyeUOK.js';
2
+ import { k as ConditionExpression, o as ResolverFn, g as ApprovalTemplateConfig, A as ApprovalTemplate, j as AuditContext, a as ApprovalInstance, e as ApprovalMode, b as AuditEntry } from './instance-SA0BU-Gb.js';
3
+ import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-Cyo0cvXU.js';
4
4
  import { z } from 'zod';
5
5
  import { L as Logger } from './Logger-BplhlU7l.js';
6
6
  import { C as Clock } from './Clock-3FnOczFJ.js';
7
- import { I as IOperationMiddleware, a as ApprovalError } from './IOperationMiddleware-BW9t8wPn.js';
8
- import { I as IAuditAdapter } from './IAuditAdapter-GixC9zS1.js';
9
- import { I as IMetricsAdapter } from './IMetricsAdapter-QvyaKQCc.js';
7
+ import { I as IOperationMiddleware, a as ApprovalError } from './IOperationMiddleware-rx7XAPrW.js';
8
+ import { I as IAuditAdapter } from './IAuditAdapter-wfOcN3Zf.js';
9
+ import { I as IMetricsAdapter } from './IMetricsAdapter-JZVK3QUq.js';
10
10
  import { I as ISchedulerAdapter } from './ISchedulerAdapter-DKv_QjVN.js';
11
- import { I as IAuthorizationPolicy } from './IAuthorizationPolicy-4ge9Gnxj.js';
11
+ import { I as IAuthorizationPolicy } from './IAuthorizationPolicy-CdvoZsGC.js';
12
12
 
13
13
  declare const SubmitOptionsSchema: z.ZodObject<{
14
14
  templateName: z.ZodString;
@@ -69,6 +69,15 @@ declare const OverrideOptionsSchema: z.ZodObject<{
69
69
  overriddenBy: z.ZodString;
70
70
  justification: z.ZodString;
71
71
  }, z.core.$strip>;
72
+ declare const RequestInfoOptionsSchema: z.ZodObject<{
73
+ approverId: z.ZodString;
74
+ question: z.ZodString;
75
+ level: z.ZodOptional<z.ZodNumber>;
76
+ }, z.core.$strip>;
77
+ declare const ProvideInfoOptionsSchema: z.ZodObject<{
78
+ respondedBy: z.ZodString;
79
+ response: z.ZodString;
80
+ }, z.core.$strip>;
72
81
  declare const UpdateDataOptionsSchema: z.ZodObject<{
73
82
  updatedBy: z.ZodString;
74
83
  data: z.ZodRecord<z.ZodString, z.ZodUnknown>;
@@ -90,6 +99,8 @@ type ResubmitOptions = z.infer<typeof ResubmitOptionsSchema>;
90
99
  type AddCommentOptions = z.infer<typeof AddCommentOptionsSchema>;
91
100
  type OverrideOptions = z.infer<typeof OverrideOptionsSchema>;
92
101
  type UpdateDataOptions = z.infer<typeof UpdateDataOptionsSchema>;
102
+ type RequestInfoOptions = z.infer<typeof RequestInfoOptionsSchema>;
103
+ type ProvideInfoOptions = z.infer<typeof ProvideInfoOptionsSchema>;
93
104
 
94
105
  /**
95
106
  * Computes deadline dates from a number of days. The default engine behaviour
@@ -444,6 +455,41 @@ declare class ApprovalEngine {
444
455
  * {@link updateData} for why.
445
456
  */
446
457
  private recomputeFutureChain;
458
+ /**
459
+ * Ask the submitter for clarification without rejecting.
460
+ *
461
+ * Approvers routinely need one fact before they can decide. The only ways to
462
+ * express that were to reject — which throws away every approval already
463
+ * collected and forces a resubmit — or to leave the request sitting while the
464
+ * question is chased by email, which quietly burns the SLA the approver is
465
+ * measured on.
466
+ *
467
+ * The instance stays `pending` and keeps its approvers: this is a question,
468
+ * not a decision. What changes is the clock — escalation, SLA and expiry
469
+ * deadlines stop advancing while the question is open, because time spent
470
+ * waiting on the submitter is not time the approver is sitting on their hands.
471
+ *
472
+ * @throws ApprovalError if the instance is not pending, or a question is already open.
473
+ */
474
+ requestInfo(instanceId: string, raw: RequestInfoOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
475
+ /**
476
+ * Answer an open clarification request and take the instance off hold.
477
+ *
478
+ * Every deadline that was paused is pushed out by exactly how long the
479
+ * question was open, so an approver gets back the full remaining time they
480
+ * had before asking rather than being penalised for asking at all.
481
+ *
482
+ * @throws ApprovalError if no question is open.
483
+ */
484
+ provideInfo(instanceId: string, raw: ProvideInfoOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
485
+ /**
486
+ * Push every pending deadline out by `ms`.
487
+ *
488
+ * Used to give back time an instance spent on hold. Deadlines that are not
489
+ * set stay unset — a level with no escalation configured does not acquire one
490
+ * by being held.
491
+ */
492
+ private extendDeadlinesBy;
447
493
  /** Add a comment to an instance without approving or rejecting. */
448
494
  addComment(instanceId: string, raw: AddCommentOptions, auditCtx?: AuditContext): Promise<void>;
449
495
  /** Resubmit a rejected instance, creating a new linked instance from level 1. */
@@ -568,4 +614,4 @@ declare class ApprovalEngine {
568
614
  private runExternalAudit;
569
615
  }
570
616
 
571
- export { type AddCommentOptions as A, type BulkResult as B, type CanApproveResult as C, type DelegateOptions as D, type EscalateOptions as E, type HealthResult as H, type IdGeneratorFn as I, type OrgProvider as O, type PreviewChainLevel as P, type ReassignOptions as R, type SubmitOptions as S, type UpdateDataOptions as U, type ValidationResult as V, type WeekendCalendarOptions as W, ApprovalEngine as a, type ApprovalEngineOptions as b, type ApprovalStatistics as c, type ApproveOptions as d, type ApproverResolverFn as e, type BusinessCalendar as f, type CancelOptions as g, type ConditionOperatorFn as h, type CycleTimeStats as i, type IdempotencyKeyFn as j, type OutOfOfficeProvider as k, type OverrideOptions as l, type PreviewResult as m, type RejectOptions as n, type ResubmitOptions as o, type RetryPolicy as p, defaultIdGenerator as q, toComparableNumber as t, validateConditionExpression as v, weekendCalendar as w };
617
+ export { type AddCommentOptions as A, type BulkResult as B, type CanApproveResult as C, type DelegateOptions as D, type EscalateOptions as E, type HealthResult as H, type IdGeneratorFn as I, type OrgProvider as O, type PreviewChainLevel as P, type ReassignOptions as R, type SubmitOptions as S, type UpdateDataOptions as U, type ValidationResult as V, type WeekendCalendarOptions as W, ApprovalEngine as a, type ApprovalEngineOptions as b, type ApprovalStatistics as c, type ApproveOptions as d, type ApproverResolverFn as e, type BusinessCalendar as f, type CancelOptions as g, type ConditionOperatorFn as h, type CycleTimeStats as i, type IdempotencyKeyFn as j, type OutOfOfficeProvider as k, type OverrideOptions as l, type PreviewResult as m, type ProvideInfoOptions as n, type RejectOptions as o, type RequestInfoOptions as p, type ResubmitOptions as q, type RetryPolicy as r, defaultIdGenerator as s, toComparableNumber as t, validateConditionExpression as v, weekendCalendar as w };
@@ -1,14 +1,14 @@
1
- import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from './IStorageAdapter-CKV_FZFV.cjs';
2
- import { k as ConditionExpression, o as ResolverFn, g as ApprovalTemplateConfig, A as ApprovalTemplate, j as AuditContext, a as ApprovalInstance, e as ApprovalMode, b as AuditEntry } from './instance-BsM0DUPf.cjs';
3
- import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-BKMeGmaG.cjs';
1
+ import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from './IStorageAdapter-Dz8jrIAK.cjs';
2
+ import { k as ConditionExpression, o as ResolverFn, g as ApprovalTemplateConfig, A as ApprovalTemplate, j as AuditContext, a as ApprovalInstance, e as ApprovalMode, b as AuditEntry } from './instance-SA0BU-Gb.cjs';
3
+ import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-Cn-LNPtD.cjs';
4
4
  import { z } from 'zod';
5
5
  import { L as Logger } from './Logger-BplhlU7l.cjs';
6
6
  import { C as Clock } from './Clock-3FnOczFJ.cjs';
7
- import { I as IOperationMiddleware, a as ApprovalError } from './IOperationMiddleware-BLw9Oc-S.cjs';
8
- import { I as IAuditAdapter } from './IAuditAdapter-B2I3CjaJ.cjs';
9
- import { I as IMetricsAdapter } from './IMetricsAdapter-QvyaKQCc.cjs';
7
+ import { I as IOperationMiddleware, a as ApprovalError } from './IOperationMiddleware-DrJy9UtL.cjs';
8
+ import { I as IAuditAdapter } from './IAuditAdapter-Cr0dLoVP.cjs';
9
+ import { I as IMetricsAdapter } from './IMetricsAdapter-JZVK3QUq.cjs';
10
10
  import { I as ISchedulerAdapter } from './ISchedulerAdapter-DKv_QjVN.cjs';
11
- import { I as IAuthorizationPolicy } from './IAuthorizationPolicy-BHDZK9QL.cjs';
11
+ import { I as IAuthorizationPolicy } from './IAuthorizationPolicy-BHybAsq5.cjs';
12
12
 
13
13
  declare const SubmitOptionsSchema: z.ZodObject<{
14
14
  templateName: z.ZodString;
@@ -69,6 +69,15 @@ declare const OverrideOptionsSchema: z.ZodObject<{
69
69
  overriddenBy: z.ZodString;
70
70
  justification: z.ZodString;
71
71
  }, z.core.$strip>;
72
+ declare const RequestInfoOptionsSchema: z.ZodObject<{
73
+ approverId: z.ZodString;
74
+ question: z.ZodString;
75
+ level: z.ZodOptional<z.ZodNumber>;
76
+ }, z.core.$strip>;
77
+ declare const ProvideInfoOptionsSchema: z.ZodObject<{
78
+ respondedBy: z.ZodString;
79
+ response: z.ZodString;
80
+ }, z.core.$strip>;
72
81
  declare const UpdateDataOptionsSchema: z.ZodObject<{
73
82
  updatedBy: z.ZodString;
74
83
  data: z.ZodRecord<z.ZodString, z.ZodUnknown>;
@@ -90,6 +99,8 @@ type ResubmitOptions = z.infer<typeof ResubmitOptionsSchema>;
90
99
  type AddCommentOptions = z.infer<typeof AddCommentOptionsSchema>;
91
100
  type OverrideOptions = z.infer<typeof OverrideOptionsSchema>;
92
101
  type UpdateDataOptions = z.infer<typeof UpdateDataOptionsSchema>;
102
+ type RequestInfoOptions = z.infer<typeof RequestInfoOptionsSchema>;
103
+ type ProvideInfoOptions = z.infer<typeof ProvideInfoOptionsSchema>;
93
104
 
94
105
  /**
95
106
  * Computes deadline dates from a number of days. The default engine behaviour
@@ -444,6 +455,41 @@ declare class ApprovalEngine {
444
455
  * {@link updateData} for why.
445
456
  */
446
457
  private recomputeFutureChain;
458
+ /**
459
+ * Ask the submitter for clarification without rejecting.
460
+ *
461
+ * Approvers routinely need one fact before they can decide. The only ways to
462
+ * express that were to reject — which throws away every approval already
463
+ * collected and forces a resubmit — or to leave the request sitting while the
464
+ * question is chased by email, which quietly burns the SLA the approver is
465
+ * measured on.
466
+ *
467
+ * The instance stays `pending` and keeps its approvers: this is a question,
468
+ * not a decision. What changes is the clock — escalation, SLA and expiry
469
+ * deadlines stop advancing while the question is open, because time spent
470
+ * waiting on the submitter is not time the approver is sitting on their hands.
471
+ *
472
+ * @throws ApprovalError if the instance is not pending, or a question is already open.
473
+ */
474
+ requestInfo(instanceId: string, raw: RequestInfoOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
475
+ /**
476
+ * Answer an open clarification request and take the instance off hold.
477
+ *
478
+ * Every deadline that was paused is pushed out by exactly how long the
479
+ * question was open, so an approver gets back the full remaining time they
480
+ * had before asking rather than being penalised for asking at all.
481
+ *
482
+ * @throws ApprovalError if no question is open.
483
+ */
484
+ provideInfo(instanceId: string, raw: ProvideInfoOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
485
+ /**
486
+ * Push every pending deadline out by `ms`.
487
+ *
488
+ * Used to give back time an instance spent on hold. Deadlines that are not
489
+ * set stay unset — a level with no escalation configured does not acquire one
490
+ * by being held.
491
+ */
492
+ private extendDeadlinesBy;
447
493
  /** Add a comment to an instance without approving or rejecting. */
448
494
  addComment(instanceId: string, raw: AddCommentOptions, auditCtx?: AuditContext): Promise<void>;
449
495
  /** Resubmit a rejected instance, creating a new linked instance from level 1. */
@@ -568,4 +614,4 @@ declare class ApprovalEngine {
568
614
  private runExternalAudit;
569
615
  }
570
616
 
571
- export { type AddCommentOptions as A, type BulkResult as B, type CanApproveResult as C, type DelegateOptions as D, type EscalateOptions as E, type HealthResult as H, type IdGeneratorFn as I, type OrgProvider as O, type PreviewChainLevel as P, type ReassignOptions as R, type SubmitOptions as S, type UpdateDataOptions as U, type ValidationResult as V, type WeekendCalendarOptions as W, ApprovalEngine as a, type ApprovalEngineOptions as b, type ApprovalStatistics as c, type ApproveOptions as d, type ApproverResolverFn as e, type BusinessCalendar as f, type CancelOptions as g, type ConditionOperatorFn as h, type CycleTimeStats as i, type IdempotencyKeyFn as j, type OutOfOfficeProvider as k, type OverrideOptions as l, type PreviewResult as m, type RejectOptions as n, type ResubmitOptions as o, type RetryPolicy as p, defaultIdGenerator as q, toComparableNumber as t, validateConditionExpression as v, weekendCalendar as w };
617
+ export { type AddCommentOptions as A, type BulkResult as B, type CanApproveResult as C, type DelegateOptions as D, type EscalateOptions as E, type HealthResult as H, type IdGeneratorFn as I, type OrgProvider as O, type PreviewChainLevel as P, type ReassignOptions as R, type SubmitOptions as S, type UpdateDataOptions as U, type ValidationResult as V, type WeekendCalendarOptions as W, ApprovalEngine as a, type ApprovalEngineOptions as b, type ApprovalStatistics as c, type ApproveOptions as d, type ApproverResolverFn as e, type BusinessCalendar as f, type CancelOptions as g, type ConditionOperatorFn as h, type CycleTimeStats as i, type IdempotencyKeyFn as j, type OutOfOfficeProvider as k, type OverrideOptions as l, type PreviewResult as m, type ProvideInfoOptions as n, type RejectOptions as o, type RequestInfoOptions as p, type ResubmitOptions as q, type RetryPolicy as r, defaultIdGenerator as s, toComparableNumber as t, validateConditionExpression as v, weekendCalendar as w };
@@ -1,4 +1,4 @@
1
- import { b as AuditEntry, a as ApprovalInstance } from './instance-BsM0DUPf.cjs';
1
+ import { b as AuditEntry, a as ApprovalInstance } from './instance-SA0BU-Gb.cjs';
2
2
 
3
3
  interface IAuditAdapter {
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { b as AuditEntry, a as ApprovalInstance } from './instance-BsM0DUPf.js';
1
+ import { b as AuditEntry, a as ApprovalInstance } from './instance-SA0BU-Gb.js';
2
2
 
3
3
  interface IAuditAdapter {
4
4
  /**
@@ -1,7 +1,7 @@
1
- import { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-BsM0DUPf.cjs';
1
+ import { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-SA0BU-Gb.cjs';
2
2
 
3
3
  interface AuthorizationContext {
4
- operation: 'submit' | 'approve' | 'reject' | 'delegate' | 'reassign' | 'cancel' | 'escalate' | 'override' | 'resubmit' | 'addComment' | 'updateData';
4
+ operation: 'submit' | 'approve' | 'reject' | 'delegate' | 'reassign' | 'cancel' | 'escalate' | 'override' | 'resubmit' | 'addComment' | 'updateData' | 'requestInfo' | 'provideInfo';
5
5
  actorId: string;
6
6
  instance: Readonly<ApprovalInstance>;
7
7
  level?: Readonly<ApprovalLevelInstance>;
@@ -1,7 +1,7 @@
1
- import { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-BsM0DUPf.js';
1
+ import { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-SA0BU-Gb.js';
2
2
 
3
3
  interface AuthorizationContext {
4
- operation: 'submit' | 'approve' | 'reject' | 'delegate' | 'reassign' | 'cancel' | 'escalate' | 'override' | 'resubmit' | 'addComment' | 'updateData';
4
+ operation: 'submit' | 'approve' | 'reject' | 'delegate' | 'reassign' | 'cancel' | 'escalate' | 'override' | 'resubmit' | 'addComment' | 'updateData' | 'requestInfo' | 'provideInfo';
5
5
  actorId: string;
6
6
  instance: Readonly<ApprovalInstance>;
7
7
  level?: Readonly<ApprovalLevelInstance>;
@@ -1,4 +1,4 @@
1
- type MetricName = 'approval.submitted' | 'approval.approved' | 'approval.rejected' | 'approval.cancelled' | 'approval.expired' | 'approval.sla_breached' | 'approval.escalated' | 'approval.reassigned' | 'approval.overridden' | 'approval.data_updated' | 'approval.reminded' | 'approval.conflict_retry' | 'approval.operation_duration_ms';
1
+ type MetricName = 'approval.submitted' | 'approval.approved' | 'approval.rejected' | 'approval.cancelled' | 'approval.expired' | 'approval.sla_breached' | 'approval.escalated' | 'approval.reassigned' | 'approval.overridden' | 'approval.data_updated' | 'approval.reminded' | 'approval.info_requested' | 'approval.info_provided' | 'approval.conflict_retry' | 'approval.operation_duration_ms';
2
2
  interface IMetricsAdapter {
3
3
  /** Increment a counter. Synchronous — never awaited. */
4
4
  increment(metric: MetricName, labels?: Record<string, string>): void;
@@ -1,4 +1,4 @@
1
- type MetricName = 'approval.submitted' | 'approval.approved' | 'approval.rejected' | 'approval.cancelled' | 'approval.expired' | 'approval.sla_breached' | 'approval.escalated' | 'approval.reassigned' | 'approval.overridden' | 'approval.data_updated' | 'approval.reminded' | 'approval.conflict_retry' | 'approval.operation_duration_ms';
1
+ type MetricName = 'approval.submitted' | 'approval.approved' | 'approval.rejected' | 'approval.cancelled' | 'approval.expired' | 'approval.sla_breached' | 'approval.escalated' | 'approval.reassigned' | 'approval.overridden' | 'approval.data_updated' | 'approval.reminded' | 'approval.info_requested' | 'approval.info_provided' | 'approval.conflict_retry' | 'approval.operation_duration_ms';
2
2
  interface IMetricsAdapter {
3
3
  /** Increment a counter. Synchronous — never awaited. */
4
4
  increment(metric: MetricName, labels?: Record<string, string>): void;
@@ -1,4 +1,4 @@
1
- import { a as ApprovalInstance, b as AuditEntry } from './instance-BsM0DUPf.js';
1
+ import { a as ApprovalInstance, b as AuditEntry } from './instance-SA0BU-Gb.cjs';
2
2
 
3
3
  interface ApprovalEvent {
4
4
  instanceId: string;
@@ -86,6 +86,24 @@ interface ReminderEvent extends ApprovalEvent {
86
86
  /** 1 for the first reminder on this level, 2 for the next, and so on. */
87
87
  reminderNumber: number;
88
88
  }
89
+ /** Emitted when an approver asks the submitter for clarification. */
90
+ interface InfoRequestedEvent extends ApprovalEvent {
91
+ askedBy: string;
92
+ question: string;
93
+ level: number;
94
+ /** Who is expected to answer — the submitter. */
95
+ recipients: string[];
96
+ }
97
+ /** Emitted when the question is answered and the instance comes off hold. */
98
+ interface InfoProvidedEvent extends ApprovalEvent {
99
+ respondedBy: string;
100
+ response: string;
101
+ level: number;
102
+ /** How long the instance spent on hold; deadlines were extended by this much. */
103
+ heldForMs: number;
104
+ /** The approvers waiting again now that the question is answered. */
105
+ recipients: string[];
106
+ }
89
107
  interface ApprovalEventMap {
90
108
  'approval:submitted': SubmittedEvent;
91
109
  'approval:approved': ApprovedEvent;
@@ -102,6 +120,8 @@ interface ApprovalEventMap {
102
120
  'approval:sla_breached': SlaBreachedEvent;
103
121
  'approval:data_updated': DataUpdatedEvent;
104
122
  'approval:reminder': ReminderEvent;
123
+ 'approval:info_requested': InfoRequestedEvent;
124
+ 'approval:info_provided': InfoProvidedEvent;
105
125
  }
106
126
  type ApprovalEventName = keyof ApprovalEventMap;
107
127
  interface HistoryEntry extends AuditEntry {
@@ -125,4 +145,4 @@ interface INotificationAdapter {
125
145
  notify(event: NotificationEvent): Promise<void>;
126
146
  }
127
147
 
128
- export type { ApprovalEvent as A, CancelledEvent as C, DataUpdatedEvent as D, EscalatedEvent as E, HistoryEntry as H, INotificationAdapter as I, LevelAdvancedEvent as L, NotificationEvent as N, OverriddenEvent as O, ReassignedEvent as R, SlaBreachedEvent as S, ApprovalEventMap as a, ApprovalEventName as b, ApprovedEvent as c, DelegatedEvent as d, ExpiredEvent as e, RejectedEvent as f, ReminderEvent as g, ResubmittedEvent as h, SubmittedEvent as i };
148
+ export type { ApprovalEvent as A, CancelledEvent as C, DataUpdatedEvent as D, EscalatedEvent as E, HistoryEntry as H, INotificationAdapter as I, LevelAdvancedEvent as L, NotificationEvent as N, OverriddenEvent as O, ReassignedEvent as R, SlaBreachedEvent as S, ApprovalEventMap as a, ApprovalEventName as b, ApprovedEvent as c, DelegatedEvent as d, ExpiredEvent as e, InfoProvidedEvent as f, InfoRequestedEvent as g, RejectedEvent as h, ReminderEvent as i, ResubmittedEvent as j, SubmittedEvent as k };
@@ -1,4 +1,4 @@
1
- import { a as ApprovalInstance, b as AuditEntry } from './instance-BsM0DUPf.cjs';
1
+ import { a as ApprovalInstance, b as AuditEntry } from './instance-SA0BU-Gb.js';
2
2
 
3
3
  interface ApprovalEvent {
4
4
  instanceId: string;
@@ -86,6 +86,24 @@ interface ReminderEvent extends ApprovalEvent {
86
86
  /** 1 for the first reminder on this level, 2 for the next, and so on. */
87
87
  reminderNumber: number;
88
88
  }
89
+ /** Emitted when an approver asks the submitter for clarification. */
90
+ interface InfoRequestedEvent extends ApprovalEvent {
91
+ askedBy: string;
92
+ question: string;
93
+ level: number;
94
+ /** Who is expected to answer — the submitter. */
95
+ recipients: string[];
96
+ }
97
+ /** Emitted when the question is answered and the instance comes off hold. */
98
+ interface InfoProvidedEvent extends ApprovalEvent {
99
+ respondedBy: string;
100
+ response: string;
101
+ level: number;
102
+ /** How long the instance spent on hold; deadlines were extended by this much. */
103
+ heldForMs: number;
104
+ /** The approvers waiting again now that the question is answered. */
105
+ recipients: string[];
106
+ }
89
107
  interface ApprovalEventMap {
90
108
  'approval:submitted': SubmittedEvent;
91
109
  'approval:approved': ApprovedEvent;
@@ -102,6 +120,8 @@ interface ApprovalEventMap {
102
120
  'approval:sla_breached': SlaBreachedEvent;
103
121
  'approval:data_updated': DataUpdatedEvent;
104
122
  'approval:reminder': ReminderEvent;
123
+ 'approval:info_requested': InfoRequestedEvent;
124
+ 'approval:info_provided': InfoProvidedEvent;
105
125
  }
106
126
  type ApprovalEventName = keyof ApprovalEventMap;
107
127
  interface HistoryEntry extends AuditEntry {
@@ -125,4 +145,4 @@ interface INotificationAdapter {
125
145
  notify(event: NotificationEvent): Promise<void>;
126
146
  }
127
147
 
128
- export type { ApprovalEvent as A, CancelledEvent as C, DataUpdatedEvent as D, EscalatedEvent as E, HistoryEntry as H, INotificationAdapter as I, LevelAdvancedEvent as L, NotificationEvent as N, OverriddenEvent as O, ReassignedEvent as R, SlaBreachedEvent as S, ApprovalEventMap as a, ApprovalEventName as b, ApprovedEvent as c, DelegatedEvent as d, ExpiredEvent as e, RejectedEvent as f, ReminderEvent as g, ResubmittedEvent as h, SubmittedEvent as i };
148
+ export type { ApprovalEvent as A, CancelledEvent as C, DataUpdatedEvent as D, EscalatedEvent as E, HistoryEntry as H, INotificationAdapter as I, LevelAdvancedEvent as L, NotificationEvent as N, OverriddenEvent as O, ReassignedEvent as R, SlaBreachedEvent as S, ApprovalEventMap as a, ApprovalEventName as b, ApprovedEvent as c, DelegatedEvent as d, ExpiredEvent as e, InfoProvidedEvent as f, InfoRequestedEvent as g, RejectedEvent as h, ReminderEvent as i, ResubmittedEvent as j, SubmittedEvent as k };
@@ -1,4 +1,4 @@
1
- import { a as ApprovalInstance } from './instance-BsM0DUPf.cjs';
1
+ import { a as ApprovalInstance } from './instance-SA0BU-Gb.cjs';
2
2
 
3
3
  declare class ApprovalError extends Error {
4
4
  readonly code: string;
@@ -1,4 +1,4 @@
1
- import { a as ApprovalInstance } from './instance-BsM0DUPf.js';
1
+ import { a as ApprovalInstance } from './instance-SA0BU-Gb.js';
2
2
 
3
3
  declare class ApprovalError extends Error {
4
4
  readonly code: string;
@@ -1,4 +1,4 @@
1
- import { A as ApprovalTemplate, a as ApprovalInstance, f as ApprovalStatus, b as AuditEntry } from './instance-BsM0DUPf.cjs';
1
+ import { A as ApprovalTemplate, a as ApprovalInstance, f as ApprovalStatus, b as AuditEntry } from './instance-SA0BU-Gb.js';
2
2
 
3
3
  interface PaginationOpts {
4
4
  limit: number;
@@ -27,6 +27,16 @@ interface InstanceFilter {
27
27
  templateName?: string;
28
28
  fromDate?: Date;
29
29
  toDate?: Date;
30
+ /**
31
+ * Match instances whose document `data` contains these field/value pairs —
32
+ * "every purchase order for vendor ACME", without the caller fetching a page
33
+ * at a time and filtering in application code.
34
+ *
35
+ * Keys are dot-paths (`vendor.id`), values compare by deep equality, and all
36
+ * pairs must match. Matching is on **own** properties only, mirroring how
37
+ * conditions resolve field paths.
38
+ */
39
+ data?: Record<string, unknown>;
30
40
  }
31
41
  interface IStorageAdapter {
32
42
  saveTemplate(template: ApprovalTemplate): Promise<void>;
@@ -1,4 +1,4 @@
1
- import { A as ApprovalTemplate, a as ApprovalInstance, f as ApprovalStatus, b as AuditEntry } from './instance-BsM0DUPf.js';
1
+ import { A as ApprovalTemplate, a as ApprovalInstance, f as ApprovalStatus, b as AuditEntry } from './instance-SA0BU-Gb.cjs';
2
2
 
3
3
  interface PaginationOpts {
4
4
  limit: number;
@@ -27,6 +27,16 @@ interface InstanceFilter {
27
27
  templateName?: string;
28
28
  fromDate?: Date;
29
29
  toDate?: Date;
30
+ /**
31
+ * Match instances whose document `data` contains these field/value pairs —
32
+ * "every purchase order for vendor ACME", without the caller fetching a page
33
+ * at a time and filtering in application code.
34
+ *
35
+ * Keys are dot-paths (`vendor.id`), values compare by deep equality, and all
36
+ * pairs must match. Matching is on **own** properties only, mirroring how
37
+ * conditions resolve field paths.
38
+ */
39
+ data?: Record<string, unknown>;
30
40
  }
31
41
  interface IStorageAdapter {
32
42
  saveTemplate(template: ApprovalTemplate): Promise<void>;
@@ -55,6 +55,25 @@ function reviveDates(instance) {
55
55
  function reviveTemplateDates(template) {
56
56
  return { ...template, createdAt: new Date(template.createdAt) };
57
57
  }
58
+ function readPath(data, path) {
59
+ return path.split(".").reduce((obj, key) => {
60
+ if (obj !== null && typeof obj === "object" && Object.prototype.hasOwnProperty.call(obj, key)) {
61
+ return obj[key];
62
+ }
63
+ return void 0;
64
+ }, data);
65
+ }
66
+ function deepEquals(a, b) {
67
+ if (Object.is(a, b)) return true;
68
+ if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
69
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
70
+ const aKeys = Object.keys(a);
71
+ const bKeys = Object.keys(b);
72
+ if (aKeys.length !== bKeys.length) return false;
73
+ return aKeys.every(
74
+ (k) => Object.prototype.hasOwnProperty.call(b, k) && deepEquals(a[k], b[k])
75
+ );
76
+ }
58
77
  function applyFilter(instance, filter) {
59
78
  if (filter.status && instance.status !== filter.status) return false;
60
79
  if (filter.documentType && instance.documentType !== filter.documentType) return false;
@@ -62,6 +81,11 @@ function applyFilter(instance, filter) {
62
81
  if (filter.templateName && instance.templateName !== filter.templateName) return false;
63
82
  if (filter.fromDate && new Date(instance.createdAt) < filter.fromDate) return false;
64
83
  if (filter.toDate && new Date(instance.createdAt) > filter.toDate) return false;
84
+ if (filter.data) {
85
+ for (const [path, expected] of Object.entries(filter.data)) {
86
+ if (!deepEquals(readPath(instance.data ?? {}, path), expected)) return false;
87
+ }
88
+ }
65
89
  return true;
66
90
  }
67
91
  var MemoryAdapter = class {