hierarchical-approval 0.7.0 → 0.8.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 +45 -0
  2. package/README.md +41 -0
  3. package/dist/{ApprovalEngine-BGfrrFxl.d.ts → ApprovalEngine-DFY53zRK.d.ts} +25 -7
  4. package/dist/{ApprovalEngine-BET-UqQo.d.cts → ApprovalEngine-DVMMtiSm.d.cts} +25 -7
  5. package/dist/{IAuditAdapter-B_DhuPsU.d.cts → IAuditAdapter-BODIlw4h.d.cts} +1 -1
  6. package/dist/{IAuditAdapter-B3vvU09m.d.ts → IAuditAdapter-CaM3A2Kt.d.ts} +1 -1
  7. package/dist/{IAuthorizationPolicy-CZESF3CJ.d.ts → IAuthorizationPolicy-DFD0ELtO.d.ts} +1 -1
  8. package/dist/{IAuthorizationPolicy-B6JzRNUk.d.cts → IAuthorizationPolicy-cT7LSHtl.d.cts} +1 -1
  9. package/dist/{INotificationAdapter-DVVmXU6a.d.cts → INotificationAdapter-Ci0gf6ac.d.cts} +1 -1
  10. package/dist/{INotificationAdapter-BdfVjYa8.d.ts → INotificationAdapter-Hn70VShV.d.ts} +1 -1
  11. package/dist/{IOperationMiddleware-KGAwT9f-.d.ts → IOperationMiddleware-BCcAqSzT.d.ts} +1 -1
  12. package/dist/{IOperationMiddleware-CXgXmGUF.d.cts → IOperationMiddleware-DOlOraCX.d.cts} +1 -1
  13. package/dist/{IStorageAdapter-D-oPxxun.d.ts → IStorageAdapter-D7jsZT6O.d.ts} +1 -1
  14. package/dist/{IStorageAdapter-BibHlgPw.d.cts → IStorageAdapter-egPQD6Ry.d.cts} +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 +81 -5
  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 +81 -6
  24. package/dist/index.js.map +1 -1
  25. package/dist/{instance-D8D7b07N.d.cts → instance-CtkmEkYa.d.cts} +34 -2
  26. package/dist/{instance-D8D7b07N.d.ts → instance-CtkmEkYa.d.ts} +34 -2
  27. package/dist/nestjs.cjs +80 -5
  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 +80 -5
  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 +80 -5
  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 +80 -5
  48. package/dist/testing.js.map +1 -1
  49. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,51 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
  _Nothing yet._
9
9
 
10
+ ## [0.8.0] - 2026-09-04
11
+
12
+ ### Added — boolean condition expressions
13
+
14
+ - **A rule's `when` now accepts `all`, `any` and `not` groups, nestable to any
15
+ depth.** Conditions previously supported a single test or an array meaning
16
+ AND, so "escalate when the amount is large **or** the vendor is high-risk"
17
+ could not be written as one rule — it took two rules with duplicated
18
+ `addLevels`, and anything involving negation or a mix of AND and OR had no
19
+ expression at all.
20
+
21
+ ```ts
22
+ when: {
23
+ any: [
24
+ { all: [
25
+ { field: 'amount', operator: '>', value: 1000 },
26
+ { field: 'dept', operator: '==', value: 'engineering' },
27
+ ] },
28
+ { field: 'override', operator: '==', value: true },
29
+ ],
30
+ }
31
+ ```
32
+
33
+ A group sets exactly one combinator. `any` short-circuits on the first child
34
+ that holds. New `ConditionExpression` and `ConditionGroup` types are exported
35
+ from the package root.
36
+
37
+ **Fully backward compatible.** A bare condition still works, and an array is
38
+ shorthand for `all` — exactly what `when: [...]` already meant.
39
+
40
+ - **Condition trees are validated when the template is defined.** `validateTemplate()`
41
+ and `defineTemplate()` now reject an empty `all`/`any`, a group setting more
42
+ than one combinator, a non-array `all`/`any`, and a leaf missing its `field`
43
+ or `operator` — reporting the offending path, e.g.
44
+ `conditions[0].when.any[1].operator`. Previously a malformed condition was
45
+ only discovered at submit time, on a real document.
46
+
47
+ Operator *names* are deliberately not checked at definition time, because
48
+ custom operators can be registered after a template is defined; an unknown
49
+ operator still throws when the condition is evaluated.
50
+
51
+ - **`validateConditionExpression(expression, path)` is exported** for callers
52
+ that build condition trees dynamically and want to check one before handing it
53
+ to a template. It collects every problem rather than throwing on the first.
54
+
10
55
  ## [0.7.0] - 2026-09-04
11
56
 
12
57
  ### Fixed — two condition-evaluation bypasses
package/README.md CHANGED
@@ -305,6 +305,47 @@ conditions: [
305
305
 
306
306
  **Built-in operators:** `>`, `<`, `>=`, `<=`, `==`, `!=`, `in`, `not_in`
307
307
 
308
+ #### Combining conditions
309
+
310
+ A `when` can be a single condition, an array (every element must hold), or a
311
+ boolean group — `all`, `any`, `not` — which nest to any depth:
312
+
313
+ ```ts
314
+ conditions: [
315
+ {
316
+ // (amount > 1000 AND dept is engineering) OR an explicit override flag
317
+ when: {
318
+ any: [
319
+ {
320
+ all: [
321
+ { field: 'amount', operator: '>', value: 1000 },
322
+ { field: 'dept', operator: '==', value: 'engineering' },
323
+ ],
324
+ },
325
+ { field: 'override', operator: '==', value: true },
326
+ ],
327
+ },
328
+ addLevels: [{ level: 3, name: 'CFO', approvers: [{ type: 'user', userId: 'cfo' }], mode: 'any' }],
329
+ },
330
+ {
331
+ // Everything outside the US skips the domestic finance review
332
+ when: { not: { field: 'region', operator: '==', value: 'US' } },
333
+ skipLevels: [2],
334
+ },
335
+ ]
336
+ ```
337
+
338
+ A group sets exactly one of `all` / `any` / `not`. `any` short-circuits on the
339
+ first child that holds. An array is shorthand for `all`, so every template
340
+ written before groups existed keeps working unchanged.
341
+
342
+ Groups are checked when the template is defined, not when a document is
343
+ submitted — `validateTemplate()` and `defineTemplate()` reject an empty `all`,
344
+ a group setting two combinators, or a leaf missing its `field`/`operator`, and
345
+ report the offending path (`conditions[0].when.any[1].operator`). Operator
346
+ *names* are deliberately not checked there, since custom operators may be
347
+ registered after the template is defined.
348
+
308
349
  #### How values are compared
309
350
 
310
351
  `==` and `!=` compare **strictly** — `'100'` does not equal `100`.
@@ -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-D-oPxxun.js';
2
- import { m as ResolverFn, g as ApprovalTemplateConfig, A as ApprovalTemplate, j as AuditContext, a as ApprovalInstance, e as ApprovalMode, b as AuditEntry } from './instance-D8D7b07N.js';
3
- import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-BdfVjYa8.js';
1
+ import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from './IStorageAdapter-D7jsZT6O.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-CtkmEkYa.js';
3
+ import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-Hn70VShV.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-KGAwT9f-.js';
8
- import { I as IAuditAdapter } from './IAuditAdapter-B3vvU09m.js';
7
+ import { I as IOperationMiddleware, a as ApprovalError } from './IOperationMiddleware-BCcAqSzT.js';
8
+ import { I as IAuditAdapter } from './IAuditAdapter-CaM3A2Kt.js';
9
9
  import { I as IMetricsAdapter } from './IMetricsAdapter-D9PUz4tM.js';
10
10
  import { I as ISchedulerAdapter } from './ISchedulerAdapter-DKv_QjVN.js';
11
- import { I as IAuthorizationPolicy } from './IAuthorizationPolicy-CZESF3CJ.js';
11
+ import { I as IAuthorizationPolicy } from './IAuthorizationPolicy-DFD0ELtO.js';
12
12
 
13
13
  declare const SubmitOptionsSchema: z.ZodObject<{
14
14
  templateName: z.ZodString;
@@ -150,6 +150,24 @@ type ConditionOperatorFn = (actual: unknown, expected: unknown) => boolean;
150
150
  * @returns The numeric value, or `null` when the value is not comparable.
151
151
  */
152
152
  declare function toComparableNumber(value: unknown): number | null;
153
+ /**
154
+ * Statically check a condition expression tree, collecting every problem rather
155
+ * than throwing on the first.
156
+ *
157
+ * Runs at template-definition time so a malformed group is caught while the
158
+ * author is looking at it, instead of at submit time on a real document. The
159
+ * operator check is deliberately deferred: custom operators can be registered
160
+ * after a template is defined, so an unknown name is only an error once the
161
+ * condition is actually evaluated.
162
+ *
163
+ * @param expression - The `when` expression to check.
164
+ * @param path - Field path prefix used in reported errors.
165
+ * @returns One entry per problem found; empty when the tree is well formed.
166
+ */
167
+ declare function validateConditionExpression(expression: ConditionExpression, path: string): Array<{
168
+ field: string;
169
+ message: string;
170
+ }>;
153
171
 
154
172
  interface ValidationResult {
155
173
  valid: boolean;
@@ -435,4 +453,4 @@ declare class ApprovalEngine {
435
453
  private runExternalAudit;
436
454
  }
437
455
 
438
- 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 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 OverrideOptions as k, type PreviewResult as l, type RejectOptions as m, type ResubmitOptions as n, type RetryPolicy as o, defaultIdGenerator as p, toComparableNumber as t, weekendCalendar as w };
456
+ 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 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 OverrideOptions as k, type PreviewResult as l, type RejectOptions as m, type ResubmitOptions as n, type RetryPolicy as o, defaultIdGenerator as p, 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-BibHlgPw.cjs';
2
- import { m as ResolverFn, g as ApprovalTemplateConfig, A as ApprovalTemplate, j as AuditContext, a as ApprovalInstance, e as ApprovalMode, b as AuditEntry } from './instance-D8D7b07N.cjs';
3
- import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-DVVmXU6a.cjs';
1
+ import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from './IStorageAdapter-egPQD6Ry.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-CtkmEkYa.cjs';
3
+ import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-Ci0gf6ac.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-CXgXmGUF.cjs';
8
- import { I as IAuditAdapter } from './IAuditAdapter-B_DhuPsU.cjs';
7
+ import { I as IOperationMiddleware, a as ApprovalError } from './IOperationMiddleware-DOlOraCX.cjs';
8
+ import { I as IAuditAdapter } from './IAuditAdapter-BODIlw4h.cjs';
9
9
  import { I as IMetricsAdapter } from './IMetricsAdapter-D9PUz4tM.cjs';
10
10
  import { I as ISchedulerAdapter } from './ISchedulerAdapter-DKv_QjVN.cjs';
11
- import { I as IAuthorizationPolicy } from './IAuthorizationPolicy-B6JzRNUk.cjs';
11
+ import { I as IAuthorizationPolicy } from './IAuthorizationPolicy-cT7LSHtl.cjs';
12
12
 
13
13
  declare const SubmitOptionsSchema: z.ZodObject<{
14
14
  templateName: z.ZodString;
@@ -150,6 +150,24 @@ type ConditionOperatorFn = (actual: unknown, expected: unknown) => boolean;
150
150
  * @returns The numeric value, or `null` when the value is not comparable.
151
151
  */
152
152
  declare function toComparableNumber(value: unknown): number | null;
153
+ /**
154
+ * Statically check a condition expression tree, collecting every problem rather
155
+ * than throwing on the first.
156
+ *
157
+ * Runs at template-definition time so a malformed group is caught while the
158
+ * author is looking at it, instead of at submit time on a real document. The
159
+ * operator check is deliberately deferred: custom operators can be registered
160
+ * after a template is defined, so an unknown name is only an error once the
161
+ * condition is actually evaluated.
162
+ *
163
+ * @param expression - The `when` expression to check.
164
+ * @param path - Field path prefix used in reported errors.
165
+ * @returns One entry per problem found; empty when the tree is well formed.
166
+ */
167
+ declare function validateConditionExpression(expression: ConditionExpression, path: string): Array<{
168
+ field: string;
169
+ message: string;
170
+ }>;
153
171
 
154
172
  interface ValidationResult {
155
173
  valid: boolean;
@@ -435,4 +453,4 @@ declare class ApprovalEngine {
435
453
  private runExternalAudit;
436
454
  }
437
455
 
438
- 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 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 OverrideOptions as k, type PreviewResult as l, type RejectOptions as m, type ResubmitOptions as n, type RetryPolicy as o, defaultIdGenerator as p, toComparableNumber as t, weekendCalendar as w };
456
+ 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 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 OverrideOptions as k, type PreviewResult as l, type RejectOptions as m, type ResubmitOptions as n, type RetryPolicy as o, defaultIdGenerator as p, toComparableNumber as t, validateConditionExpression as v, weekendCalendar as w };
@@ -1,4 +1,4 @@
1
- import { b as AuditEntry, a as ApprovalInstance } from './instance-D8D7b07N.cjs';
1
+ import { b as AuditEntry, a as ApprovalInstance } from './instance-CtkmEkYa.cjs';
2
2
 
3
3
  interface IAuditAdapter {
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { b as AuditEntry, a as ApprovalInstance } from './instance-D8D7b07N.js';
1
+ import { b as AuditEntry, a as ApprovalInstance } from './instance-CtkmEkYa.js';
2
2
 
3
3
  interface IAuditAdapter {
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-D8D7b07N.js';
1
+ import { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-CtkmEkYa.js';
2
2
 
3
3
  interface AuthorizationContext {
4
4
  operation: 'submit' | 'approve' | 'reject' | 'delegate' | 'reassign' | 'cancel' | 'escalate' | 'override' | 'resubmit' | 'addComment';
@@ -1,4 +1,4 @@
1
- import { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-D8D7b07N.cjs';
1
+ import { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-CtkmEkYa.cjs';
2
2
 
3
3
  interface AuthorizationContext {
4
4
  operation: 'submit' | 'approve' | 'reject' | 'delegate' | 'reassign' | 'cancel' | 'escalate' | 'override' | 'resubmit' | 'addComment';
@@ -1,4 +1,4 @@
1
- import { a as ApprovalInstance, b as AuditEntry } from './instance-D8D7b07N.cjs';
1
+ import { a as ApprovalInstance, b as AuditEntry } from './instance-CtkmEkYa.cjs';
2
2
 
3
3
  interface ApprovalEvent {
4
4
  instanceId: string;
@@ -1,4 +1,4 @@
1
- import { a as ApprovalInstance, b as AuditEntry } from './instance-D8D7b07N.js';
1
+ import { a as ApprovalInstance, b as AuditEntry } from './instance-CtkmEkYa.js';
2
2
 
3
3
  interface ApprovalEvent {
4
4
  instanceId: string;
@@ -1,4 +1,4 @@
1
- import { a as ApprovalInstance } from './instance-D8D7b07N.js';
1
+ import { a as ApprovalInstance } from './instance-CtkmEkYa.js';
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-D8D7b07N.cjs';
1
+ import { a as ApprovalInstance } from './instance-CtkmEkYa.cjs';
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-D8D7b07N.js';
1
+ import { A as ApprovalTemplate, a as ApprovalInstance, f as ApprovalStatus, b as AuditEntry } from './instance-CtkmEkYa.js';
2
2
 
3
3
  interface PaginationOpts {
4
4
  limit: number;
@@ -1,4 +1,4 @@
1
- import { A as ApprovalTemplate, a as ApprovalInstance, f as ApprovalStatus, b as AuditEntry } from './instance-D8D7b07N.cjs';
1
+ import { A as ApprovalTemplate, a as ApprovalInstance, f as ApprovalStatus, b as AuditEntry } from './instance-CtkmEkYa.cjs';
2
2
 
3
3
  interface PaginationOpts {
4
4
  limit: number;
@@ -1,5 +1,5 @@
1
- import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from '../IStorageAdapter-BibHlgPw.cjs';
2
- import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-D8D7b07N.cjs';
1
+ import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from '../IStorageAdapter-egPQD6Ry.cjs';
2
+ import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-CtkmEkYa.cjs';
3
3
 
4
4
  declare class MemoryAdapter implements IStorageAdapter {
5
5
  private templates;
@@ -1,5 +1,5 @@
1
- import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from '../IStorageAdapter-D-oPxxun.js';
2
- import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-D8D7b07N.js';
1
+ import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from '../IStorageAdapter-D7jsZT6O.js';
2
+ import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-CtkmEkYa.js';
3
3
 
4
4
  declare class MemoryAdapter implements IStorageAdapter {
5
5
  private templates;
@@ -1,7 +1,7 @@
1
1
  import * as tls from 'tls';
2
2
  import * as pg from 'pg';
3
- import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from '../IStorageAdapter-BibHlgPw.cjs';
4
- import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-D8D7b07N.cjs';
3
+ import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from '../IStorageAdapter-egPQD6Ry.cjs';
4
+ import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-CtkmEkYa.cjs';
5
5
 
6
6
  interface PostgresAdapterOptions {
7
7
  connectionString?: string;
@@ -1,7 +1,7 @@
1
1
  import * as tls from 'tls';
2
2
  import * as pg from 'pg';
3
- import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from '../IStorageAdapter-D-oPxxun.js';
4
- import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-D8D7b07N.js';
3
+ import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from '../IStorageAdapter-D7jsZT6O.js';
4
+ import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-CtkmEkYa.js';
5
5
 
6
6
  interface PostgresAdapterOptions {
7
7
  connectionString?: string;
package/dist/index.cjs CHANGED
@@ -498,16 +498,88 @@ function evaluateCondition(condition, data) {
498
498
  const actual = getField(data, condition.field);
499
499
  return fn(actual, condition.value);
500
500
  }
501
- function evaluateRule(rule, data) {
502
- if (Array.isArray(rule)) {
503
- return rule.every((c) => evaluateCondition(c, data));
501
+ function asGroup(expression) {
502
+ if (expression === null || typeof expression !== "object" || Array.isArray(expression)) {
503
+ return null;
504
+ }
505
+ const candidate = expression;
506
+ const keys = ["all", "any", "not"].filter((k) => candidate[k] !== void 0);
507
+ if (keys.length === 0) return null;
508
+ if (keys.length > 1) {
509
+ throw new ApprovalValidationError(
510
+ `Condition group must set exactly one of "all", "any" or "not" (got ${keys.join(", ")}).`
511
+ );
512
+ }
513
+ const key = keys[0];
514
+ if (key !== "not" && !Array.isArray(candidate[key])) {
515
+ throw new ApprovalValidationError(`Condition group "${key}" must be an array of expressions.`);
516
+ }
517
+ if (key !== "not" && candidate[key].length === 0) {
518
+ throw new ApprovalValidationError(`Condition group "${key}" must not be empty.`);
519
+ }
520
+ return expression;
521
+ }
522
+ function evaluateExpression(expression, data) {
523
+ if (Array.isArray(expression)) {
524
+ return expression.every((child) => evaluateExpression(child, data));
525
+ }
526
+ const group = asGroup(expression);
527
+ if (group === null) {
528
+ return evaluateCondition(expression, data);
529
+ }
530
+ if (group.all !== void 0) {
531
+ return group.all.every((child) => evaluateExpression(child, data));
532
+ }
533
+ if (group.any !== void 0) {
534
+ return group.any.some((child) => evaluateExpression(child, data));
504
535
  }
505
- return evaluateCondition(rule, data);
536
+ return !evaluateExpression(group.not, data);
537
+ }
538
+ function validateConditionExpression(expression, path) {
539
+ const errors = [];
540
+ const walk = (node, at) => {
541
+ if (Array.isArray(node)) {
542
+ if (node.length === 0) {
543
+ errors.push({ field: at, message: "Condition list must not be empty." });
544
+ }
545
+ node.forEach((child, i) => walk(child, `${at}[${i}]`));
546
+ return;
547
+ }
548
+ let group;
549
+ try {
550
+ group = asGroup(node);
551
+ } catch (err) {
552
+ errors.push({ field: at, message: err.message });
553
+ return;
554
+ }
555
+ if (group === null) {
556
+ const leaf = node;
557
+ if (typeof leaf?.field !== "string" || leaf.field.length === 0) {
558
+ errors.push({
559
+ field: `${at}.field`,
560
+ message: "Condition requires a non-empty field path."
561
+ });
562
+ }
563
+ if (typeof leaf?.operator !== "string" || leaf.operator.length === 0) {
564
+ errors.push({ field: `${at}.operator`, message: "Condition requires an operator." });
565
+ }
566
+ return;
567
+ }
568
+ if (group.all !== void 0) {
569
+ group.all.forEach((child, i) => walk(child, `${at}.all[${i}]`));
570
+ } else if (group.any !== void 0) {
571
+ group.any.forEach((child, i) => walk(child, `${at}.any[${i}]`));
572
+ } else {
573
+ walk(group.not, `${at}.not`);
574
+ }
575
+ };
576
+ walk(expression, path);
577
+ return errors;
506
578
  }
507
579
  function evaluateConditions(conditions, data) {
508
580
  const mutations = { addLevels: [], skipLevels: /* @__PURE__ */ new Set() };
509
581
  for (const rule of conditions) {
510
- if (evaluateRule(rule.when, data)) {
582
+ if (evaluateExpression(rule.when, data)) {
511
583
  if (rule.addLevels) mutations.addLevels.push(...rule.addLevels);
512
584
  if (rule.skipLevels) rule.skipLevels.forEach((l) => mutations.skipLevels.add(l));
513
585
  }
@@ -805,6 +877,9 @@ var ApprovalEngine = class {
805
877
  }
806
878
  if (config.conditions) {
807
879
  config.conditions.forEach((rule, ruleIdx) => {
880
+ errors.push(
881
+ ...validateConditionExpression(rule.when, `conditions[${ruleIdx}].when`)
882
+ );
808
883
  if (rule.addLevels) {
809
884
  rule.addLevels.forEach((al, alIdx) => {
810
885
  const conflictsWithStatic = config.levels.some((l) => l.level === al.level);
@@ -2619,6 +2694,7 @@ exports.defaultIdGenerator = defaultIdGenerator;
2619
2694
  exports.noopLogger = noopLogger;
2620
2695
  exports.systemClock = systemClock;
2621
2696
  exports.toComparableNumber = toComparableNumber;
2697
+ exports.validateConditionExpression = validateConditionExpression;
2622
2698
  exports.weekendCalendar = weekendCalendar;
2623
2699
  //# sourceMappingURL=index.cjs.map
2624
2700
  //# sourceMappingURL=index.cjs.map