hierarchical-approval 0.5.0 → 0.6.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 +128 -0
- package/README.md +66 -0
- package/dist/{ApprovalEngine-mkh6D9n9.d.cts → ApprovalEngine-CIQFYhsX.d.cts} +100 -18
- package/dist/{ApprovalEngine-jy4cYeES.d.ts → ApprovalEngine-D3rHIli1.d.ts} +100 -18
- package/dist/ISchedulerAdapter-DKv_QjVN.d.cts +13 -0
- package/dist/ISchedulerAdapter-DKv_QjVN.d.ts +13 -0
- package/dist/{IStorageAdapter-ClBdEeTj.d.cts → IStorageAdapter-BibHlgPw.d.cts} +1 -1
- package/dist/{IStorageAdapter-8ajiGUeu.d.ts → IStorageAdapter-D-oPxxun.d.ts} +1 -1
- package/dist/{Logger-CIzvalFc.d.cts → Logger-BplhlU7l.d.cts} +1 -0
- package/dist/{Logger-CIzvalFc.d.ts → Logger-BplhlU7l.d.ts} +1 -0
- package/dist/adapters/MemoryAdapter.cjs +19 -7
- package/dist/adapters/MemoryAdapter.cjs.map +1 -1
- package/dist/adapters/MemoryAdapter.d.cts +2 -2
- package/dist/adapters/MemoryAdapter.d.ts +2 -2
- package/dist/adapters/MemoryAdapter.js +19 -7
- package/dist/adapters/MemoryAdapter.js.map +1 -1
- package/dist/adapters/PostgresAdapter.cjs +60 -31
- package/dist/adapters/PostgresAdapter.cjs.map +1 -1
- package/dist/adapters/PostgresAdapter.d.cts +2 -2
- package/dist/adapters/PostgresAdapter.d.ts +2 -2
- package/dist/adapters/PostgresAdapter.js +60 -31
- package/dist/adapters/PostgresAdapter.js.map +1 -1
- package/dist/index.cjs +717 -99
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -5
- package/dist/index.d.ts +6 -5
- package/dist/index.js +717 -99
- package/dist/index.js.map +1 -1
- package/dist/nestjs.cjs +698 -92
- package/dist/nestjs.cjs.map +1 -1
- package/dist/nestjs.d.cts +4 -3
- package/dist/nestjs.d.ts +4 -3
- package/dist/nestjs.js +698 -92
- package/dist/nestjs.js.map +1 -1
- package/dist/plugins/audit.cjs +2 -0
- package/dist/plugins/audit.cjs.map +1 -1
- package/dist/plugins/audit.d.cts +1 -1
- package/dist/plugins/audit.d.ts +1 -1
- package/dist/plugins/audit.js +2 -0
- package/dist/plugins/audit.js.map +1 -1
- package/dist/plugins/metrics.cjs +2 -0
- package/dist/plugins/metrics.cjs.map +1 -1
- package/dist/plugins/metrics.d.cts +1 -1
- package/dist/plugins/metrics.d.ts +1 -1
- package/dist/plugins/metrics.js +2 -0
- package/dist/plugins/metrics.js.map +1 -1
- package/dist/plugins/notify.cjs +2 -0
- package/dist/plugins/notify.cjs.map +1 -1
- package/dist/plugins/notify.d.cts +1 -1
- package/dist/plugins/notify.d.ts +1 -1
- package/dist/plugins/notify.js +2 -0
- package/dist/plugins/notify.js.map +1 -1
- package/dist/plugins/resilience.cjs +2 -0
- package/dist/plugins/resilience.cjs.map +1 -1
- package/dist/plugins/resilience.d.cts +1 -1
- package/dist/plugins/resilience.d.ts +1 -1
- package/dist/plugins/resilience.js +2 -0
- package/dist/plugins/resilience.js.map +1 -1
- package/dist/plugins/scheduler.cjs +69 -0
- package/dist/plugins/scheduler.cjs.map +1 -0
- package/dist/plugins/scheduler.d.cts +43 -0
- package/dist/plugins/scheduler.d.ts +43 -0
- package/dist/plugins/scheduler.js +67 -0
- package/dist/plugins/scheduler.js.map +1 -0
- package/dist/plugins/webhook.cjs +226 -0
- package/dist/plugins/webhook.cjs.map +1 -0
- package/dist/plugins/webhook.d.cts +214 -0
- package/dist/plugins/webhook.d.ts +214 -0
- package/dist/plugins/webhook.js +221 -0
- package/dist/plugins/webhook.js.map +1 -0
- package/dist/testing.cjs +717 -99
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +4 -3
- package/dist/testing.d.ts +4 -3
- package/dist/testing.js +717 -99
- package/dist/testing.js.map +1 -1
- package/package.json +11 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,134 @@
|
|
|
3
3
|
All notable changes to `hierarchical-approval` are documented here. This project
|
|
4
4
|
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
5
5
|
|
|
6
|
+
## [Unreleased]
|
|
7
|
+
|
|
8
|
+
_Nothing yet._
|
|
9
|
+
|
|
10
|
+
## [0.6.0] - 2026-08-21
|
|
11
|
+
|
|
12
|
+
### Fixed — event delivery, template reads, and the CI lint gate
|
|
13
|
+
|
|
14
|
+
- **A throwing `engine.on()` listener no longer breaks the operation that emitted
|
|
15
|
+
the event.** `EventBus` delivered events straight through `eventemitter3`, which
|
|
16
|
+
invokes listeners synchronously and does not swallow exceptions — and the engine
|
|
17
|
+
emits *after* persisting but *before* dispatching notifications and running
|
|
18
|
+
after-middleware. So one buggy subscriber would reject `approve()` with its own
|
|
19
|
+
error (while the instance stayed persisted as approved), skip notification
|
|
20
|
+
dispatch entirely, leave a tracing span opened and never ended, and suppress the
|
|
21
|
+
`approval:completed` emit on the following line. Listener failures are now
|
|
22
|
+
isolated per listener and reported via the engine's logger; a rejecting `async`
|
|
23
|
+
listener is caught too, instead of surfacing as a process-level
|
|
24
|
+
`unhandledRejection`. **Behaviour change:** a listener error no longer propagates
|
|
25
|
+
to the caller. Register an `onListenerError` sink by passing a `logger`, or use
|
|
26
|
+
`EventBus.setListenerErrorHandler` directly.
|
|
27
|
+
- **`approval:completed` is now delivered to notification adapters.** It was
|
|
28
|
+
emitted on the in-process bus only, so a webhook/email integrator never learned
|
|
29
|
+
a document had been fully approved and had to infer it from `approval:approved`
|
|
30
|
+
plus `isFinal`. Dispatched on both the normal-completion and `override` paths.
|
|
31
|
+
- **`PostgresAdapter.getTemplate`/`listTemplates` now return `createdAt` as a real
|
|
32
|
+
`Date`.** They returned the JSONB payload raw, so `createdAt` was an ISO string
|
|
33
|
+
despite `ApprovalTemplate` typing it as a `Date`. `TemplateRegistry.update()`
|
|
34
|
+
threaded that string into the next `saveTemplate()`, which called
|
|
35
|
+
`createdAt.toISOString()` on it — meaning **`engine.updateTemplate()` failed 100%
|
|
36
|
+
of the time against real PostgreSQL** while passing against `MemoryAdapter`,
|
|
37
|
+
which tolerates the string silently. No migration needed.
|
|
38
|
+
- **CI enforces lint again.** The workflow ran `npm run lint || true`, so lint could
|
|
39
|
+
never fail a build; an unused import had already accumulated on `main` as a
|
|
40
|
+
result. The `|| true` is gone.
|
|
41
|
+
|
|
42
|
+
### Added — cycle-time analytics
|
|
43
|
+
|
|
44
|
+
- **`getStatistics()` now reports time-to-decision.** The returned
|
|
45
|
+
`ApprovalStatistics` gains `cycleTime: CycleTimeStats` and
|
|
46
|
+
`cycleTimeByTemplate: Record<string, CycleTimeStats>`, each shaped as
|
|
47
|
+
`{ count, averageMs, p50Ms, p95Ms, minMs, maxMs }` (all durations in
|
|
48
|
+
milliseconds; new `CycleTimeStats` interface, exported from the package root).
|
|
49
|
+
- Counts instances in status `approved`, `rejected`, or `cancelled`.
|
|
50
|
+
**`expired` is deliberately excluded** — its terminal timestamp reflects a
|
|
51
|
+
scheduler deadline firing, not a decision, so including it would skew the
|
|
52
|
+
distribution rather than describe it.
|
|
53
|
+
- Elapsed time per instance is `updatedAt - createdAt`; no new fields were
|
|
54
|
+
added to `ApprovalInstance` or either storage adapter.
|
|
55
|
+
- Every field is `0` (never `NaN`) when `count` is `0`. `cycleTimeByTemplate`
|
|
56
|
+
mirrors `byTemplate`'s population rule: a template only appears once it has
|
|
57
|
+
at least one completed instance.
|
|
58
|
+
- Adapter-agnostic — computed from existing `getInstancesByFilter` pages, so
|
|
59
|
+
`MemoryAdapter` and `PostgresAdapter` both support it with no changes.
|
|
60
|
+
|
|
61
|
+
### Added — `plugins/webhook`
|
|
62
|
+
|
|
63
|
+
- **`hierarchical-approval/plugins/webhook`** — a sixth built-in plug-in
|
|
64
|
+
subpath: an HTTP `INotificationAdapter` with signing and retry, on its own
|
|
65
|
+
tree-shakeable import path with **zero new dependencies**.
|
|
66
|
+
- `WebhookNotificationAdapter` POSTs each event as JSON to a configured URL.
|
|
67
|
+
`notify()` never throws (per the `INotificationAdapter` contract) — it logs
|
|
68
|
+
and drops the notification once retries are exhausted.
|
|
69
|
+
- **Signing (opt-in via `secret`).** Each request carries an
|
|
70
|
+
`X-Approval-Signature: t=<unix-seconds>,v1=<hex-hmac>` header (Stripe-style),
|
|
71
|
+
the HMAC-SHA256 digest of the signing string `` `<unix-seconds>.<json-body>` ``.
|
|
72
|
+
Unsigned when `secret` is omitted. `DEFAULT_SIGNATURE_HEADER` names the
|
|
73
|
+
default header (`'X-Approval-Signature'`); override via `signatureHeader`.
|
|
74
|
+
- **Retry.** `5xx`, `408`, `429`, and network/timeout errors are retried with
|
|
75
|
+
exponential backoff and full jitter, up to `maxAttempts` (default `3`); a
|
|
76
|
+
`429` honors `Retry-After` (seconds or an HTTP date) in place of the
|
|
77
|
+
computed backoff. Any other `4xx` fails on the first attempt.
|
|
78
|
+
- **Durability.** The public `deliver()` method throws
|
|
79
|
+
`WebhookDeliveryError` (`status?`, `attempts`, `cause?`) on final failure
|
|
80
|
+
instead of swallowing it, so it can be bound as the `transport` of
|
|
81
|
+
`plugins/notify`'s `OutboxNotificationAdapter` for at-least-once delivery
|
|
82
|
+
across process restarts — no adapter shim needed.
|
|
83
|
+
- Ships a new `HttpClient` port (a plain `fetch`-shaped function type) and
|
|
84
|
+
`getDefaultHttpClient()`. **No new dependency** — the port is structurally
|
|
85
|
+
satisfied by the global `fetch` (Node.js 18+); pass a custom `httpClient`
|
|
86
|
+
to use a different implementation.
|
|
87
|
+
|
|
88
|
+
### Added — `plugins/scheduler`, and the `schedulerAdapter` option now works
|
|
89
|
+
|
|
90
|
+
- **`hierarchical-approval/plugins/scheduler`** — ships `InMemorySchedulerAdapter`,
|
|
91
|
+
the first reference implementation of the `ISchedulerAdapter` port, which until
|
|
92
|
+
now was exported from the package root with no implementation anywhere to copy.
|
|
93
|
+
- **`ApprovalEngineOptions.schedulerAdapter` was a no-op.** It was declared and
|
|
94
|
+
documented as "Replaces built-in setInterval polling", but `scheduleAt` and
|
|
95
|
+
`cancel` were never called — an injected BullMQ/Temporal/cron scheduler was
|
|
96
|
+
silently ignored while the built-in poller kept running. When supplied it now
|
|
97
|
+
drives the periodic scan via self-rescheduling one-shot calls. Default
|
|
98
|
+
behaviour with no option supplied is unchanged.
|
|
99
|
+
|
|
100
|
+
### Fixed — `PostgresAdapter`
|
|
101
|
+
|
|
102
|
+
Three defects affecting users on published `0.5.0`. If you use `PostgresAdapter`,
|
|
103
|
+
upgrading is recommended.
|
|
104
|
+
|
|
105
|
+
- **Cursor pagination silently corrupted every page after the first.**
|
|
106
|
+
`getInstancesByCursor`'s cursor decoder split the `updatedAt_iso:id` string on
|
|
107
|
+
the _first_ colon, but an ISO-8601 timestamp (e.g.
|
|
108
|
+
`2026-06-26T09:00:00.000Z`) itself contains colons — so every decoded cursor
|
|
109
|
+
had a truncated timestamp and a corrupted id, breaking every subsequent page
|
|
110
|
+
fetch via `getInstancesByCursor`. Also hardened cursor encoding to normalize
|
|
111
|
+
through `Date.prototype.toISOString()` regardless of the row value's shape.
|
|
112
|
+
No migration needed.
|
|
113
|
+
- **`templateId` was silently dropped on every write and always read back as
|
|
114
|
+
`''`.** `PostgresAdapter` had no `template_id` column, so
|
|
115
|
+
`ApprovalInstance.templateId` was permanently lost for any instance persisted
|
|
116
|
+
through this adapter. Fixed by adding a `template_id` column and including it
|
|
117
|
+
in both the insert and read paths.
|
|
118
|
+
**Action required on upgrade:** run the adapter's migration (its
|
|
119
|
+
`CREATE TABLE`/`ALTER TABLE ... ADD COLUMN IF NOT EXISTS` statements, executed
|
|
120
|
+
automatically the next time the adapter initializes) to add the new column —
|
|
121
|
+
existing rows backfill to `''` until rewritten.
|
|
122
|
+
- **The `schema` option was interpolated into SQL without the safe-identifier
|
|
123
|
+
validation `tablePrefix` already had**, an inconsistent injection surface at
|
|
124
|
+
construction time. `schema` is now validated against the same
|
|
125
|
+
`/^[a-z][a-z0-9_]*$/` pattern as `tablePrefix`, throwing
|
|
126
|
+
`ApprovalValidationError` on an invalid value. No migration needed; only
|
|
127
|
+
affects adapter construction with an attacker-controlled `schema` value.
|
|
128
|
+
|
|
129
|
+
### Fixed — `MemoryAdapter` and notifications
|
|
130
|
+
|
|
131
|
+
- **`TemplatedNotificationAdapter`** — now logs fatal events (notification render/send failures) at the `fatal` level instead of `error`.
|
|
132
|
+
- **`MemoryAdapter`** — `getInstancesByFilter` now honors `fromDate`/`toDate` (previously the JSON-cloned string dates made the comparison a no-op and both bounds silently matched nothing). `getTemplate`/`listTemplates` now return `createdAt` as a real `Date`, matching the `ApprovalTemplate` contract.
|
|
133
|
+
|
|
6
134
|
## [0.5.0] - 2026-07-23
|
|
7
135
|
|
|
8
136
|
### Added — NestJS integration
|
package/README.md
CHANGED
|
@@ -721,9 +721,27 @@ const stats = await engine.getStatistics({ documentType: 'purchase_order' });
|
|
|
721
721
|
// byStatus: { pending, approved, rejected, cancelled, expired },
|
|
722
722
|
// overdue: number, // pending past an escalation/expiry deadline
|
|
723
723
|
// approvalRate: number, // approved / (approved + rejected); 0 when none resolved
|
|
724
|
+
// byTemplate: Record<string, { total, approved, rejected, pending }>,
|
|
725
|
+
// cycleTime: CycleTimeStats,
|
|
726
|
+
// cycleTimeByTemplate: Record<string, CycleTimeStats>,
|
|
724
727
|
// }
|
|
725
728
|
```
|
|
726
729
|
|
|
730
|
+
`byTemplate` breaks the same counts down per template name (only populated for templates with at least one matching instance).
|
|
731
|
+
|
|
732
|
+
**Cycle-time analytics.** `cycleTime` (and its per-template mirror `cycleTimeByTemplate`) reports time-to-decision, in milliseconds, as `{ count, averageMs, p50Ms, p95Ms, minMs, maxMs }`:
|
|
733
|
+
|
|
734
|
+
```ts
|
|
735
|
+
console.log(`Median time-to-decision: ${(stats.cycleTime.p50Ms / 60_000).toFixed(1)} min`);
|
|
736
|
+
console.log(`p95 over ${stats.cycleTime.count} completed instances: ${stats.cycleTime.p95Ms}ms`);
|
|
737
|
+
|
|
738
|
+
for (const [templateName, timing] of Object.entries(stats.cycleTimeByTemplate)) {
|
|
739
|
+
console.log(templateName, timing.averageMs);
|
|
740
|
+
}
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
Only instances in status `approved`, `rejected`, or `cancelled` count as "completed" for this calculation — **`expired` is deliberately excluded**: its terminal timestamp reflects a scheduler deadline firing, not a decision being made, so including it would skew the distribution rather than describe it. Elapsed time per instance is `updatedAt - createdAt`. When `count` is `0` every other field is `0`, never `NaN`.
|
|
744
|
+
|
|
727
745
|
### Health check
|
|
728
746
|
|
|
729
747
|
```ts
|
|
@@ -1072,6 +1090,54 @@ const engine = new ApprovalEngine({
|
|
|
1072
1090
|
|
|
1073
1091
|
Every operation is wrapped in a span named `approval.<operation>` (e.g. `approval.approve`) carrying `approval.tenant_id`, `approval.actor_id` and `approval.instance_id` attributes. On success the span records the resulting `approval.result_status`/`approval.result_level` and status `OK`; on failure it calls `recordException`, tags `approval.error_code`, and sets status `ERROR` — then re-throws (tracing never swallows an error). There is **no hard dependency** on `@opentelemetry/api`: the middleware defaults to a no-op tracer, so you install and wire OpenTelemetry only if you want traces.
|
|
1074
1092
|
|
|
1093
|
+
### `plugins/webhook` — signed HTTP delivery with retries
|
|
1094
|
+
|
|
1095
|
+
```ts
|
|
1096
|
+
import { WebhookNotificationAdapter } from 'hierarchical-approval/plugins/webhook';
|
|
1097
|
+
|
|
1098
|
+
const webhook = new WebhookNotificationAdapter({
|
|
1099
|
+
url: 'https://example.com/hooks/approvals',
|
|
1100
|
+
secret: process.env.WEBHOOK_SECRET, // opt-in signing; omit to send unsigned requests
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
const engine = new ApprovalEngine({ adapter, notificationAdapter: webhook });
|
|
1104
|
+
```
|
|
1105
|
+
|
|
1106
|
+
Every event is POSTed to `url` as JSON. When `secret` is configured, each request also carries an `X-Approval-Signature: t=<unix-seconds>,v1=<hex-hmac>` header (Stripe-style), where `v1` is the HMAC-SHA256 digest of the signing string `` `${timestamp}.${body}` ``. Folding the timestamp into the signed payload lets a receiver reject stale/replayed requests. A receiver verifies it like this:
|
|
1107
|
+
|
|
1108
|
+
```ts
|
|
1109
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
1110
|
+
|
|
1111
|
+
function verifyApprovalSignature(rawBody: string, header: string, secret: string): boolean {
|
|
1112
|
+
const [tPart, vPart] = header.split(',');
|
|
1113
|
+
const timestamp = tPart!.slice(2); // strip "t="
|
|
1114
|
+
const signature = vPart!.slice(3); // strip "v1="
|
|
1115
|
+
const expected = createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex');
|
|
1116
|
+
const sigBuf = Buffer.from(signature, 'hex');
|
|
1117
|
+
const expBuf = Buffer.from(expected, 'hex');
|
|
1118
|
+
return sigBuf.length === expBuf.length && timingSafeEqual(sigBuf, expBuf);
|
|
1119
|
+
}
|
|
1120
|
+
```
|
|
1121
|
+
|
|
1122
|
+
`5xx` responses, `408`, and `429`, plus network/timeout errors, are retried with exponential backoff and full jitter up to `maxAttempts` (default `3`); a `429` honors a `Retry-After` header (seconds or an HTTP date) in place of the computed backoff. Any other `4xx` (e.g. `400`, `401`, `404`) is treated as permanent and fails on the first attempt. `notify()` — the method the engine calls — never throws, per the `INotificationAdapter` contract: on exhausting `maxAttempts` it logs and drops the notification, which makes the adapter used alone _at-most-once_.
|
|
1123
|
+
|
|
1124
|
+
For at-least-once delivery that survives a process restart, compose the adapter's throwing `deliver()` method as the `transport` of `OutboxNotificationAdapter` instead of wiring `notify()` directly:
|
|
1125
|
+
|
|
1126
|
+
```ts
|
|
1127
|
+
import { OutboxNotificationAdapter } from 'hierarchical-approval/plugins/notify';
|
|
1128
|
+
import { WebhookNotificationAdapter } from 'hierarchical-approval/plugins/webhook';
|
|
1129
|
+
|
|
1130
|
+
const webhook = new WebhookNotificationAdapter({
|
|
1131
|
+
url: 'https://example.com/hooks/approvals',
|
|
1132
|
+
secret,
|
|
1133
|
+
});
|
|
1134
|
+
const durable = new OutboxNotificationAdapter({ transport: webhook.deliver.bind(webhook) });
|
|
1135
|
+
|
|
1136
|
+
const engine = new ApprovalEngine({ adapter, notificationAdapter: durable });
|
|
1137
|
+
```
|
|
1138
|
+
|
|
1139
|
+
`deliver` and `OutboxNotificationAdapter`'s `NotificationTransport` type are structurally compatible, so this composition needs no adapter shim. This adapter takes **no new dependency**: its `HttpClient` port is a plain `fetch`-shaped function, and the global `fetch` (Node.js 18+) satisfies it directly — pass a custom `httpClient` in the options only if you need different behavior (a proxy, request mocking, a different runtime's fetch).
|
|
1140
|
+
|
|
1075
1141
|
---
|
|
1076
1142
|
|
|
1077
1143
|
## NestJS integration
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from './IStorageAdapter-
|
|
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
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
3
|
import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-DVVmXU6a.cjs';
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
-
import { L as Logger } from './Logger-
|
|
5
|
+
import { L as Logger } from './Logger-BplhlU7l.cjs';
|
|
6
6
|
import { C as Clock } from './Clock-3FnOczFJ.cjs';
|
|
7
7
|
import { I as IOperationMiddleware, a as ApprovalError } from './IOperationMiddleware-CXgXmGUF.cjs';
|
|
8
8
|
import { I as IAuditAdapter } from './IAuditAdapter-B_DhuPsU.cjs';
|
|
9
9
|
import { I as IMetricsAdapter } from './IMetricsAdapter-D9PUz4tM.cjs';
|
|
10
|
+
import { I as ISchedulerAdapter } from './ISchedulerAdapter-DKv_QjVN.cjs';
|
|
10
11
|
import { I as IAuthorizationPolicy } from './IAuthorizationPolicy-B6JzRNUk.cjs';
|
|
11
12
|
|
|
12
13
|
declare const SubmitOptionsSchema: z.ZodObject<{
|
|
@@ -107,7 +108,8 @@ interface WeekendCalendarOptions {
|
|
|
107
108
|
*/
|
|
108
109
|
declare function weekendCalendar(options?: WeekendCalendarOptions): BusinessCalendar;
|
|
109
110
|
|
|
110
|
-
type
|
|
111
|
+
type IdGeneratorPrefix = 'inst' | 'tpl';
|
|
112
|
+
type IdGeneratorFn = (prefix: IdGeneratorPrefix) => string;
|
|
111
113
|
declare const defaultIdGenerator: IdGeneratorFn;
|
|
112
114
|
|
|
113
115
|
interface OrgProvider {
|
|
@@ -129,18 +131,6 @@ type ApproverResolverFn = (config: Record<string, unknown>, ctx: {
|
|
|
129
131
|
|
|
130
132
|
type ConditionOperatorFn = (actual: unknown, expected: unknown) => boolean;
|
|
131
133
|
|
|
132
|
-
interface ISchedulerAdapter {
|
|
133
|
-
/**
|
|
134
|
-
* Schedule a one-shot callback to run at the given date.
|
|
135
|
-
* Returns an opaque handle that can be passed to cancel().
|
|
136
|
-
*/
|
|
137
|
-
scheduleAt(id: string, runAt: Date, callback: () => Promise<void>): Promise<string>;
|
|
138
|
-
/** Cancel a previously scheduled callback by its handle. */
|
|
139
|
-
cancel(handle: string): Promise<void>;
|
|
140
|
-
/** Gracefully shut down the scheduler and release resources. */
|
|
141
|
-
shutdown(): Promise<void>;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
134
|
interface ValidationResult {
|
|
145
135
|
valid: boolean;
|
|
146
136
|
errors: Array<{
|
|
@@ -192,6 +182,49 @@ interface ApprovalStatistics {
|
|
|
192
182
|
rejected: number;
|
|
193
183
|
pending: number;
|
|
194
184
|
}>;
|
|
185
|
+
/**
|
|
186
|
+
* Time-to-decision ("cycle time") analytics, in milliseconds, for
|
|
187
|
+
* **completed** instances matching the filter — status `'approved'`,
|
|
188
|
+
* `'rejected'`, or `'cancelled'`. `'expired'` instances are excluded: their
|
|
189
|
+
* terminal timestamp reflects a scheduler deadline firing, not a decision
|
|
190
|
+
* being made, so they would skew the distribution rather than describe it.
|
|
191
|
+
*
|
|
192
|
+
* Elapsed time per instance is `updatedAt - createdAt`: `createdAt` is the
|
|
193
|
+
* submission instant, and `updatedAt` is set at the moment the instance
|
|
194
|
+
* transitions to its terminal status (see {@link ApprovalEngine.approve},
|
|
195
|
+
* {@link ApprovalEngine.reject}, {@link ApprovalEngine.cancel}). See
|
|
196
|
+
* {@link CycleTimeStats} for the zeroed shape returned when there are no
|
|
197
|
+
* completed instances.
|
|
198
|
+
*/
|
|
199
|
+
cycleTime: CycleTimeStats;
|
|
200
|
+
/**
|
|
201
|
+
* The same {@link cycleTime} analytics broken down per template name.
|
|
202
|
+
* Mirrors the {@link byTemplate} population rule: a template only appears
|
|
203
|
+
* here when at least one of its instances is completed (`count > 0`).
|
|
204
|
+
*/
|
|
205
|
+
cycleTimeByTemplate: Record<string, CycleTimeStats>;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Time-to-decision ("cycle time") statistics for a set of completed approval
|
|
209
|
+
* instances. All duration fields are in **milliseconds**.
|
|
210
|
+
*
|
|
211
|
+
* When {@link count} is `0` (no completed instances matched), every other
|
|
212
|
+
* field is `0` — never `NaN` — mirroring {@link computeTimingStats}'s
|
|
213
|
+
* empty-input behavior, which this type's values are derived from.
|
|
214
|
+
*/
|
|
215
|
+
interface CycleTimeStats {
|
|
216
|
+
/** Number of completed instances included in this computation. */
|
|
217
|
+
count: number;
|
|
218
|
+
/** Arithmetic mean time-to-decision. `0` when {@link count} is `0`. */
|
|
219
|
+
averageMs: number;
|
|
220
|
+
/** 50th percentile (median) time-to-decision, via nearest-rank. `0` when {@link count} is `0`. */
|
|
221
|
+
p50Ms: number;
|
|
222
|
+
/** 95th percentile time-to-decision, via nearest-rank. `0` when {@link count} is `0`. */
|
|
223
|
+
p95Ms: number;
|
|
224
|
+
/** Smallest observed time-to-decision. `0` when {@link count} is `0`. */
|
|
225
|
+
minMs: number;
|
|
226
|
+
/** Largest observed time-to-decision. `0` when {@link count} is `0`. */
|
|
227
|
+
maxMs: number;
|
|
195
228
|
}
|
|
196
229
|
interface HealthResult {
|
|
197
230
|
status: 'healthy' | 'degraded' | 'unhealthy';
|
|
@@ -225,7 +258,7 @@ interface ApprovalEngineOptions {
|
|
|
225
258
|
*/
|
|
226
259
|
calendar?: BusinessCalendar;
|
|
227
260
|
/** Custom ID generator for instances and templates. Defaults to timestamp+random. */
|
|
228
|
-
generateId?:
|
|
261
|
+
generateId?: (prefix: IdGeneratorPrefix) => string;
|
|
229
262
|
/** Custom optimistic locking retry policy. */
|
|
230
263
|
retryPolicy?: RetryPolicy;
|
|
231
264
|
/** Custom idempotency key derivation function. Default: SHA-256 of tenant+documentType+documentId+templateName. */
|
|
@@ -236,7 +269,20 @@ interface ApprovalEngineOptions {
|
|
|
236
269
|
auditAdapter?: IAuditAdapter;
|
|
237
270
|
/** Metrics adapter for Prometheus / Datadog / OpenTelemetry. */
|
|
238
271
|
metricsAdapter?: IMetricsAdapter;
|
|
239
|
-
/**
|
|
272
|
+
/**
|
|
273
|
+
* Custom scheduler adapter (BullMQ, Temporal, cron) that drives the recurring
|
|
274
|
+
* escalation/expiry/SLA-breach/delegation-revert scan.
|
|
275
|
+
*
|
|
276
|
+
* When provided, the built-in `setInterval` poll (see
|
|
277
|
+
* {@link EscalationScheduler.start}) is never started. Instead,
|
|
278
|
+
* {@link ISchedulerAdapter.scheduleAt} schedules each scan, and its callback
|
|
279
|
+
* reschedules the next one itself once the scan completes — the adapter
|
|
280
|
+
* changes *how* the periodic scan is triggered, not *what* it scans; every
|
|
281
|
+
* tick still runs the exact same overdue-instance query the built-in poller
|
|
282
|
+
* runs. {@link ISchedulerAdapter.cancel} and {@link ISchedulerAdapter.shutdown}
|
|
283
|
+
* are invoked during {@link ApprovalEngine.shutdown}. Omitting this option
|
|
284
|
+
* preserves the built-in `setInterval` polling behavior unchanged.
|
|
285
|
+
*/
|
|
240
286
|
schedulerAdapter?: ISchedulerAdapter;
|
|
241
287
|
/** Authorization policy called before every mutating operation. */
|
|
242
288
|
authorizationPolicy?: IAuthorizationPolicy;
|
|
@@ -249,6 +295,7 @@ declare class ApprovalEngine {
|
|
|
249
295
|
private readonly registry;
|
|
250
296
|
private readonly resolver;
|
|
251
297
|
private readonly escalation;
|
|
298
|
+
private readonly escalationPollIntervalMs;
|
|
252
299
|
private readonly tenantId;
|
|
253
300
|
private readonly logger;
|
|
254
301
|
private readonly clock;
|
|
@@ -257,6 +304,10 @@ declare class ApprovalEngine {
|
|
|
257
304
|
private readonly maxBulkItems;
|
|
258
305
|
private readonly retryPolicy;
|
|
259
306
|
private readonly idempotencyKeyFn;
|
|
307
|
+
/** Handle for the currently-scheduled escalation tick when {@link ApprovalEngineOptions.schedulerAdapter} is set. */
|
|
308
|
+
private schedulerAdapterHandle;
|
|
309
|
+
/** Set by {@link shutdown}; stops the self-rescheduling loop from scheduling another tick. */
|
|
310
|
+
private schedulerStopped;
|
|
260
311
|
constructor(opts: ApprovalEngineOptions);
|
|
261
312
|
on<K extends ApprovalEventName>(event: K, listener: (payload: ApprovalEventMap[K]) => void): this;
|
|
262
313
|
off<K extends ApprovalEventName>(event: K, listener: (payload: ApprovalEventMap[K]) => void): this;
|
|
@@ -313,6 +364,37 @@ declare class ApprovalEngine {
|
|
|
313
364
|
*/
|
|
314
365
|
getStatistics(filter?: Omit<InstanceFilter, 'status'>): Promise<ApprovalStatistics>;
|
|
315
366
|
shutdown(): Promise<void>;
|
|
367
|
+
/**
|
|
368
|
+
* Schedules the next escalation scan via {@link ApprovalEngineOptions.schedulerAdapter}.
|
|
369
|
+
*
|
|
370
|
+
* Called once from the constructor to start the loop, then re-invoked by the
|
|
371
|
+
* scheduled callback itself after each scan completes — a self-rescheduling
|
|
372
|
+
* chain of one-shot {@link ISchedulerAdapter.scheduleAt} calls standing in for
|
|
373
|
+
* the `setInterval` that {@link EscalationScheduler.start} would otherwise
|
|
374
|
+
* use. Each invocation runs the exact same {@link EscalationScheduler.tick}
|
|
375
|
+
* scan the built-in poller runs; only the timer mechanism differs.
|
|
376
|
+
*
|
|
377
|
+
* A no-op once {@link shutdown} has set {@link schedulerStopped} — this is
|
|
378
|
+
* what stops the chain from rescheduling itself forever after teardown.
|
|
379
|
+
*/
|
|
380
|
+
private scheduleNextEscalationTick;
|
|
381
|
+
/**
|
|
382
|
+
* Compute {@link CycleTimeStats} for every "completed" instance (see
|
|
383
|
+
* {@link CYCLE_TIME_STATUSES}) matching `filter`. Adapter-agnostic: fetches
|
|
384
|
+
* full instances (not just counts) via {@link fetchAllByFilter} so the
|
|
385
|
+
* actual `createdAt`/`updatedAt` timestamps are available, then reuses the
|
|
386
|
+
* shared {@link computeTimingStats} quantile routine from the metrics
|
|
387
|
+
* plugin rather than a second implementation.
|
|
388
|
+
*/
|
|
389
|
+
private computeCycleTimeStats;
|
|
390
|
+
/**
|
|
391
|
+
* Page through every instance matching `filter` via the adapter's
|
|
392
|
+
* `getInstancesByFilter`, accumulating pages until the adapter reports no
|
|
393
|
+
* more results. Needed because adapters may impose a default page size
|
|
394
|
+
* (e.g. `PostgresAdapter` defaults to 50) when no explicit `limit` is given,
|
|
395
|
+
* so a single unbounded call cannot be relied on to return everything.
|
|
396
|
+
*/
|
|
397
|
+
private fetchAllByFilter;
|
|
316
398
|
private escalateInternal;
|
|
317
399
|
private expireInstance;
|
|
318
400
|
private markSlaBreached;
|
|
@@ -333,4 +415,4 @@ declare class ApprovalEngine {
|
|
|
333
415
|
private runExternalAudit;
|
|
334
416
|
}
|
|
335
417
|
|
|
336
|
-
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
|
|
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 };
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from './IStorageAdapter-
|
|
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
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
3
|
import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-BdfVjYa8.js';
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
-
import { L as Logger } from './Logger-
|
|
5
|
+
import { L as Logger } from './Logger-BplhlU7l.js';
|
|
6
6
|
import { C as Clock } from './Clock-3FnOczFJ.js';
|
|
7
7
|
import { I as IOperationMiddleware, a as ApprovalError } from './IOperationMiddleware-KGAwT9f-.js';
|
|
8
8
|
import { I as IAuditAdapter } from './IAuditAdapter-B3vvU09m.js';
|
|
9
9
|
import { I as IMetricsAdapter } from './IMetricsAdapter-D9PUz4tM.js';
|
|
10
|
+
import { I as ISchedulerAdapter } from './ISchedulerAdapter-DKv_QjVN.js';
|
|
10
11
|
import { I as IAuthorizationPolicy } from './IAuthorizationPolicy-CZESF3CJ.js';
|
|
11
12
|
|
|
12
13
|
declare const SubmitOptionsSchema: z.ZodObject<{
|
|
@@ -107,7 +108,8 @@ interface WeekendCalendarOptions {
|
|
|
107
108
|
*/
|
|
108
109
|
declare function weekendCalendar(options?: WeekendCalendarOptions): BusinessCalendar;
|
|
109
110
|
|
|
110
|
-
type
|
|
111
|
+
type IdGeneratorPrefix = 'inst' | 'tpl';
|
|
112
|
+
type IdGeneratorFn = (prefix: IdGeneratorPrefix) => string;
|
|
111
113
|
declare const defaultIdGenerator: IdGeneratorFn;
|
|
112
114
|
|
|
113
115
|
interface OrgProvider {
|
|
@@ -129,18 +131,6 @@ type ApproverResolverFn = (config: Record<string, unknown>, ctx: {
|
|
|
129
131
|
|
|
130
132
|
type ConditionOperatorFn = (actual: unknown, expected: unknown) => boolean;
|
|
131
133
|
|
|
132
|
-
interface ISchedulerAdapter {
|
|
133
|
-
/**
|
|
134
|
-
* Schedule a one-shot callback to run at the given date.
|
|
135
|
-
* Returns an opaque handle that can be passed to cancel().
|
|
136
|
-
*/
|
|
137
|
-
scheduleAt(id: string, runAt: Date, callback: () => Promise<void>): Promise<string>;
|
|
138
|
-
/** Cancel a previously scheduled callback by its handle. */
|
|
139
|
-
cancel(handle: string): Promise<void>;
|
|
140
|
-
/** Gracefully shut down the scheduler and release resources. */
|
|
141
|
-
shutdown(): Promise<void>;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
134
|
interface ValidationResult {
|
|
145
135
|
valid: boolean;
|
|
146
136
|
errors: Array<{
|
|
@@ -192,6 +182,49 @@ interface ApprovalStatistics {
|
|
|
192
182
|
rejected: number;
|
|
193
183
|
pending: number;
|
|
194
184
|
}>;
|
|
185
|
+
/**
|
|
186
|
+
* Time-to-decision ("cycle time") analytics, in milliseconds, for
|
|
187
|
+
* **completed** instances matching the filter — status `'approved'`,
|
|
188
|
+
* `'rejected'`, or `'cancelled'`. `'expired'` instances are excluded: their
|
|
189
|
+
* terminal timestamp reflects a scheduler deadline firing, not a decision
|
|
190
|
+
* being made, so they would skew the distribution rather than describe it.
|
|
191
|
+
*
|
|
192
|
+
* Elapsed time per instance is `updatedAt - createdAt`: `createdAt` is the
|
|
193
|
+
* submission instant, and `updatedAt` is set at the moment the instance
|
|
194
|
+
* transitions to its terminal status (see {@link ApprovalEngine.approve},
|
|
195
|
+
* {@link ApprovalEngine.reject}, {@link ApprovalEngine.cancel}). See
|
|
196
|
+
* {@link CycleTimeStats} for the zeroed shape returned when there are no
|
|
197
|
+
* completed instances.
|
|
198
|
+
*/
|
|
199
|
+
cycleTime: CycleTimeStats;
|
|
200
|
+
/**
|
|
201
|
+
* The same {@link cycleTime} analytics broken down per template name.
|
|
202
|
+
* Mirrors the {@link byTemplate} population rule: a template only appears
|
|
203
|
+
* here when at least one of its instances is completed (`count > 0`).
|
|
204
|
+
*/
|
|
205
|
+
cycleTimeByTemplate: Record<string, CycleTimeStats>;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Time-to-decision ("cycle time") statistics for a set of completed approval
|
|
209
|
+
* instances. All duration fields are in **milliseconds**.
|
|
210
|
+
*
|
|
211
|
+
* When {@link count} is `0` (no completed instances matched), every other
|
|
212
|
+
* field is `0` — never `NaN` — mirroring {@link computeTimingStats}'s
|
|
213
|
+
* empty-input behavior, which this type's values are derived from.
|
|
214
|
+
*/
|
|
215
|
+
interface CycleTimeStats {
|
|
216
|
+
/** Number of completed instances included in this computation. */
|
|
217
|
+
count: number;
|
|
218
|
+
/** Arithmetic mean time-to-decision. `0` when {@link count} is `0`. */
|
|
219
|
+
averageMs: number;
|
|
220
|
+
/** 50th percentile (median) time-to-decision, via nearest-rank. `0` when {@link count} is `0`. */
|
|
221
|
+
p50Ms: number;
|
|
222
|
+
/** 95th percentile time-to-decision, via nearest-rank. `0` when {@link count} is `0`. */
|
|
223
|
+
p95Ms: number;
|
|
224
|
+
/** Smallest observed time-to-decision. `0` when {@link count} is `0`. */
|
|
225
|
+
minMs: number;
|
|
226
|
+
/** Largest observed time-to-decision. `0` when {@link count} is `0`. */
|
|
227
|
+
maxMs: number;
|
|
195
228
|
}
|
|
196
229
|
interface HealthResult {
|
|
197
230
|
status: 'healthy' | 'degraded' | 'unhealthy';
|
|
@@ -225,7 +258,7 @@ interface ApprovalEngineOptions {
|
|
|
225
258
|
*/
|
|
226
259
|
calendar?: BusinessCalendar;
|
|
227
260
|
/** Custom ID generator for instances and templates. Defaults to timestamp+random. */
|
|
228
|
-
generateId?:
|
|
261
|
+
generateId?: (prefix: IdGeneratorPrefix) => string;
|
|
229
262
|
/** Custom optimistic locking retry policy. */
|
|
230
263
|
retryPolicy?: RetryPolicy;
|
|
231
264
|
/** Custom idempotency key derivation function. Default: SHA-256 of tenant+documentType+documentId+templateName. */
|
|
@@ -236,7 +269,20 @@ interface ApprovalEngineOptions {
|
|
|
236
269
|
auditAdapter?: IAuditAdapter;
|
|
237
270
|
/** Metrics adapter for Prometheus / Datadog / OpenTelemetry. */
|
|
238
271
|
metricsAdapter?: IMetricsAdapter;
|
|
239
|
-
/**
|
|
272
|
+
/**
|
|
273
|
+
* Custom scheduler adapter (BullMQ, Temporal, cron) that drives the recurring
|
|
274
|
+
* escalation/expiry/SLA-breach/delegation-revert scan.
|
|
275
|
+
*
|
|
276
|
+
* When provided, the built-in `setInterval` poll (see
|
|
277
|
+
* {@link EscalationScheduler.start}) is never started. Instead,
|
|
278
|
+
* {@link ISchedulerAdapter.scheduleAt} schedules each scan, and its callback
|
|
279
|
+
* reschedules the next one itself once the scan completes — the adapter
|
|
280
|
+
* changes *how* the periodic scan is triggered, not *what* it scans; every
|
|
281
|
+
* tick still runs the exact same overdue-instance query the built-in poller
|
|
282
|
+
* runs. {@link ISchedulerAdapter.cancel} and {@link ISchedulerAdapter.shutdown}
|
|
283
|
+
* are invoked during {@link ApprovalEngine.shutdown}. Omitting this option
|
|
284
|
+
* preserves the built-in `setInterval` polling behavior unchanged.
|
|
285
|
+
*/
|
|
240
286
|
schedulerAdapter?: ISchedulerAdapter;
|
|
241
287
|
/** Authorization policy called before every mutating operation. */
|
|
242
288
|
authorizationPolicy?: IAuthorizationPolicy;
|
|
@@ -249,6 +295,7 @@ declare class ApprovalEngine {
|
|
|
249
295
|
private readonly registry;
|
|
250
296
|
private readonly resolver;
|
|
251
297
|
private readonly escalation;
|
|
298
|
+
private readonly escalationPollIntervalMs;
|
|
252
299
|
private readonly tenantId;
|
|
253
300
|
private readonly logger;
|
|
254
301
|
private readonly clock;
|
|
@@ -257,6 +304,10 @@ declare class ApprovalEngine {
|
|
|
257
304
|
private readonly maxBulkItems;
|
|
258
305
|
private readonly retryPolicy;
|
|
259
306
|
private readonly idempotencyKeyFn;
|
|
307
|
+
/** Handle for the currently-scheduled escalation tick when {@link ApprovalEngineOptions.schedulerAdapter} is set. */
|
|
308
|
+
private schedulerAdapterHandle;
|
|
309
|
+
/** Set by {@link shutdown}; stops the self-rescheduling loop from scheduling another tick. */
|
|
310
|
+
private schedulerStopped;
|
|
260
311
|
constructor(opts: ApprovalEngineOptions);
|
|
261
312
|
on<K extends ApprovalEventName>(event: K, listener: (payload: ApprovalEventMap[K]) => void): this;
|
|
262
313
|
off<K extends ApprovalEventName>(event: K, listener: (payload: ApprovalEventMap[K]) => void): this;
|
|
@@ -313,6 +364,37 @@ declare class ApprovalEngine {
|
|
|
313
364
|
*/
|
|
314
365
|
getStatistics(filter?: Omit<InstanceFilter, 'status'>): Promise<ApprovalStatistics>;
|
|
315
366
|
shutdown(): Promise<void>;
|
|
367
|
+
/**
|
|
368
|
+
* Schedules the next escalation scan via {@link ApprovalEngineOptions.schedulerAdapter}.
|
|
369
|
+
*
|
|
370
|
+
* Called once from the constructor to start the loop, then re-invoked by the
|
|
371
|
+
* scheduled callback itself after each scan completes — a self-rescheduling
|
|
372
|
+
* chain of one-shot {@link ISchedulerAdapter.scheduleAt} calls standing in for
|
|
373
|
+
* the `setInterval` that {@link EscalationScheduler.start} would otherwise
|
|
374
|
+
* use. Each invocation runs the exact same {@link EscalationScheduler.tick}
|
|
375
|
+
* scan the built-in poller runs; only the timer mechanism differs.
|
|
376
|
+
*
|
|
377
|
+
* A no-op once {@link shutdown} has set {@link schedulerStopped} — this is
|
|
378
|
+
* what stops the chain from rescheduling itself forever after teardown.
|
|
379
|
+
*/
|
|
380
|
+
private scheduleNextEscalationTick;
|
|
381
|
+
/**
|
|
382
|
+
* Compute {@link CycleTimeStats} for every "completed" instance (see
|
|
383
|
+
* {@link CYCLE_TIME_STATUSES}) matching `filter`. Adapter-agnostic: fetches
|
|
384
|
+
* full instances (not just counts) via {@link fetchAllByFilter} so the
|
|
385
|
+
* actual `createdAt`/`updatedAt` timestamps are available, then reuses the
|
|
386
|
+
* shared {@link computeTimingStats} quantile routine from the metrics
|
|
387
|
+
* plugin rather than a second implementation.
|
|
388
|
+
*/
|
|
389
|
+
private computeCycleTimeStats;
|
|
390
|
+
/**
|
|
391
|
+
* Page through every instance matching `filter` via the adapter's
|
|
392
|
+
* `getInstancesByFilter`, accumulating pages until the adapter reports no
|
|
393
|
+
* more results. Needed because adapters may impose a default page size
|
|
394
|
+
* (e.g. `PostgresAdapter` defaults to 50) when no explicit `limit` is given,
|
|
395
|
+
* so a single unbounded call cannot be relied on to return everything.
|
|
396
|
+
*/
|
|
397
|
+
private fetchAllByFilter;
|
|
316
398
|
private escalateInternal;
|
|
317
399
|
private expireInstance;
|
|
318
400
|
private markSlaBreached;
|
|
@@ -333,4 +415,4 @@ declare class ApprovalEngine {
|
|
|
333
415
|
private runExternalAudit;
|
|
334
416
|
}
|
|
335
417
|
|
|
336
|
-
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
|
|
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 };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
interface ISchedulerAdapter {
|
|
2
|
+
/**
|
|
3
|
+
* Schedule a one-shot callback to run at the given date.
|
|
4
|
+
* Returns an opaque handle that can be passed to cancel().
|
|
5
|
+
*/
|
|
6
|
+
scheduleAt(id: string, runAt: Date, callback: () => Promise<void>): Promise<string>;
|
|
7
|
+
/** Cancel a previously scheduled callback by its handle. */
|
|
8
|
+
cancel(handle: string): Promise<void>;
|
|
9
|
+
/** Gracefully shut down the scheduler and release resources. */
|
|
10
|
+
shutdown(): Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type { ISchedulerAdapter as I };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
interface ISchedulerAdapter {
|
|
2
|
+
/**
|
|
3
|
+
* Schedule a one-shot callback to run at the given date.
|
|
4
|
+
* Returns an opaque handle that can be passed to cancel().
|
|
5
|
+
*/
|
|
6
|
+
scheduleAt(id: string, runAt: Date, callback: () => Promise<void>): Promise<string>;
|
|
7
|
+
/** Cancel a previously scheduled callback by its handle. */
|
|
8
|
+
cancel(handle: string): Promise<void>;
|
|
9
|
+
/** Gracefully shut down the scheduler and release resources. */
|
|
10
|
+
shutdown(): Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type { ISchedulerAdapter as I };
|
|
@@ -40,7 +40,7 @@ interface IStorageAdapter {
|
|
|
40
40
|
getInstancesByFilter(tenantId: string, filter: InstanceFilter, opts?: PaginationOpts): Promise<PaginatedResult<ApprovalInstance>>;
|
|
41
41
|
/** Optional cursor-based pagination — more efficient than offset at scale. */
|
|
42
42
|
getInstancesByCursor?(tenantId: string, filter: InstanceFilter, opts: CursorPaginationOpts): Promise<CursorPaginatedResult<ApprovalInstance>>;
|
|
43
|
-
getOverdueInstances(tenantId: string, asOf: Date): Promise<ApprovalInstance[]>;
|
|
43
|
+
getOverdueInstances(tenantId: string, asOf: Date, filter?: InstanceFilter): Promise<ApprovalInstance[]>;
|
|
44
44
|
getIdempotentInstance(tenantId: string, idempotencyKey: string): Promise<ApprovalInstance | null>;
|
|
45
45
|
appendAuditEntry(tenantId: string, instanceId: string, entry: AuditEntry): Promise<void>;
|
|
46
46
|
}
|
|
@@ -40,7 +40,7 @@ interface IStorageAdapter {
|
|
|
40
40
|
getInstancesByFilter(tenantId: string, filter: InstanceFilter, opts?: PaginationOpts): Promise<PaginatedResult<ApprovalInstance>>;
|
|
41
41
|
/** Optional cursor-based pagination — more efficient than offset at scale. */
|
|
42
42
|
getInstancesByCursor?(tenantId: string, filter: InstanceFilter, opts: CursorPaginationOpts): Promise<CursorPaginatedResult<ApprovalInstance>>;
|
|
43
|
-
getOverdueInstances(tenantId: string, asOf: Date): Promise<ApprovalInstance[]>;
|
|
43
|
+
getOverdueInstances(tenantId: string, asOf: Date, filter?: InstanceFilter): Promise<ApprovalInstance[]>;
|
|
44
44
|
getIdempotentInstance(tenantId: string, idempotencyKey: string): Promise<ApprovalInstance | null>;
|
|
45
45
|
appendAuditEntry(tenantId: string, instanceId: string, entry: AuditEntry): Promise<void>;
|
|
46
46
|
}
|
|
@@ -2,6 +2,7 @@ interface Logger {
|
|
|
2
2
|
info(msg: string, context?: Record<string, unknown>): void;
|
|
3
3
|
warn(msg: string, context?: Record<string, unknown>): void;
|
|
4
4
|
error(msg: string, err?: unknown, context?: Record<string, unknown>): void;
|
|
5
|
+
fatal(msg: string, err?: unknown, context?: Record<string, unknown>): void;
|
|
5
6
|
debug(msg: string, context?: Record<string, unknown>): void;
|
|
6
7
|
}
|
|
7
8
|
declare const noopLogger: Logger;
|