hierarchical-approval 0.6.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.
- package/CHANGELOG.md +112 -0
- package/README.md +84 -4
- package/dist/{ApprovalEngine-D3rHIli1.d.ts → ApprovalEngine-DFY53zRK.d.ts} +45 -7
- package/dist/{ApprovalEngine-CIQFYhsX.d.cts → ApprovalEngine-DVMMtiSm.d.cts} +45 -7
- package/dist/{IAuditAdapter-B_DhuPsU.d.cts → IAuditAdapter-BODIlw4h.d.cts} +1 -1
- package/dist/{IAuditAdapter-B3vvU09m.d.ts → IAuditAdapter-CaM3A2Kt.d.ts} +1 -1
- package/dist/{IAuthorizationPolicy-CZESF3CJ.d.ts → IAuthorizationPolicy-DFD0ELtO.d.ts} +1 -1
- package/dist/{IAuthorizationPolicy-B6JzRNUk.d.cts → IAuthorizationPolicy-cT7LSHtl.d.cts} +1 -1
- package/dist/{INotificationAdapter-DVVmXU6a.d.cts → INotificationAdapter-Ci0gf6ac.d.cts} +1 -1
- package/dist/{INotificationAdapter-BdfVjYa8.d.ts → INotificationAdapter-Hn70VShV.d.ts} +1 -1
- package/dist/{IOperationMiddleware-KGAwT9f-.d.ts → IOperationMiddleware-BCcAqSzT.d.ts} +1 -1
- package/dist/{IOperationMiddleware-CXgXmGUF.d.cts → IOperationMiddleware-DOlOraCX.d.cts} +1 -1
- package/dist/{IStorageAdapter-D-oPxxun.d.ts → IStorageAdapter-D7jsZT6O.d.ts} +1 -1
- package/dist/{IStorageAdapter-BibHlgPw.d.cts → IStorageAdapter-egPQD6Ry.d.cts} +1 -1
- package/dist/adapters/MemoryAdapter.d.cts +2 -2
- package/dist/adapters/MemoryAdapter.d.ts +2 -2
- package/dist/adapters/PostgresAdapter.d.cts +2 -2
- package/dist/adapters/PostgresAdapter.d.ts +2 -2
- package/dist/index.cjs +109 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -8
- package/dist/index.d.ts +8 -8
- package/dist/index.js +108 -11
- package/dist/index.js.map +1 -1
- package/dist/{instance-D8D7b07N.d.cts → instance-CtkmEkYa.d.cts} +34 -2
- package/dist/{instance-D8D7b07N.d.ts → instance-CtkmEkYa.d.ts} +34 -2
- package/dist/nestjs.cjs +107 -10
- package/dist/nestjs.cjs.map +1 -1
- package/dist/nestjs.d.cts +7 -7
- package/dist/nestjs.d.ts +7 -7
- package/dist/nestjs.js +107 -10
- package/dist/nestjs.js.map +1 -1
- package/dist/plugins/audit.d.cts +2 -2
- package/dist/plugins/audit.d.ts +2 -2
- package/dist/plugins/notify.d.cts +2 -2
- package/dist/plugins/notify.d.ts +2 -2
- package/dist/plugins/resilience.d.cts +3 -3
- package/dist/plugins/resilience.d.ts +3 -3
- package/dist/plugins/tracing.d.cts +2 -2
- package/dist/plugins/tracing.d.ts +2 -2
- package/dist/plugins/webhook.d.cts +2 -2
- package/dist/plugins/webhook.d.ts +2 -2
- package/dist/testing.cjs +107 -10
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +7 -7
- package/dist/testing.d.ts +7 -7
- package/dist/testing.js +107 -10
- package/dist/testing.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,118 @@ 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
|
+
|
|
55
|
+
## [0.7.0] - 2026-09-04
|
|
56
|
+
|
|
57
|
+
### Fixed — two condition-evaluation bypasses
|
|
58
|
+
|
|
59
|
+
Both of these let a `ConditionRule` fire when it should not have, and because
|
|
60
|
+
conditions decide which levels an instance gets, a spurious match on a
|
|
61
|
+
`skipLevels` rule **removes approval levels from a live document**. Anyone using
|
|
62
|
+
`skipLevels` — or `addLevels` to *escalate* above a threshold — should upgrade.
|
|
63
|
+
|
|
64
|
+
- **Numeric operators no longer coerce non-numbers to zero.** `>`, `<`, `>=` and
|
|
65
|
+
`<=` compared with `Number(actual)`, and `Number()` maps `null`, `''`, `' '`,
|
|
66
|
+
`[]` and `false` all to `0`. So a fast-track rule like
|
|
67
|
+
`{ when: { field: 'amount', operator: '<', value: 5000 }, skipLevels: [2, 3] }`
|
|
68
|
+
matched a purchase order whose `amount` was `null` or blank, silently skipping
|
|
69
|
+
two approval levels on exactly the documents whose value was unknown. `false`
|
|
70
|
+
and `[]` did the same, and `true` compared as `1`.
|
|
71
|
+
|
|
72
|
+
A numeric comparison against a non-number is now treated as *undecidable* rather
|
|
73
|
+
than false-y: it reports **no match**, which is the outcome `undefined` has
|
|
74
|
+
always produced. Accepted operands are finite numbers, bigints, `Date`
|
|
75
|
+
(compared as epoch milliseconds), and numeric strings such as `'100'` or
|
|
76
|
+
`' 1e3 '` — ERP payloads routinely arrive as JSON strings, so string comparison
|
|
77
|
+
is retained. Rejected: `null`, `undefined`, booleans, arrays, objects, blank
|
|
78
|
+
strings, `NaN` and `Infinity`.
|
|
79
|
+
|
|
80
|
+
**Behaviour change.** A rule that was matching on blank or boolean data stops
|
|
81
|
+
matching. That is the fix, but it does change which levels such an instance
|
|
82
|
+
gets, so re-check any template whose conditions run against optional fields.
|
|
83
|
+
`==` and `!=` are untouched — they were already strict.
|
|
84
|
+
|
|
85
|
+
- **Dot-path field lookup now reads own properties only.** `getField` tested
|
|
86
|
+
`key in obj`, which walks the prototype chain, so a condition on `isFastTrack`
|
|
87
|
+
was satisfied by an inherited `Object.prototype.isFastTrack` that no document
|
|
88
|
+
ever declared — turning any prototype pollution elsewhere in the dependency
|
|
89
|
+
tree into an approval-level bypass. Resolution now uses
|
|
90
|
+
`Object.prototype.hasOwnProperty`, so a segment that is not an own property
|
|
91
|
+
resolves to `undefined` exactly as an absent field does. This also closes
|
|
92
|
+
`__proto__`, `constructor` and `prototype` as readable paths.
|
|
93
|
+
|
|
94
|
+
**Behaviour change.** Context data whose fields live on a prototype (a class
|
|
95
|
+
instance with getters, rather than a plain object) no longer resolves. Plain
|
|
96
|
+
objects, arrays, array indices and `Object.create(null)` objects are
|
|
97
|
+
unaffected, and context data does not survive JSONB round-tripping as a class
|
|
98
|
+
instance in any case.
|
|
99
|
+
|
|
100
|
+
### Added
|
|
101
|
+
|
|
102
|
+
- **`toComparableNumber(value)` is exported from the package root.** The same
|
|
103
|
+
strict coercion the built-in numeric operators use, returning `number` or
|
|
104
|
+
`null`, so a custom operator registered with `registerConditionOperator()` can
|
|
105
|
+
inherit the identical semantics instead of re-introducing `Number()`. The
|
|
106
|
+
README's `between` recipe now uses it — the previous version of that snippet
|
|
107
|
+
demonstrated the zero-coercion bug.
|
|
108
|
+
|
|
109
|
+
### Tests
|
|
110
|
+
|
|
111
|
+
- Restored the **17 `ConditionEvaluator` unit tests that commit `8d648d8`
|
|
112
|
+
deleted**, having replaced the suite body with a `// ... existing tests ...`
|
|
113
|
+
placeholder and a single test. Coverage of the evaluator had silently dropped
|
|
114
|
+
to 74% of statements and 56% of functions.
|
|
115
|
+
- Added 36 tests across the two fixes (per-type non-comparable operand matrix for
|
|
116
|
+
each numeric operator, the `skipLevels` bypass scenario, prototype-pollution
|
|
117
|
+
guards, `toComparableNumber` directly) plus an executable copy of the README's
|
|
118
|
+
custom-operator recipes, so a documented snippet cannot rot or stop compiling.
|
|
119
|
+
All 24 of the pre-existing-bug assertions were confirmed to fail against the
|
|
120
|
+
unfixed source.
|
|
121
|
+
|
|
10
122
|
## [0.6.0] - 2026-08-21
|
|
11
123
|
|
|
12
124
|
### Fixed — event delivery, template reads, and the CI lint gate
|
package/README.md
CHANGED
|
@@ -305,6 +305,74 @@ 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
|
+
|
|
349
|
+
#### How values are compared
|
|
350
|
+
|
|
351
|
+
`==` and `!=` compare **strictly** — `'100'` does not equal `100`.
|
|
352
|
+
|
|
353
|
+
The numeric operators (`>`, `<`, `>=`, `<=`) match only when **both** sides are
|
|
354
|
+
unambiguously numeric: finite numbers, bigints, `Date` (compared as epoch
|
|
355
|
+
milliseconds), and numeric strings such as `'100'` or `' 1e3 '` — because ERP
|
|
356
|
+
payloads routinely arrive as JSON strings. Anything else — `null`, `undefined`,
|
|
357
|
+
`true`/`false`, arrays, objects, blank strings, `NaN`, `Infinity` — is treated as
|
|
358
|
+
*not comparable*, and the condition reports **no match**.
|
|
359
|
+
|
|
360
|
+
This matters for safety. A rule that skips levels on small amounts must not fire
|
|
361
|
+
on a document whose amount was never populated:
|
|
362
|
+
|
|
363
|
+
```ts
|
|
364
|
+
{ when: { field: 'amount', operator: '<', value: 5000 }, skipLevels: [2, 3] }
|
|
365
|
+
|
|
366
|
+
// amount: 4999 -> matches, levels 2 and 3 are skipped
|
|
367
|
+
// amount: null -> NO match, every level is kept
|
|
368
|
+
// amount: '' -> NO match, every level is kept
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
Field paths resolve **own properties only**, so an inherited or polluted
|
|
372
|
+
`Object.prototype` member can never satisfy a condition. A path segment that is
|
|
373
|
+
not an own property — including `__proto__`, `constructor`, and `prototype` —
|
|
374
|
+
resolves to `undefined`, exactly as a genuinely absent field does.
|
|
375
|
+
|
|
308
376
|
**Register custom operators** at engine level:
|
|
309
377
|
|
|
310
378
|
```ts
|
|
@@ -312,11 +380,23 @@ engine.registerConditionOperator(
|
|
|
312
380
|
'contains',
|
|
313
381
|
(actual, expected) => typeof actual === 'string' && actual.includes(String(expected)),
|
|
314
382
|
);
|
|
383
|
+
```
|
|
315
384
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
385
|
+
Custom numeric operators should reuse the same strictness via the exported
|
|
386
|
+
`toComparableNumber` helper, which returns `null` for anything that is not
|
|
387
|
+
unambiguously a number:
|
|
388
|
+
|
|
389
|
+
```ts
|
|
390
|
+
import { toComparableNumber } from 'hierarchical-approval';
|
|
391
|
+
|
|
392
|
+
engine.registerConditionOperator('between', (actual, expected) => {
|
|
393
|
+
const value = toComparableNumber(actual);
|
|
394
|
+
if (value === null || !Array.isArray(expected) || expected.length !== 2) return false;
|
|
395
|
+
const min = toComparableNumber(expected[0]);
|
|
396
|
+
const max = toComparableNumber(expected[1]);
|
|
397
|
+
if (min === null || max === null) return false;
|
|
398
|
+
return value >= min && value <= max;
|
|
399
|
+
});
|
|
320
400
|
```
|
|
321
401
|
|
|
322
402
|
---
|
|
@@ -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-
|
|
2
|
-
import {
|
|
3
|
-
import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-
|
|
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-
|
|
8
|
-
import { I as IAuditAdapter } from './IAuditAdapter-
|
|
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-
|
|
11
|
+
import { I as IAuthorizationPolicy } from './IAuthorizationPolicy-DFD0ELtO.js';
|
|
12
12
|
|
|
13
13
|
declare const SubmitOptionsSchema: z.ZodObject<{
|
|
14
14
|
templateName: z.ZodString;
|
|
@@ -130,6 +130,44 @@ type ApproverResolverFn = (config: Record<string, unknown>, ctx: {
|
|
|
130
130
|
}) => Promise<string[]> | string[];
|
|
131
131
|
|
|
132
132
|
type ConditionOperatorFn = (actual: unknown, expected: unknown) => boolean;
|
|
133
|
+
/**
|
|
134
|
+
* Coerce a value to a number *only* when it unambiguously represents one.
|
|
135
|
+
*
|
|
136
|
+
* Plain `Number()` maps `null`, `''`, `' '`, `[]` and `false` all to `0`, which
|
|
137
|
+
* in an approval engine is an approval-bypass hazard: a rule such as
|
|
138
|
+
* `{ amount: '<' 5000 } -> skipLevels: [2, 3]` would fire on a document whose
|
|
139
|
+
* `amount` is missing or blank, silently skipping two approval levels. Numeric
|
|
140
|
+
* comparison against a value that is not a number is not "false-y", it is
|
|
141
|
+
* *undecidable*, so this returns `null` and the comparison reports no match —
|
|
142
|
+
* the same outcome `undefined` has always produced.
|
|
143
|
+
*
|
|
144
|
+
* Accepted: finite numbers, bigints, `Date` (compared as epoch ms), and numeric
|
|
145
|
+
* strings such as `'100'` or `' 1e3 '` (ERP payloads routinely arrive as JSON
|
|
146
|
+
* strings). Rejected: `null`, `undefined`, booleans, arrays, objects, blank
|
|
147
|
+
* strings, and the non-finite `NaN` / `Infinity`.
|
|
148
|
+
*
|
|
149
|
+
* @param value - The raw value taken from the condition or the context data.
|
|
150
|
+
* @returns The numeric value, or `null` when the value is not comparable.
|
|
151
|
+
*/
|
|
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
|
+
}>;
|
|
133
171
|
|
|
134
172
|
interface ValidationResult {
|
|
135
173
|
valid: boolean;
|
|
@@ -415,4 +453,4 @@ declare class ApprovalEngine {
|
|
|
415
453
|
private runExternalAudit;
|
|
416
454
|
}
|
|
417
455
|
|
|
418
|
-
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, 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-
|
|
2
|
-
import {
|
|
3
|
-
import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-
|
|
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-
|
|
8
|
-
import { I as IAuditAdapter } from './IAuditAdapter-
|
|
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-
|
|
11
|
+
import { I as IAuthorizationPolicy } from './IAuthorizationPolicy-cT7LSHtl.cjs';
|
|
12
12
|
|
|
13
13
|
declare const SubmitOptionsSchema: z.ZodObject<{
|
|
14
14
|
templateName: z.ZodString;
|
|
@@ -130,6 +130,44 @@ type ApproverResolverFn = (config: Record<string, unknown>, ctx: {
|
|
|
130
130
|
}) => Promise<string[]> | string[];
|
|
131
131
|
|
|
132
132
|
type ConditionOperatorFn = (actual: unknown, expected: unknown) => boolean;
|
|
133
|
+
/**
|
|
134
|
+
* Coerce a value to a number *only* when it unambiguously represents one.
|
|
135
|
+
*
|
|
136
|
+
* Plain `Number()` maps `null`, `''`, `' '`, `[]` and `false` all to `0`, which
|
|
137
|
+
* in an approval engine is an approval-bypass hazard: a rule such as
|
|
138
|
+
* `{ amount: '<' 5000 } -> skipLevels: [2, 3]` would fire on a document whose
|
|
139
|
+
* `amount` is missing or blank, silently skipping two approval levels. Numeric
|
|
140
|
+
* comparison against a value that is not a number is not "false-y", it is
|
|
141
|
+
* *undecidable*, so this returns `null` and the comparison reports no match —
|
|
142
|
+
* the same outcome `undefined` has always produced.
|
|
143
|
+
*
|
|
144
|
+
* Accepted: finite numbers, bigints, `Date` (compared as epoch ms), and numeric
|
|
145
|
+
* strings such as `'100'` or `' 1e3 '` (ERP payloads routinely arrive as JSON
|
|
146
|
+
* strings). Rejected: `null`, `undefined`, booleans, arrays, objects, blank
|
|
147
|
+
* strings, and the non-finite `NaN` / `Infinity`.
|
|
148
|
+
*
|
|
149
|
+
* @param value - The raw value taken from the condition or the context data.
|
|
150
|
+
* @returns The numeric value, or `null` when the value is not comparable.
|
|
151
|
+
*/
|
|
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
|
+
}>;
|
|
133
171
|
|
|
134
172
|
interface ValidationResult {
|
|
135
173
|
valid: boolean;
|
|
@@ -415,4 +453,4 @@ declare class ApprovalEngine {
|
|
|
415
453
|
private runExternalAudit;
|
|
416
454
|
}
|
|
417
455
|
|
|
418
|
-
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, 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 { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-
|
|
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-
|
|
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 ApprovalTemplate, a as ApprovalInstance, f as ApprovalStatus, b as AuditEntry } from './instance-
|
|
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-
|
|
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-
|
|
2
|
-
import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-
|
|
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-
|
|
2
|
-
import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-
|
|
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-
|
|
4
|
-
import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-
|
|
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-
|
|
4
|
-
import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-
|
|
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
|
@@ -439,11 +439,33 @@ var EscalationScheduler = class {
|
|
|
439
439
|
};
|
|
440
440
|
|
|
441
441
|
// src/engine/ConditionEvaluator.ts
|
|
442
|
+
function toComparableNumber(value) {
|
|
443
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
444
|
+
if (typeof value === "bigint") return Number(value);
|
|
445
|
+
if (value instanceof Date) {
|
|
446
|
+
const time = value.getTime();
|
|
447
|
+
return Number.isFinite(time) ? time : null;
|
|
448
|
+
}
|
|
449
|
+
if (typeof value === "string") {
|
|
450
|
+
if (value.trim() === "") return null;
|
|
451
|
+
const parsed = Number(value);
|
|
452
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
453
|
+
}
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
function numeric(compare) {
|
|
457
|
+
return (actual, expected) => {
|
|
458
|
+
const left = toComparableNumber(actual);
|
|
459
|
+
const right = toComparableNumber(expected);
|
|
460
|
+
if (left === null || right === null) return false;
|
|
461
|
+
return compare(left, right);
|
|
462
|
+
};
|
|
463
|
+
}
|
|
442
464
|
var operatorRegistry = /* @__PURE__ */ new Map([
|
|
443
|
-
[">", (a, e) =>
|
|
444
|
-
["<", (a, e) =>
|
|
445
|
-
[">=", (a, e) =>
|
|
446
|
-
["<=", (a, e) =>
|
|
465
|
+
[">", numeric((a, e) => a > e)],
|
|
466
|
+
["<", numeric((a, e) => a < e)],
|
|
467
|
+
[">=", numeric((a, e) => a >= e)],
|
|
468
|
+
["<=", numeric((a, e) => a <= e)],
|
|
447
469
|
["==", (a, e) => a === e],
|
|
448
470
|
["!=", (a, e) => a !== e],
|
|
449
471
|
["in", (a, e) => Array.isArray(e) && e.includes(a)],
|
|
@@ -460,7 +482,7 @@ function registerConditionOperator(name, fn) {
|
|
|
460
482
|
}
|
|
461
483
|
function getField(data, path) {
|
|
462
484
|
return path.split(".").reduce((obj, key) => {
|
|
463
|
-
if (obj !== null && typeof obj === "object" && key
|
|
485
|
+
if (obj !== null && typeof obj === "object" && Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
464
486
|
return obj[key];
|
|
465
487
|
}
|
|
466
488
|
return void 0;
|
|
@@ -476,16 +498,88 @@ function evaluateCondition(condition, data) {
|
|
|
476
498
|
const actual = getField(data, condition.field);
|
|
477
499
|
return fn(actual, condition.value);
|
|
478
500
|
}
|
|
479
|
-
function
|
|
480
|
-
if (Array.isArray(
|
|
481
|
-
return
|
|
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);
|
|
482
529
|
}
|
|
483
|
-
|
|
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));
|
|
535
|
+
}
|
|
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;
|
|
484
578
|
}
|
|
485
579
|
function evaluateConditions(conditions, data) {
|
|
486
580
|
const mutations = { addLevels: [], skipLevels: /* @__PURE__ */ new Set() };
|
|
487
581
|
for (const rule of conditions) {
|
|
488
|
-
if (
|
|
582
|
+
if (evaluateExpression(rule.when, data)) {
|
|
489
583
|
if (rule.addLevels) mutations.addLevels.push(...rule.addLevels);
|
|
490
584
|
if (rule.skipLevels) rule.skipLevels.forEach((l) => mutations.skipLevels.add(l));
|
|
491
585
|
}
|
|
@@ -783,6 +877,9 @@ var ApprovalEngine = class {
|
|
|
783
877
|
}
|
|
784
878
|
if (config.conditions) {
|
|
785
879
|
config.conditions.forEach((rule, ruleIdx) => {
|
|
880
|
+
errors.push(
|
|
881
|
+
...validateConditionExpression(rule.when, `conditions[${ruleIdx}].when`)
|
|
882
|
+
);
|
|
786
883
|
if (rule.addLevels) {
|
|
787
884
|
rule.addLevels.forEach((al, alIdx) => {
|
|
788
885
|
const conflictsWithStatic = config.levels.some((l) => l.level === al.level);
|
|
@@ -2596,6 +2693,8 @@ exports.MemoryAdapter = MemoryAdapter;
|
|
|
2596
2693
|
exports.defaultIdGenerator = defaultIdGenerator;
|
|
2597
2694
|
exports.noopLogger = noopLogger;
|
|
2598
2695
|
exports.systemClock = systemClock;
|
|
2696
|
+
exports.toComparableNumber = toComparableNumber;
|
|
2697
|
+
exports.validateConditionExpression = validateConditionExpression;
|
|
2599
2698
|
exports.weekendCalendar = weekendCalendar;
|
|
2600
2699
|
//# sourceMappingURL=index.cjs.map
|
|
2601
2700
|
//# sourceMappingURL=index.cjs.map
|