hierarchical-approval 1.5.0 → 1.7.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 +81 -0
- package/README.md +34 -0
- package/dist/{ApprovalEngine-SI-IRUMY.d.cts → ApprovalEngine-D16llfJv.d.cts} +46 -8
- package/dist/{ApprovalEngine-CyZJDbjP.d.ts → ApprovalEngine-gQdFy2CO.d.ts} +46 -8
- package/dist/{IAuditAdapter-wfOcN3Zf.d.ts → IAuditAdapter-DDV4Rf9F.d.ts} +1 -1
- package/dist/{IAuditAdapter-Cr0dLoVP.d.cts → IAuditAdapter-UJNdJanq.d.cts} +1 -1
- package/dist/{IAuthorizationPolicy-BHybAsq5.d.cts → IAuthorizationPolicy-CFJ-gXKl.d.ts} +2 -2
- package/dist/{IAuthorizationPolicy-CdvoZsGC.d.ts → IAuthorizationPolicy-CN6LAaKg.d.cts} +2 -2
- package/dist/{IMetricsAdapter-JZVK3QUq.d.cts → IMetricsAdapter-DWq8IFaf.d.cts} +1 -1
- package/dist/{IMetricsAdapter-JZVK3QUq.d.ts → IMetricsAdapter-DWq8IFaf.d.ts} +1 -1
- package/dist/{INotificationAdapter-Cn-LNPtD.d.cts → INotificationAdapter-Dv_MuYwS.d.ts} +12 -2
- package/dist/{INotificationAdapter-Cyo0cvXU.d.ts → INotificationAdapter-Dy2d0JKy.d.cts} +12 -2
- package/dist/{IOperationMiddleware-DrJy9UtL.d.cts → IOperationMiddleware-DGG-guxK.d.cts} +1 -1
- package/dist/{IOperationMiddleware-rx7XAPrW.d.ts → IOperationMiddleware-DWYXVuJD.d.ts} +1 -1
- package/dist/{IStorageAdapter-Dz8jrIAK.d.cts → IStorageAdapter-CRSfnH8U.d.cts} +1 -1
- package/dist/{IStorageAdapter-CXRyeUOK.d.ts → IStorageAdapter-DMa5VNNz.d.ts} +1 -1
- package/dist/adapters/MemoryAdapter.d.cts +2 -2
- package/dist/adapters/MemoryAdapter.d.ts +2 -2
- package/dist/adapters/PostgresAdapter.cjs +43 -9
- 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 +43 -9
- package/dist/adapters/PostgresAdapter.js.map +1 -1
- package/dist/index.cjs +187 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -9
- package/dist/index.d.ts +9 -9
- package/dist/index.js +187 -0
- package/dist/index.js.map +1 -1
- package/dist/{instance-SA0BU-Gb.d.cts → instance-BE0uJmg3.d.cts} +27 -2
- package/dist/{instance-SA0BU-Gb.d.ts → instance-BE0uJmg3.d.ts} +27 -2
- package/dist/nestjs.cjs +187 -0
- package/dist/nestjs.cjs.map +1 -1
- package/dist/nestjs.d.cts +8 -8
- package/dist/nestjs.d.ts +8 -8
- package/dist/nestjs.js +187 -0
- 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/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.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 +187 -0
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +8 -8
- package/dist/testing.d.ts +8 -8
- package/dist/testing.js +187 -0
- package/dist/testing.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,87 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
7
7
|
|
|
8
8
|
_Nothing yet._
|
|
9
9
|
|
|
10
|
+
## [1.7.0] - 2026-09-04
|
|
11
|
+
|
|
12
|
+
### Added — attachment references
|
|
13
|
+
|
|
14
|
+
- **`addAttachment()` / `removeAttachment()` attach supporting evidence to an
|
|
15
|
+
approval** — a quote PDF, a signed contract, a screenshot of a system of
|
|
16
|
+
record. Approvers had nowhere to put the document their decision rested on, so
|
|
17
|
+
it lived in an email thread the audit trail never saw.
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
await engine.addAttachment(id, {
|
|
21
|
+
actorId: 'buyer-1',
|
|
22
|
+
name: 'quote.pdf',
|
|
23
|
+
uri: 's3://procurement/quotes/q-1.pdf',
|
|
24
|
+
contentType: 'application/pdf',
|
|
25
|
+
sizeBytes: 48_120,
|
|
26
|
+
});
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
- **References only, never bytes.** Approval documents belong in the object
|
|
30
|
+
store or DMS the organisation already runs, which handles retention, virus
|
|
31
|
+
scanning and access control far better than an approval table could; copying
|
|
32
|
+
them here would make the audit database the largest and least governed copy of
|
|
33
|
+
them. Removing detaches the reference and never deletes from the underlying
|
|
34
|
+
store.
|
|
35
|
+
|
|
36
|
+
- **The audit trail keeps what was removed** — name and URI are recorded on the
|
|
37
|
+
`attachment_removed` entry, so the record still shows an approver saw evidence
|
|
38
|
+
that is no longer listed. Dropping that would let the trail imply a decision
|
|
39
|
+
was made on less than it was.
|
|
40
|
+
|
|
41
|
+
- Persisted across all four PostgreSQL sites (schema, `migrate()`,
|
|
42
|
+
insert, update, and read-back), with `addedAt` revived as a `Date`. An
|
|
43
|
+
instance with no attachments reads back as `undefined` rather than `[]`,
|
|
44
|
+
keeping the round-trip shape identical to `MemoryAdapter`.
|
|
45
|
+
|
|
46
|
+
New exports: `Attachment`, `AttachmentEvent`, `AddAttachmentOptions`,
|
|
47
|
+
`RemoveAttachmentOptions`. `ApprovalInstance` gains `attachments`.
|
|
48
|
+
|
|
49
|
+
## [1.6.0] - 2026-09-04
|
|
50
|
+
|
|
51
|
+
### Fixed — PostgresAdapter silently dropped most instance updates
|
|
52
|
+
|
|
53
|
+
**Anyone running the PostgreSQL adapter should upgrade and run `migrate()`.**
|
|
54
|
+
`MemoryAdapter` stores whole objects and was always correct, so this reproduced
|
|
55
|
+
only against a real database — which is exactly where it mattered.
|
|
56
|
+
|
|
57
|
+
`updateInstance` wrote just six columns (`status`, `current_level`, `version`,
|
|
58
|
+
`levels`, `sla_breached_at`, `updated_at`). Every other field an operation
|
|
59
|
+
mutates was computed, logged, emitted as an event, and then thrown away on
|
|
60
|
+
write:
|
|
61
|
+
|
|
62
|
+
- **`updateData()` (0.9.0) did not change the stored document.** The engine
|
|
63
|
+
recomputed the chain and emitted `approval:data_updated`, but `data` was never
|
|
64
|
+
written, so the next read returned the old values — and any later
|
|
65
|
+
re-evaluation ran against them.
|
|
66
|
+
- **`requestInfo()` (1.5.0) lost the hold entirely.** `info_request` had no
|
|
67
|
+
column at all, so a held instance came back unheld: `provideInfo()` then threw
|
|
68
|
+
"No clarification request is open", and the scheduler escalated and expired an
|
|
69
|
+
approval that was supposed to be paused.
|
|
70
|
+
- **`provideInfo()`'s deadline give-back never landed** — `expires_at` and
|
|
71
|
+
`sla_deadline_at` were not written, so the time an instance spent on hold was
|
|
72
|
+
silently forfeited.
|
|
73
|
+
- `metadata`, `deadline_action` and `template_snapshot` were likewise never
|
|
74
|
+
updated after insert.
|
|
75
|
+
|
|
76
|
+
`updateInstance` now writes every mutable column, `saveInstance` and
|
|
77
|
+
`rowToInstance` carry `info_request` (reviving `askedAt` as a `Date`), and
|
|
78
|
+
`migrate()` adds the column to existing deployments via
|
|
79
|
+
`ADD COLUMN IF NOT EXISTS`. Optimistic concurrency is unchanged — the update
|
|
80
|
+
still guards on `version`.
|
|
81
|
+
|
|
82
|
+
Regression tests assert the generated SQL and parameters for each field, so a
|
|
83
|
+
future column cannot be added to the type and forgotten in the writer.
|
|
84
|
+
|
|
85
|
+
### Note on the roadmap
|
|
86
|
+
|
|
87
|
+
Attachment references were the planned 1.6.0. They are deferred: shipping a new
|
|
88
|
+
feature ahead of a data-loss fix in an already-published release would have been
|
|
89
|
+
the wrong order.
|
|
90
|
+
|
|
10
91
|
## [1.5.0] - 2026-09-04
|
|
11
92
|
|
|
12
93
|
### Added — request for information
|
package/README.md
CHANGED
|
@@ -399,6 +399,40 @@ engine.registerConditionOperator('between', (actual, expected) => {
|
|
|
399
399
|
});
|
|
400
400
|
```
|
|
401
401
|
|
|
402
|
+
### Attachments
|
|
403
|
+
|
|
404
|
+
Attach supporting evidence — a quote PDF, a signed contract, a screenshot of a
|
|
405
|
+
system of record:
|
|
406
|
+
|
|
407
|
+
```ts
|
|
408
|
+
const withQuote = await engine.addAttachment(instance.id, {
|
|
409
|
+
actorId: 'buyer-1',
|
|
410
|
+
name: 'quote.pdf',
|
|
411
|
+
uri: 's3://procurement/quotes/q-1.pdf', // a reference, not bytes
|
|
412
|
+
contentType: 'application/pdf',
|
|
413
|
+
sizeBytes: 48_120,
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
await engine.removeAttachment(instance.id, {
|
|
417
|
+
actorId: 'buyer-1',
|
|
418
|
+
attachmentId: withQuote.attachments[0].id,
|
|
419
|
+
reason: 'superseded by revised quote',
|
|
420
|
+
});
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
**The engine stores references, never bytes.** Approval documents belong in the
|
|
424
|
+
object store or DMS you already run, which handles retention, virus scanning and
|
|
425
|
+
access control far better than an approval table could — copying them here would
|
|
426
|
+
make the audit database the largest and least governed copy of them.
|
|
427
|
+
|
|
428
|
+
Removing detaches the reference; it never deletes from the underlying store,
|
|
429
|
+
which is the DMS's call. The audit entry keeps the name and URI of what was
|
|
430
|
+
removed, so the trail still shows an approver saw evidence that is no longer
|
|
431
|
+
listed.
|
|
432
|
+
|
|
433
|
+
Emits `approval:attachment_added` / `approval:attachment_removed`. Allowed on
|
|
434
|
+
any non-terminal instance, subject to your authorization policy.
|
|
435
|
+
|
|
402
436
|
### Asking the submitter a question
|
|
403
437
|
|
|
404
438
|
Approvers routinely need one fact before they can decide. `requestInfo()` puts
|
|
@@ -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-CRSfnH8U.cjs';
|
|
2
|
+
import { l as ConditionExpression, p as ResolverFn, g as ApprovalTemplateConfig, A as ApprovalTemplate, k as AuditContext, a as ApprovalInstance, e as ApprovalMode, b as AuditEntry } from './instance-BE0uJmg3.cjs';
|
|
3
|
+
import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-Dy2d0JKy.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-
|
|
9
|
-
import { I as IMetricsAdapter } from './IMetricsAdapter-
|
|
7
|
+
import { I as IOperationMiddleware, a as ApprovalError } from './IOperationMiddleware-DGG-guxK.cjs';
|
|
8
|
+
import { I as IAuditAdapter } from './IAuditAdapter-UJNdJanq.cjs';
|
|
9
|
+
import { I as IMetricsAdapter } from './IMetricsAdapter-DWq8IFaf.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-CN6LAaKg.cjs';
|
|
12
12
|
|
|
13
13
|
declare const SubmitOptionsSchema: z.ZodObject<{
|
|
14
14
|
templateName: z.ZodString;
|
|
@@ -78,6 +78,18 @@ declare const ProvideInfoOptionsSchema: z.ZodObject<{
|
|
|
78
78
|
respondedBy: z.ZodString;
|
|
79
79
|
response: z.ZodString;
|
|
80
80
|
}, z.core.$strip>;
|
|
81
|
+
declare const AddAttachmentOptionsSchema: z.ZodObject<{
|
|
82
|
+
actorId: z.ZodString;
|
|
83
|
+
name: z.ZodString;
|
|
84
|
+
uri: z.ZodString;
|
|
85
|
+
contentType: z.ZodOptional<z.ZodString>;
|
|
86
|
+
sizeBytes: z.ZodOptional<z.ZodNumber>;
|
|
87
|
+
}, z.core.$strip>;
|
|
88
|
+
declare const RemoveAttachmentOptionsSchema: z.ZodObject<{
|
|
89
|
+
actorId: z.ZodString;
|
|
90
|
+
attachmentId: z.ZodString;
|
|
91
|
+
reason: z.ZodOptional<z.ZodString>;
|
|
92
|
+
}, z.core.$strip>;
|
|
81
93
|
declare const UpdateDataOptionsSchema: z.ZodObject<{
|
|
82
94
|
updatedBy: z.ZodString;
|
|
83
95
|
data: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
@@ -101,6 +113,8 @@ type OverrideOptions = z.infer<typeof OverrideOptionsSchema>;
|
|
|
101
113
|
type UpdateDataOptions = z.infer<typeof UpdateDataOptionsSchema>;
|
|
102
114
|
type RequestInfoOptions = z.infer<typeof RequestInfoOptionsSchema>;
|
|
103
115
|
type ProvideInfoOptions = z.infer<typeof ProvideInfoOptionsSchema>;
|
|
116
|
+
type AddAttachmentOptions = z.infer<typeof AddAttachmentOptionsSchema>;
|
|
117
|
+
type RemoveAttachmentOptions = z.infer<typeof RemoveAttachmentOptionsSchema>;
|
|
104
118
|
|
|
105
119
|
/**
|
|
106
120
|
* Computes deadline dates from a number of days. The default engine behaviour
|
|
@@ -132,7 +146,7 @@ interface WeekendCalendarOptions {
|
|
|
132
146
|
*/
|
|
133
147
|
declare function weekendCalendar(options?: WeekendCalendarOptions): BusinessCalendar;
|
|
134
148
|
|
|
135
|
-
type IdGeneratorPrefix = 'inst' | 'tpl';
|
|
149
|
+
type IdGeneratorPrefix = 'inst' | 'tpl' | 'att';
|
|
136
150
|
type IdGeneratorFn = (prefix: IdGeneratorPrefix) => string;
|
|
137
151
|
declare const defaultIdGenerator: IdGeneratorFn;
|
|
138
152
|
|
|
@@ -490,6 +504,30 @@ declare class ApprovalEngine {
|
|
|
490
504
|
* by being held.
|
|
491
505
|
*/
|
|
492
506
|
private extendDeadlinesBy;
|
|
507
|
+
/**
|
|
508
|
+
* Attach supporting evidence to an approval — a quote PDF, a signed contract,
|
|
509
|
+
* a screenshot of a system of record.
|
|
510
|
+
*
|
|
511
|
+
* Stores a **reference**, never bytes. Approval documents belong in the object
|
|
512
|
+
* store or DMS the organisation already runs, which handles retention, virus
|
|
513
|
+
* scanning and access control far better than an approval table could;
|
|
514
|
+
* copying them here would make the audit database the largest and least
|
|
515
|
+
* governed copy of them.
|
|
516
|
+
*
|
|
517
|
+
* Allowed on any non-terminal instance, by anyone the authorization policy
|
|
518
|
+
* permits: the submitter adding a missing quote and an approver adding the
|
|
519
|
+
* note that justifies their decision are both ordinary.
|
|
520
|
+
*/
|
|
521
|
+
addAttachment(instanceId: string, raw: AddAttachmentOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
|
|
522
|
+
/**
|
|
523
|
+
* Detach a reference from an approval.
|
|
524
|
+
*
|
|
525
|
+
* The audit entry keeps the name and URI of what was removed, so the record
|
|
526
|
+
* still shows an approver saw evidence that is no longer listed — dropping
|
|
527
|
+
* that would let the trail imply a decision was made on less than it was.
|
|
528
|
+
* Nothing is deleted from the underlying store; that is the DMS's call.
|
|
529
|
+
*/
|
|
530
|
+
removeAttachment(instanceId: string, raw: RemoveAttachmentOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
|
|
493
531
|
/** Add a comment to an instance without approving or rejecting. */
|
|
494
532
|
addComment(instanceId: string, raw: AddCommentOptions, auditCtx?: AuditContext): Promise<void>;
|
|
495
533
|
/** Resubmit a rejected instance, creating a new linked instance from level 1. */
|
|
@@ -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-DMa5VNNz.js';
|
|
2
|
+
import { l as ConditionExpression, p as ResolverFn, g as ApprovalTemplateConfig, A as ApprovalTemplate, k as AuditContext, a as ApprovalInstance, e as ApprovalMode, b as AuditEntry } from './instance-BE0uJmg3.js';
|
|
3
|
+
import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-Dv_MuYwS.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-
|
|
9
|
-
import { I as IMetricsAdapter } from './IMetricsAdapter-
|
|
7
|
+
import { I as IOperationMiddleware, a as ApprovalError } from './IOperationMiddleware-DWYXVuJD.js';
|
|
8
|
+
import { I as IAuditAdapter } from './IAuditAdapter-DDV4Rf9F.js';
|
|
9
|
+
import { I as IMetricsAdapter } from './IMetricsAdapter-DWq8IFaf.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-CFJ-gXKl.js';
|
|
12
12
|
|
|
13
13
|
declare const SubmitOptionsSchema: z.ZodObject<{
|
|
14
14
|
templateName: z.ZodString;
|
|
@@ -78,6 +78,18 @@ declare const ProvideInfoOptionsSchema: z.ZodObject<{
|
|
|
78
78
|
respondedBy: z.ZodString;
|
|
79
79
|
response: z.ZodString;
|
|
80
80
|
}, z.core.$strip>;
|
|
81
|
+
declare const AddAttachmentOptionsSchema: z.ZodObject<{
|
|
82
|
+
actorId: z.ZodString;
|
|
83
|
+
name: z.ZodString;
|
|
84
|
+
uri: z.ZodString;
|
|
85
|
+
contentType: z.ZodOptional<z.ZodString>;
|
|
86
|
+
sizeBytes: z.ZodOptional<z.ZodNumber>;
|
|
87
|
+
}, z.core.$strip>;
|
|
88
|
+
declare const RemoveAttachmentOptionsSchema: z.ZodObject<{
|
|
89
|
+
actorId: z.ZodString;
|
|
90
|
+
attachmentId: z.ZodString;
|
|
91
|
+
reason: z.ZodOptional<z.ZodString>;
|
|
92
|
+
}, z.core.$strip>;
|
|
81
93
|
declare const UpdateDataOptionsSchema: z.ZodObject<{
|
|
82
94
|
updatedBy: z.ZodString;
|
|
83
95
|
data: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
@@ -101,6 +113,8 @@ type OverrideOptions = z.infer<typeof OverrideOptionsSchema>;
|
|
|
101
113
|
type UpdateDataOptions = z.infer<typeof UpdateDataOptionsSchema>;
|
|
102
114
|
type RequestInfoOptions = z.infer<typeof RequestInfoOptionsSchema>;
|
|
103
115
|
type ProvideInfoOptions = z.infer<typeof ProvideInfoOptionsSchema>;
|
|
116
|
+
type AddAttachmentOptions = z.infer<typeof AddAttachmentOptionsSchema>;
|
|
117
|
+
type RemoveAttachmentOptions = z.infer<typeof RemoveAttachmentOptionsSchema>;
|
|
104
118
|
|
|
105
119
|
/**
|
|
106
120
|
* Computes deadline dates from a number of days. The default engine behaviour
|
|
@@ -132,7 +146,7 @@ interface WeekendCalendarOptions {
|
|
|
132
146
|
*/
|
|
133
147
|
declare function weekendCalendar(options?: WeekendCalendarOptions): BusinessCalendar;
|
|
134
148
|
|
|
135
|
-
type IdGeneratorPrefix = 'inst' | 'tpl';
|
|
149
|
+
type IdGeneratorPrefix = 'inst' | 'tpl' | 'att';
|
|
136
150
|
type IdGeneratorFn = (prefix: IdGeneratorPrefix) => string;
|
|
137
151
|
declare const defaultIdGenerator: IdGeneratorFn;
|
|
138
152
|
|
|
@@ -490,6 +504,30 @@ declare class ApprovalEngine {
|
|
|
490
504
|
* by being held.
|
|
491
505
|
*/
|
|
492
506
|
private extendDeadlinesBy;
|
|
507
|
+
/**
|
|
508
|
+
* Attach supporting evidence to an approval — a quote PDF, a signed contract,
|
|
509
|
+
* a screenshot of a system of record.
|
|
510
|
+
*
|
|
511
|
+
* Stores a **reference**, never bytes. Approval documents belong in the object
|
|
512
|
+
* store or DMS the organisation already runs, which handles retention, virus
|
|
513
|
+
* scanning and access control far better than an approval table could;
|
|
514
|
+
* copying them here would make the audit database the largest and least
|
|
515
|
+
* governed copy of them.
|
|
516
|
+
*
|
|
517
|
+
* Allowed on any non-terminal instance, by anyone the authorization policy
|
|
518
|
+
* permits: the submitter adding a missing quote and an approver adding the
|
|
519
|
+
* note that justifies their decision are both ordinary.
|
|
520
|
+
*/
|
|
521
|
+
addAttachment(instanceId: string, raw: AddAttachmentOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
|
|
522
|
+
/**
|
|
523
|
+
* Detach a reference from an approval.
|
|
524
|
+
*
|
|
525
|
+
* The audit entry keeps the name and URI of what was removed, so the record
|
|
526
|
+
* still shows an approver saw evidence that is no longer listed — dropping
|
|
527
|
+
* that would let the trail imply a decision was made on less than it was.
|
|
528
|
+
* Nothing is deleted from the underlying store; that is the DMS's call.
|
|
529
|
+
*/
|
|
530
|
+
removeAttachment(instanceId: string, raw: RemoveAttachmentOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
|
|
493
531
|
/** Add a comment to an instance without approving or rejecting. */
|
|
494
532
|
addComment(instanceId: string, raw: AddCommentOptions, auditCtx?: AuditContext): Promise<void>;
|
|
495
533
|
/** Resubmit a rejected instance, creating a new linked instance from level 1. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-
|
|
1
|
+
import { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-BE0uJmg3.js';
|
|
2
2
|
|
|
3
3
|
interface AuthorizationContext {
|
|
4
|
-
operation: 'submit' | 'approve' | 'reject' | 'delegate' | 'reassign' | 'cancel' | 'escalate' | 'override' | 'resubmit' | 'addComment' | 'updateData' | 'requestInfo' | 'provideInfo';
|
|
4
|
+
operation: 'submit' | 'approve' | 'reject' | 'delegate' | 'reassign' | 'cancel' | 'escalate' | 'override' | 'resubmit' | 'addComment' | 'updateData' | 'requestInfo' | 'provideInfo' | 'addAttachment' | 'removeAttachment';
|
|
5
5
|
actorId: string;
|
|
6
6
|
instance: Readonly<ApprovalInstance>;
|
|
7
7
|
level?: Readonly<ApprovalLevelInstance>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-
|
|
1
|
+
import { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-BE0uJmg3.cjs';
|
|
2
2
|
|
|
3
3
|
interface AuthorizationContext {
|
|
4
|
-
operation: 'submit' | 'approve' | 'reject' | 'delegate' | 'reassign' | 'cancel' | 'escalate' | 'override' | 'resubmit' | 'addComment' | 'updateData' | 'requestInfo' | 'provideInfo';
|
|
4
|
+
operation: 'submit' | 'approve' | 'reject' | 'delegate' | 'reassign' | 'cancel' | 'escalate' | 'override' | 'resubmit' | 'addComment' | 'updateData' | 'requestInfo' | 'provideInfo' | 'addAttachment' | 'removeAttachment';
|
|
5
5
|
actorId: string;
|
|
6
6
|
instance: Readonly<ApprovalInstance>;
|
|
7
7
|
level?: Readonly<ApprovalLevelInstance>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
type MetricName = 'approval.submitted' | 'approval.approved' | 'approval.rejected' | 'approval.cancelled' | 'approval.expired' | 'approval.sla_breached' | 'approval.escalated' | 'approval.reassigned' | 'approval.overridden' | 'approval.data_updated' | 'approval.reminded' | 'approval.info_requested' | 'approval.info_provided' | 'approval.conflict_retry' | 'approval.operation_duration_ms';
|
|
1
|
+
type MetricName = 'approval.submitted' | 'approval.approved' | 'approval.rejected' | 'approval.cancelled' | 'approval.expired' | 'approval.sla_breached' | 'approval.escalated' | 'approval.reassigned' | 'approval.overridden' | 'approval.data_updated' | 'approval.reminded' | 'approval.info_requested' | 'approval.info_provided' | 'approval.attachment_added' | 'approval.attachment_removed' | 'approval.conflict_retry' | 'approval.operation_duration_ms';
|
|
2
2
|
interface IMetricsAdapter {
|
|
3
3
|
/** Increment a counter. Synchronous — never awaited. */
|
|
4
4
|
increment(metric: MetricName, labels?: Record<string, string>): void;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
type MetricName = 'approval.submitted' | 'approval.approved' | 'approval.rejected' | 'approval.cancelled' | 'approval.expired' | 'approval.sla_breached' | 'approval.escalated' | 'approval.reassigned' | 'approval.overridden' | 'approval.data_updated' | 'approval.reminded' | 'approval.info_requested' | 'approval.info_provided' | 'approval.conflict_retry' | 'approval.operation_duration_ms';
|
|
1
|
+
type MetricName = 'approval.submitted' | 'approval.approved' | 'approval.rejected' | 'approval.cancelled' | 'approval.expired' | 'approval.sla_breached' | 'approval.escalated' | 'approval.reassigned' | 'approval.overridden' | 'approval.data_updated' | 'approval.reminded' | 'approval.info_requested' | 'approval.info_provided' | 'approval.attachment_added' | 'approval.attachment_removed' | 'approval.conflict_retry' | 'approval.operation_duration_ms';
|
|
2
2
|
interface IMetricsAdapter {
|
|
3
3
|
/** Increment a counter. Synchronous — never awaited. */
|
|
4
4
|
increment(metric: MetricName, labels?: Record<string, string>): void;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as ApprovalInstance, b as AuditEntry } from './instance-
|
|
1
|
+
import { a as ApprovalInstance, b as AuditEntry } from './instance-BE0uJmg3.js';
|
|
2
2
|
|
|
3
3
|
interface ApprovalEvent {
|
|
4
4
|
instanceId: string;
|
|
@@ -104,6 +104,14 @@ interface InfoProvidedEvent extends ApprovalEvent {
|
|
|
104
104
|
/** The approvers waiting again now that the question is answered. */
|
|
105
105
|
recipients: string[];
|
|
106
106
|
}
|
|
107
|
+
/** Emitted when supporting evidence is attached to or removed from an approval. */
|
|
108
|
+
interface AttachmentEvent extends ApprovalEvent {
|
|
109
|
+
actorId: string;
|
|
110
|
+
attachmentId: string;
|
|
111
|
+
name: string;
|
|
112
|
+
uri: string;
|
|
113
|
+
level?: number;
|
|
114
|
+
}
|
|
107
115
|
interface ApprovalEventMap {
|
|
108
116
|
'approval:submitted': SubmittedEvent;
|
|
109
117
|
'approval:approved': ApprovedEvent;
|
|
@@ -122,6 +130,8 @@ interface ApprovalEventMap {
|
|
|
122
130
|
'approval:reminder': ReminderEvent;
|
|
123
131
|
'approval:info_requested': InfoRequestedEvent;
|
|
124
132
|
'approval:info_provided': InfoProvidedEvent;
|
|
133
|
+
'approval:attachment_added': AttachmentEvent;
|
|
134
|
+
'approval:attachment_removed': AttachmentEvent;
|
|
125
135
|
}
|
|
126
136
|
type ApprovalEventName = keyof ApprovalEventMap;
|
|
127
137
|
interface HistoryEntry extends AuditEntry {
|
|
@@ -145,4 +155,4 @@ interface INotificationAdapter {
|
|
|
145
155
|
notify(event: NotificationEvent): Promise<void>;
|
|
146
156
|
}
|
|
147
157
|
|
|
148
|
-
export type { ApprovalEvent as A, CancelledEvent as C, DataUpdatedEvent as D, EscalatedEvent as E, HistoryEntry as H, INotificationAdapter as I, LevelAdvancedEvent as L, NotificationEvent as N, OverriddenEvent as O, ReassignedEvent as R, SlaBreachedEvent as S, ApprovalEventMap as a, ApprovalEventName as b, ApprovedEvent as c,
|
|
158
|
+
export type { ApprovalEvent as A, CancelledEvent as C, DataUpdatedEvent as D, EscalatedEvent as E, HistoryEntry as H, INotificationAdapter as I, LevelAdvancedEvent as L, NotificationEvent as N, OverriddenEvent as O, ReassignedEvent as R, SlaBreachedEvent as S, ApprovalEventMap as a, ApprovalEventName as b, ApprovedEvent as c, AttachmentEvent as d, DelegatedEvent as e, ExpiredEvent as f, InfoProvidedEvent as g, InfoRequestedEvent as h, RejectedEvent as i, ReminderEvent as j, ResubmittedEvent as k, SubmittedEvent as l };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as ApprovalInstance, b as AuditEntry } from './instance-
|
|
1
|
+
import { a as ApprovalInstance, b as AuditEntry } from './instance-BE0uJmg3.cjs';
|
|
2
2
|
|
|
3
3
|
interface ApprovalEvent {
|
|
4
4
|
instanceId: string;
|
|
@@ -104,6 +104,14 @@ interface InfoProvidedEvent extends ApprovalEvent {
|
|
|
104
104
|
/** The approvers waiting again now that the question is answered. */
|
|
105
105
|
recipients: string[];
|
|
106
106
|
}
|
|
107
|
+
/** Emitted when supporting evidence is attached to or removed from an approval. */
|
|
108
|
+
interface AttachmentEvent extends ApprovalEvent {
|
|
109
|
+
actorId: string;
|
|
110
|
+
attachmentId: string;
|
|
111
|
+
name: string;
|
|
112
|
+
uri: string;
|
|
113
|
+
level?: number;
|
|
114
|
+
}
|
|
107
115
|
interface ApprovalEventMap {
|
|
108
116
|
'approval:submitted': SubmittedEvent;
|
|
109
117
|
'approval:approved': ApprovedEvent;
|
|
@@ -122,6 +130,8 @@ interface ApprovalEventMap {
|
|
|
122
130
|
'approval:reminder': ReminderEvent;
|
|
123
131
|
'approval:info_requested': InfoRequestedEvent;
|
|
124
132
|
'approval:info_provided': InfoProvidedEvent;
|
|
133
|
+
'approval:attachment_added': AttachmentEvent;
|
|
134
|
+
'approval:attachment_removed': AttachmentEvent;
|
|
125
135
|
}
|
|
126
136
|
type ApprovalEventName = keyof ApprovalEventMap;
|
|
127
137
|
interface HistoryEntry extends AuditEntry {
|
|
@@ -145,4 +155,4 @@ interface INotificationAdapter {
|
|
|
145
155
|
notify(event: NotificationEvent): Promise<void>;
|
|
146
156
|
}
|
|
147
157
|
|
|
148
|
-
export type { ApprovalEvent as A, CancelledEvent as C, DataUpdatedEvent as D, EscalatedEvent as E, HistoryEntry as H, INotificationAdapter as I, LevelAdvancedEvent as L, NotificationEvent as N, OverriddenEvent as O, ReassignedEvent as R, SlaBreachedEvent as S, ApprovalEventMap as a, ApprovalEventName as b, ApprovedEvent as c,
|
|
158
|
+
export type { ApprovalEvent as A, CancelledEvent as C, DataUpdatedEvent as D, EscalatedEvent as E, HistoryEntry as H, INotificationAdapter as I, LevelAdvancedEvent as L, NotificationEvent as N, OverriddenEvent as O, ReassignedEvent as R, SlaBreachedEvent as S, ApprovalEventMap as a, ApprovalEventName as b, ApprovedEvent as c, AttachmentEvent as d, DelegatedEvent as e, ExpiredEvent as f, InfoProvidedEvent as g, InfoRequestedEvent as h, RejectedEvent as i, ReminderEvent as j, ResubmittedEvent as k, SubmittedEvent as l };
|
|
@@ -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-BE0uJmg3.cjs';
|
|
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-BE0uJmg3.js';
|
|
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-CRSfnH8U.cjs';
|
|
2
|
+
import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-BE0uJmg3.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-DMa5VNNz.js';
|
|
2
|
+
import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-BE0uJmg3.js';
|
|
3
3
|
|
|
4
4
|
declare class MemoryAdapter implements IStorageAdapter {
|
|
5
5
|
private templates;
|
|
@@ -115,6 +115,8 @@ var PostgresAdapter = class {
|
|
|
115
115
|
sla_deadline_at TIMESTAMPTZ,
|
|
116
116
|
sla_breached_at TIMESTAMPTZ,
|
|
117
117
|
template_snapshot JSONB,
|
|
118
|
+
info_request JSONB,
|
|
119
|
+
attachments JSONB NOT NULL DEFAULT '[]',
|
|
118
120
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
119
121
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
120
122
|
PRIMARY KEY (tenant_id, id),
|
|
@@ -157,6 +159,8 @@ var PostgresAdapter = class {
|
|
|
157
159
|
ALTER TABLE IF EXISTS ${this.p}_instances ADD COLUMN IF NOT EXISTS sla_deadline_at TIMESTAMPTZ;
|
|
158
160
|
ALTER TABLE IF EXISTS ${this.p}_instances ADD COLUMN IF NOT EXISTS sla_breached_at TIMESTAMPTZ;
|
|
159
161
|
ALTER TABLE IF EXISTS ${this.p}_instances ADD COLUMN IF NOT EXISTS template_snapshot JSONB;
|
|
162
|
+
ALTER TABLE IF EXISTS ${this.p}_instances ADD COLUMN IF NOT EXISTS info_request JSONB;
|
|
163
|
+
ALTER TABLE IF EXISTS ${this.p}_instances ADD COLUMN IF NOT EXISTS attachments JSONB NOT NULL DEFAULT '[]';
|
|
160
164
|
`);
|
|
161
165
|
}
|
|
162
166
|
/** Enable PostgreSQL Row Level Security for the instances and audit_log tables. */
|
|
@@ -216,9 +220,9 @@ var PostgresAdapter = class {
|
|
|
216
220
|
status, current_level, version, idempotency_key,
|
|
217
221
|
data, metadata, levels,
|
|
218
222
|
parent_instance_id, expires_at, deadline_action,
|
|
219
|
-
sla_deadline_at, sla_breached_at, template_snapshot,
|
|
223
|
+
sla_deadline_at, sla_breached_at, template_snapshot, info_request, attachments,
|
|
220
224
|
created_at, updated_at)
|
|
221
|
-
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22)
|
|
225
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24)
|
|
222
226
|
ON CONFLICT (tenant_id, id) DO NOTHING`,
|
|
223
227
|
[
|
|
224
228
|
instance.id,
|
|
@@ -241,6 +245,8 @@ var PostgresAdapter = class {
|
|
|
241
245
|
instance.slaDeadlineAt?.toISOString() ?? null,
|
|
242
246
|
instance.slaBreachedAt?.toISOString() ?? null,
|
|
243
247
|
instance.templateSnapshot ? JSON.stringify(instance.templateSnapshot) : null,
|
|
248
|
+
instance.infoRequest ? JSON.stringify(instance.infoRequest) : null,
|
|
249
|
+
JSON.stringify(instance.attachments ?? []),
|
|
244
250
|
instance.createdAt.toISOString(),
|
|
245
251
|
instance.updatedAt.toISOString()
|
|
246
252
|
]
|
|
@@ -249,13 +255,25 @@ var PostgresAdapter = class {
|
|
|
249
255
|
async updateInstance(instance, expectedVersion) {
|
|
250
256
|
const pool = await this.getPool();
|
|
251
257
|
const result = await pool.query(
|
|
258
|
+
// Every column an engine operation can mutate must be written here.
|
|
259
|
+
// Listing only a subset silently dropped writes: updateData() changed
|
|
260
|
+
// `data`, requestInfo() set `info_request`, and provideInfo() shifted the
|
|
261
|
+
// deadline columns — none of which reached the database.
|
|
252
262
|
`UPDATE ${this.p}_instances SET
|
|
253
|
-
status
|
|
254
|
-
current_level
|
|
255
|
-
version
|
|
256
|
-
levels
|
|
257
|
-
sla_breached_at
|
|
258
|
-
updated_at
|
|
263
|
+
status = $4,
|
|
264
|
+
current_level = $5,
|
|
265
|
+
version = $6 + 1,
|
|
266
|
+
levels = $7,
|
|
267
|
+
sla_breached_at = $8,
|
|
268
|
+
updated_at = $9,
|
|
269
|
+
data = $10,
|
|
270
|
+
metadata = $11,
|
|
271
|
+
expires_at = $12,
|
|
272
|
+
deadline_action = $13,
|
|
273
|
+
sla_deadline_at = $14,
|
|
274
|
+
template_snapshot = $15,
|
|
275
|
+
info_request = $16,
|
|
276
|
+
attachments = $17
|
|
259
277
|
WHERE tenant_id = $1 AND id = $2 AND version = $3
|
|
260
278
|
RETURNING id`,
|
|
261
279
|
[
|
|
@@ -267,7 +285,15 @@ var PostgresAdapter = class {
|
|
|
267
285
|
expectedVersion,
|
|
268
286
|
JSON.stringify(instance.levels),
|
|
269
287
|
instance.slaBreachedAt?.toISOString() ?? null,
|
|
270
|
-
instance.updatedAt.toISOString()
|
|
288
|
+
instance.updatedAt.toISOString(),
|
|
289
|
+
JSON.stringify(instance.data ?? {}),
|
|
290
|
+
JSON.stringify(instance.metadata ?? {}),
|
|
291
|
+
instance.expiresAt?.toISOString() ?? null,
|
|
292
|
+
instance.deadlineAction ?? null,
|
|
293
|
+
instance.slaDeadlineAt?.toISOString() ?? null,
|
|
294
|
+
instance.templateSnapshot ? JSON.stringify(instance.templateSnapshot) : null,
|
|
295
|
+
instance.infoRequest ? JSON.stringify(instance.infoRequest) : null,
|
|
296
|
+
JSON.stringify(instance.attachments ?? [])
|
|
271
297
|
]
|
|
272
298
|
);
|
|
273
299
|
if (result.rowCount === 0) throw new ApprovalConflictError(instance.id);
|
|
@@ -516,6 +542,14 @@ var PostgresAdapter = class {
|
|
|
516
542
|
slaDeadlineAt: row["sla_deadline_at"] ? new Date(row["sla_deadline_at"]) : void 0,
|
|
517
543
|
slaBreachedAt: row["sla_breached_at"] ? new Date(row["sla_breached_at"]) : void 0,
|
|
518
544
|
templateSnapshot: row["template_snapshot"] ?? void 0,
|
|
545
|
+
// Absent attachments map to undefined, not [], so a Postgres round trip
|
|
546
|
+
// returns the same shape MemoryAdapter does for an instance that never
|
|
547
|
+
// had any — the parity the round-trip test pins down.
|
|
548
|
+
attachments: (row["attachments"] ?? []).length ? row["attachments"].map((a) => ({ ...a, addedAt: new Date(a.addedAt) })) : void 0,
|
|
549
|
+
infoRequest: row["info_request"] ? {
|
|
550
|
+
...row["info_request"],
|
|
551
|
+
askedAt: new Date(row["info_request"].askedAt)
|
|
552
|
+
} : void 0,
|
|
519
553
|
createdAt: new Date(row["created_at"]),
|
|
520
554
|
updatedAt: new Date(row["updated_at"])
|
|
521
555
|
};
|