hierarchical-approval 1.1.0 → 1.2.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 +40 -0
- package/README.md +34 -0
- package/dist/{ApprovalEngine-qwImBe9A.d.cts → ApprovalEngine-CdtOJOsu.d.cts} +6 -6
- package/dist/{ApprovalEngine-BDnQwKi9.d.ts → ApprovalEngine-DRC-o30z.d.ts} +6 -6
- package/dist/{IAuditAdapter-CWdZxS3U.d.cts → IAuditAdapter-B2I3CjaJ.d.cts} +1 -1
- package/dist/{IAuditAdapter-99JBU3Db.d.ts → IAuditAdapter-GixC9zS1.d.ts} +1 -1
- package/dist/{IAuthorizationPolicy-U-jZrl2F.d.ts → IAuthorizationPolicy-4ge9Gnxj.d.ts} +1 -1
- package/dist/{IAuthorizationPolicy-ChZIUJhQ.d.cts → IAuthorizationPolicy-BHDZK9QL.d.cts} +1 -1
- package/dist/{INotificationAdapter-BKwhxhe2.d.cts → INotificationAdapter-BKMeGmaG.d.cts} +1 -1
- package/dist/{INotificationAdapter-CsSWiCZE.d.ts → INotificationAdapter-BmSeB7FZ.d.ts} +1 -1
- package/dist/{IOperationMiddleware-D162RiJs.d.cts → IOperationMiddleware-BLw9Oc-S.d.cts} +1 -1
- package/dist/{IOperationMiddleware-CPkGSVfr.d.ts → IOperationMiddleware-BW9t8wPn.d.ts} +1 -1
- package/dist/{IStorageAdapter-BZarNxs9.d.cts → IStorageAdapter-CKV_FZFV.d.cts} +1 -1
- package/dist/{IStorageAdapter-DrY-rhhF.d.ts → IStorageAdapter-DQJmR6Mt.d.ts} +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 +70 -6
- 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 +70 -6
- package/dist/index.js.map +1 -1
- package/dist/{instance-DuEgSIIz.d.cts → instance-BsM0DUPf.d.cts} +15 -0
- package/dist/{instance-DuEgSIIz.d.ts → instance-BsM0DUPf.d.ts} +15 -0
- package/dist/nestjs.cjs +70 -6
- 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 +70 -6
- 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 +70 -6
- 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 +70 -6
- package/dist/testing.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,46 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
7
7
|
|
|
8
8
|
_Nothing yet._
|
|
9
9
|
|
|
10
|
+
## [1.2.0] - 2026-09-04
|
|
11
|
+
|
|
12
|
+
### Added — template inheritance
|
|
13
|
+
|
|
14
|
+
- **`extends` derives one template from another.** ERP tenants run many
|
|
15
|
+
near-identical workflows — one per region, legal entity or document class —
|
|
16
|
+
that share a spine and differ in a level or two. Each had to be written out in
|
|
17
|
+
full, so a change to the shared part meant editing every copy and hoping none
|
|
18
|
+
were missed.
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
await engine.defineTemplate({
|
|
22
|
+
name: 'PO-EU',
|
|
23
|
+
extends: 'PO-base',
|
|
24
|
+
documentType: 'purchase_order',
|
|
25
|
+
removeLevels: [2],
|
|
26
|
+
levels: [{ level: 3, approvers: [{ type: 'user', userId: 'eu-fin' }] }],
|
|
27
|
+
});
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Levels are keyed by number: a child level overrides the base level *field by
|
|
31
|
+
field* (so a derived template can swap just the approvers and inherit name,
|
|
32
|
+
mode and deadlines), child-only levels are appended, and `removeLevels` drops
|
|
33
|
+
inherited ones. `conditions` are replaced wholesale when supplied — merging
|
|
34
|
+
two rule lists would produce a chain neither author intended. `escalation`,
|
|
35
|
+
`slaDeadlineDays` and `allowOverride` are inherited unless the child sets them.
|
|
36
|
+
|
|
37
|
+
- **Resolution happens once, at define time, and the flattened result is what
|
|
38
|
+
gets stored.** Resolving lazily on read would mean editing a base silently
|
|
39
|
+
reshapes every derived template — and every in-flight instance running against
|
|
40
|
+
one — which is exactly the surprise `templateSnapshot` exists to prevent
|
|
41
|
+
elsewhere in the engine. A base that itself extends something is already
|
|
42
|
+
flattened, so chains resolve naturally and cycles cannot form.
|
|
43
|
+
|
|
44
|
+
- **Validation runs on the flattened template**, not the fragment: a derived
|
|
45
|
+
config carrying no levels of its own is valid (it inherits the chain), while
|
|
46
|
+
one whose `removeLevels` strips the chain entirely is rejected at definition
|
|
47
|
+
time. `extends` and `removeLevels` are directives for the call and are not
|
|
48
|
+
persisted.
|
|
49
|
+
|
|
10
50
|
## [1.1.0] - 2026-09-04
|
|
11
51
|
|
|
12
52
|
### Added — approval reminders
|
package/README.md
CHANGED
|
@@ -399,6 +399,40 @@ engine.registerConditionOperator('between', (actual, expected) => {
|
|
|
399
399
|
});
|
|
400
400
|
```
|
|
401
401
|
|
|
402
|
+
### Template inheritance
|
|
403
|
+
|
|
404
|
+
ERP tenants run many near-identical workflows — one per region, entity or
|
|
405
|
+
document class — that share a spine and differ in a level or two. `extends`
|
|
406
|
+
derives one template from another:
|
|
407
|
+
|
|
408
|
+
```ts
|
|
409
|
+
await engine.defineTemplate({
|
|
410
|
+
name: 'PO-EU',
|
|
411
|
+
extends: 'PO-base',
|
|
412
|
+
documentType: 'purchase_order',
|
|
413
|
+
removeLevels: [2], // drop an inherited level
|
|
414
|
+
levels: [
|
|
415
|
+
{ level: 3, approvers: [{ type: 'user', userId: 'eu-fin' }] }, // override just the approvers
|
|
416
|
+
{ level: 4, name: 'EU Compliance', approvers: [...], mode: 'any' }, // add a new level
|
|
417
|
+
],
|
|
418
|
+
});
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
| Field | Merge rule |
|
|
422
|
+
|---|---|
|
|
423
|
+
| `levels` | Keyed by level number: a child level overrides the base level *field by field*, child-only levels are appended, `removeLevels` drops inherited ones |
|
|
424
|
+
| `conditions` | Replaced wholesale when the child supplies them |
|
|
425
|
+
| `escalation`, `slaDeadlineDays`, `allowOverride` | Child value if set, otherwise inherited |
|
|
426
|
+
|
|
427
|
+
**Resolution happens once, at define time, and the flattened result is stored.**
|
|
428
|
+
Editing a base later never silently reshapes a derived template or an instance
|
|
429
|
+
already running against one — the same insulation `templateSnapshot` gives
|
|
430
|
+
in-flight instances. A base that itself extends something is already flattened,
|
|
431
|
+
so chains resolve naturally and cycles cannot form.
|
|
432
|
+
|
|
433
|
+
Validation runs on the *flattened* template, so a derived config that strips its
|
|
434
|
+
whole chain is rejected at definition time.
|
|
435
|
+
|
|
402
436
|
### Reminders
|
|
403
437
|
|
|
404
438
|
Escalation reassigns work; a reminder only nudges, which is usually what an
|
|
@@ -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 { 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-
|
|
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-CKV_FZFV.cjs';
|
|
2
|
+
import { k as ConditionExpression, o as ResolverFn, g as ApprovalTemplateConfig, A as ApprovalTemplate, j as AuditContext, a as ApprovalInstance, e as ApprovalMode, b as AuditEntry } from './instance-BsM0DUPf.cjs';
|
|
3
|
+
import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-BKMeGmaG.cjs';
|
|
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-BLw9Oc-S.cjs';
|
|
8
|
+
import { I as IAuditAdapter } from './IAuditAdapter-B2I3CjaJ.cjs';
|
|
9
9
|
import { I as IMetricsAdapter } from './IMetricsAdapter-QvyaKQCc.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-BHDZK9QL.cjs';
|
|
12
12
|
|
|
13
13
|
declare const SubmitOptionsSchema: z.ZodObject<{
|
|
14
14
|
templateName: z.ZodString;
|
|
@@ -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 { 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-
|
|
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-DQJmR6Mt.js';
|
|
2
|
+
import { k as ConditionExpression, o as ResolverFn, g as ApprovalTemplateConfig, A as ApprovalTemplate, j as AuditContext, a as ApprovalInstance, e as ApprovalMode, b as AuditEntry } from './instance-BsM0DUPf.js';
|
|
3
|
+
import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-BmSeB7FZ.js';
|
|
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-BW9t8wPn.js';
|
|
8
|
+
import { I as IAuditAdapter } from './IAuditAdapter-GixC9zS1.js';
|
|
9
9
|
import { I as IMetricsAdapter } from './IMetricsAdapter-QvyaKQCc.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-4ge9Gnxj.js';
|
|
12
12
|
|
|
13
13
|
declare const SubmitOptionsSchema: z.ZodObject<{
|
|
14
14
|
templateName: z.ZodString;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-
|
|
1
|
+
import { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-BsM0DUPf.js';
|
|
2
2
|
|
|
3
3
|
interface AuthorizationContext {
|
|
4
4
|
operation: 'submit' | 'approve' | 'reject' | 'delegate' | 'reassign' | 'cancel' | 'escalate' | 'override' | 'resubmit' | 'addComment' | 'updateData';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-
|
|
1
|
+
import { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-BsM0DUPf.cjs';
|
|
2
2
|
|
|
3
3
|
interface AuthorizationContext {
|
|
4
4
|
operation: 'submit' | 'approve' | 'reject' | 'delegate' | 'reassign' | 'cancel' | 'escalate' | 'override' | 'resubmit' | 'addComment' | 'updateData';
|
|
@@ -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-BsM0DUPf.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-BsM0DUPf.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-CKV_FZFV.cjs';
|
|
2
|
+
import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-BsM0DUPf.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-DQJmR6Mt.js';
|
|
2
|
+
import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-BsM0DUPf.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-CKV_FZFV.cjs';
|
|
4
|
+
import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-BsM0DUPf.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-DQJmR6Mt.js';
|
|
4
|
+
import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-BsM0DUPf.js';
|
|
5
5
|
|
|
6
6
|
interface PostgresAdapterOptions {
|
|
7
7
|
connectionString?: string;
|
package/dist/index.cjs
CHANGED
|
@@ -224,6 +224,66 @@ var TemplateRegistry = class {
|
|
|
224
224
|
this.clock = opts?.clock ?? systemClock;
|
|
225
225
|
this.generateId = opts?.generateId ?? defaultIdGenerator;
|
|
226
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* Flatten a template that declares `extends` against its base.
|
|
229
|
+
*
|
|
230
|
+
* Resolution is done once, at define time, and the flattened result is what
|
|
231
|
+
* gets stored. Resolving lazily on read would mean editing a base silently
|
|
232
|
+
* reshapes every derived template — and every in-flight instance running
|
|
233
|
+
* against one — which is exactly the surprise `templateSnapshot` exists to
|
|
234
|
+
* prevent elsewhere in the engine.
|
|
235
|
+
*
|
|
236
|
+
* Merge rules:
|
|
237
|
+
* - **Levels** are keyed by level number. A child level with the same number
|
|
238
|
+
* overrides the base level field by field, so a derived template can change
|
|
239
|
+
* just the approvers and inherit the rest. Child-only levels are appended,
|
|
240
|
+
* and `removeLevels` drops inherited ones.
|
|
241
|
+
* - **conditions** are replaced wholesale when the child supplies them.
|
|
242
|
+
* Concatenating two rule lists would produce a chain neither author
|
|
243
|
+
* intended, and there is no meaningful way to "merge" boolean rules.
|
|
244
|
+
* - **escalation**, **slaDeadlineDays** and **allowOverride** are taken from
|
|
245
|
+
* the child when set, otherwise inherited.
|
|
246
|
+
*
|
|
247
|
+
* A base that itself extends something is already flattened in storage, so
|
|
248
|
+
* chains resolve naturally and cycles cannot form: the base must exist before
|
|
249
|
+
* the child can name it.
|
|
250
|
+
*
|
|
251
|
+
* @param config - The raw config as supplied by the caller.
|
|
252
|
+
* @returns The config with inheritance applied, or unchanged if no `extends`.
|
|
253
|
+
*/
|
|
254
|
+
async resolveInheritance(config) {
|
|
255
|
+
if (!config.extends) return config;
|
|
256
|
+
if (config.extends === config.name) {
|
|
257
|
+
throw new ApprovalValidationError(`Template "${config.name}" cannot extend itself.`);
|
|
258
|
+
}
|
|
259
|
+
const base = await this.adapter.getTemplate(this.tenantId, config.extends);
|
|
260
|
+
if (!base) {
|
|
261
|
+
throw new ApprovalTemplateNotFoundError(
|
|
262
|
+
`${config.extends} (referenced by "${config.name}" via extends)`
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
const removed = new Set(config.removeLevels ?? []);
|
|
266
|
+
const byLevel = /* @__PURE__ */ new Map();
|
|
267
|
+
for (const lvl of base.levels) {
|
|
268
|
+
if (!removed.has(lvl.level)) byLevel.set(lvl.level, lvl);
|
|
269
|
+
}
|
|
270
|
+
for (const lvl of config.levels ?? []) {
|
|
271
|
+
const inherited = byLevel.get(lvl.level);
|
|
272
|
+
byLevel.set(lvl.level, inherited ? { ...inherited, ...lvl } : lvl);
|
|
273
|
+
}
|
|
274
|
+
const levels = [...byLevel.values()].sort((a, b) => a.level - b.level);
|
|
275
|
+
const resolved = {
|
|
276
|
+
...config,
|
|
277
|
+
levels,
|
|
278
|
+
conditions: config.conditions ?? base.conditions,
|
|
279
|
+
escalation: config.escalation ?? base.escalation,
|
|
280
|
+
slaDeadlineDays: config.slaDeadlineDays ?? base.slaDeadlineDays,
|
|
281
|
+
allowOverride: config.allowOverride ?? base.allowOverride
|
|
282
|
+
};
|
|
283
|
+
delete resolved.extends;
|
|
284
|
+
delete resolved.removeLevels;
|
|
285
|
+
return resolved;
|
|
286
|
+
}
|
|
227
287
|
async define(config) {
|
|
228
288
|
const existing = await this.adapter.getTemplate(this.tenantId, config.name);
|
|
229
289
|
if (existing) {
|
|
@@ -855,9 +915,11 @@ var ApprovalEngine = class _ApprovalEngine {
|
|
|
855
915
|
/** Validate a template config without persisting. Synchronous; never throws. */
|
|
856
916
|
validateTemplate(config) {
|
|
857
917
|
const errors = [];
|
|
858
|
-
|
|
918
|
+
const hasOwnLevels = Boolean(config.levels && config.levels.length > 0);
|
|
919
|
+
if (!hasOwnLevels && config.extends === void 0) {
|
|
859
920
|
errors.push({ field: "levels", message: "Template must have at least one level." });
|
|
860
|
-
}
|
|
921
|
+
}
|
|
922
|
+
if (hasOwnLevels) {
|
|
861
923
|
const levelNums = /* @__PURE__ */ new Set();
|
|
862
924
|
config.levels.forEach((l, i) => {
|
|
863
925
|
if (levelNums.has(l.level)) {
|
|
@@ -986,25 +1048,27 @@ var ApprovalEngine = class _ApprovalEngine {
|
|
|
986
1048
|
return { valid: errors.length === 0, errors };
|
|
987
1049
|
}
|
|
988
1050
|
async defineTemplate(config) {
|
|
989
|
-
const
|
|
1051
|
+
const resolved = await this.registry.resolveInheritance(config);
|
|
1052
|
+
const validation = this.validateTemplate(resolved);
|
|
990
1053
|
if (!validation.valid) {
|
|
991
1054
|
const first = validation.errors[0];
|
|
992
1055
|
throw new ApprovalValidationError(
|
|
993
1056
|
`Invalid template configuration: ${first?.message ?? "unknown error"}`
|
|
994
1057
|
);
|
|
995
1058
|
}
|
|
996
|
-
return this.registry.define(
|
|
1059
|
+
return this.registry.define(resolved);
|
|
997
1060
|
}
|
|
998
1061
|
/** Update an existing template, incrementing its version. In-flight instances are protected by their templateSnapshot. */
|
|
999
1062
|
async updateTemplate(config) {
|
|
1000
|
-
const
|
|
1063
|
+
const resolved = await this.registry.resolveInheritance(config);
|
|
1064
|
+
const validation = this.validateTemplate(resolved);
|
|
1001
1065
|
if (!validation.valid) {
|
|
1002
1066
|
const first = validation.errors[0];
|
|
1003
1067
|
throw new ApprovalValidationError(
|
|
1004
1068
|
`Invalid template configuration: ${first?.message ?? "unknown error"}`
|
|
1005
1069
|
);
|
|
1006
1070
|
}
|
|
1007
|
-
return this.registry.update(
|
|
1071
|
+
return this.registry.update(resolved);
|
|
1008
1072
|
}
|
|
1009
1073
|
async getTemplate(name) {
|
|
1010
1074
|
return this.registry.get(name);
|