vintasend-managed-templates 1.0.0-alpha3 → 1.0.0-alpha4
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/README.md +106 -12
- package/dist/base-template-manager-backend.d.ts +36 -1
- package/dist/base-template-manager-backend.d.ts.map +1 -1
- package/dist/errors.d.ts +21 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +21 -0
- package/dist/in-memory-template-manager-backend.d.ts +21 -0
- package/dist/in-memory-template-manager-backend.d.ts.map +1 -1
- package/dist/in-memory-template-manager-backend.js +38 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/lifecycle.d.ts +59 -0
- package/dist/lifecycle.d.ts.map +1 -0
- package/dist/lifecycle.js +85 -0
- package/dist/managed-template-renderer.d.ts +92 -10
- package/dist/managed-template-renderer.d.ts.map +1 -1
- package/dist/managed-template-renderer.js +129 -12
- package/dist/managed-template-service.d.ts +38 -8
- package/dist/managed-template-service.d.ts.map +1 -1
- package/dist/managed-template-service.js +41 -9
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -95,7 +95,53 @@ await notificationService.createNotification({
|
|
|
95
95
|
});
|
|
96
96
|
```
|
|
97
97
|
|
|
98
|
-
Nothing else about creating or sending notifications changes.
|
|
98
|
+
Nothing else about creating or sending notifications changes. A send renders the key's newest
|
|
99
|
+
**active** version — see [Which version a send renders](#which-version-a-send-renders) — so a
|
|
100
|
+
template has to be activated before it goes out.
|
|
101
|
+
|
|
102
|
+
### Sending before a template is written: fallbacks
|
|
103
|
+
|
|
104
|
+
An application can register a default for each key, so it can send a notification before anyone
|
|
105
|
+
has written its template in the store. While a key has nothing published, `render` hands the
|
|
106
|
+
default to a renderer of your choosing; once a version of the key is activated, sends use it.
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
import { PugEmailTemplateRendererFactory } from 'vintasend-pug';
|
|
110
|
+
|
|
111
|
+
const renderer = new ManagedTemplateEmailRenderer<Config>(managerBackend, innerRenderer, {
|
|
112
|
+
fallback: {
|
|
113
|
+
// Optional; defaults to the inner renderer. Its `render` gets a copy of the notification
|
|
114
|
+
// with `bodyTemplate` / `subjectTemplate` replaced by the values registered below.
|
|
115
|
+
renderer: new PugEmailTemplateRendererFactory<Config>().create(),
|
|
116
|
+
templates: {
|
|
117
|
+
welcome: {
|
|
118
|
+
subjectTemplate: 'emails/welcome/subject.pug', // file paths, for a file renderer
|
|
119
|
+
bodyTemplate: 'emails/welcome/body.pug',
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
renderer.getFallbackTemplate('welcome'); // the registered default, or null — for a dashboard
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The text renderer takes the same option. The rules:
|
|
129
|
+
|
|
130
|
+
* **Only when the key itself has nothing published** — no versions at all, or only drafts and
|
|
131
|
+
retired ones. A stored template that fails to compose (it extends a base that is missing) throws
|
|
132
|
+
`ManagedTemplateCompositionReferenceError` instead: that template exists and is broken, and a
|
|
133
|
+
default must not hide it.
|
|
134
|
+
* **Only for an unpinned notification.** If a pinned version is missing, the template existed when
|
|
135
|
+
the notification was created, so the render throws.
|
|
136
|
+
* **Only for a registered key**, matched as an own property — a key such as `constructor` never
|
|
137
|
+
matches the prototype.
|
|
138
|
+
* **Only on `render`**, the send path. `renderManaged`, `renderTemplate` and the service's
|
|
139
|
+
`render` (what previews use) never fall back, and `getLatestTemplateVersion` still answers `null`
|
|
140
|
+
for a key with nothing published.
|
|
141
|
+
|
|
142
|
+
A fallback payload carries `templateSource: 'fallback'` and no `templateVersion`, so the
|
|
143
|
+
notification's `usedTemplateVersion` stays `null` and a host can tell the default went out. The
|
|
144
|
+
renderer logs the key and the notification id when it falls back — never the context.
|
|
99
145
|
|
|
100
146
|
### What the inner renderer has to do
|
|
101
147
|
|
|
@@ -307,14 +353,33 @@ await service.updateTemplate('welcome', {
|
|
|
307
353
|
// tags: undefined carries them forward; [] clears them
|
|
308
354
|
});
|
|
309
355
|
|
|
310
|
-
await service.getTemplate('welcome'); // latest version
|
|
356
|
+
await service.getTemplate('welcome'); // latest version, whatever its status
|
|
311
357
|
await service.getTemplate('welcome', 1); // a specific one
|
|
358
|
+
await service.getActiveTemplate('welcome'); // what an unpinned send renders
|
|
312
359
|
await service.getTemplateVersions('welcome'); // every version, newest first
|
|
313
360
|
```
|
|
314
361
|
|
|
315
|
-
An absent `version` means "the latest version of this key"
|
|
316
|
-
|
|
317
|
-
|
|
362
|
+
An absent `version` means "the latest version of this key" for reads, status changes and tagging —
|
|
363
|
+
the editing view, which includes a draft someone is working on. Sends are the exception, below.
|
|
364
|
+
|
|
365
|
+
### Which version a send renders
|
|
366
|
+
|
|
367
|
+
"Latest" means two things, and they are kept apart:
|
|
368
|
+
|
|
369
|
+
| | Resolves to | Used by |
|
|
370
|
+
|---|---|---|
|
|
371
|
+
| The editing view | the newest version, whatever its status | `getTemplate(key)`, status changes, tagging, the API's reads |
|
|
372
|
+
| The send path | the newest **`active`** version | `render`, `renderManaged` with no version and no pin, `getLatestTemplateVersion` |
|
|
373
|
+
|
|
374
|
+
A draft is never sent: publishing is the deliberate act that puts a version in front of
|
|
375
|
+
recipients. When several versions of a key are active at once, the **highest-numbered** active one
|
|
376
|
+
renders. A key with no active version throws `ManagedTemplateNoActiveVersionError`, a subclass of
|
|
377
|
+
`ManagedTemplateNotFoundError` — so a key holding only drafts counts as "not customized yet", and a
|
|
378
|
+
registered fallback applies.
|
|
379
|
+
|
|
380
|
+
A notification **pinned** to a version renders that version whatever its status today. The pin is
|
|
381
|
+
there so a notification renders what was reviewed when it was created, so deactivating the version
|
|
382
|
+
later does not change what a pinned notification renders.
|
|
318
383
|
|
|
319
384
|
### Version-pinned rendering
|
|
320
385
|
|
|
@@ -326,12 +391,13 @@ const { version, rendered } = await renderer.renderManaged(notification, context
|
|
|
326
391
|
```
|
|
327
392
|
|
|
328
393
|
Which version renders is decided in this order: an explicit `version` argument, then the
|
|
329
|
-
notification's own `requestedTemplateVersion`, then
|
|
394
|
+
notification's own `requestedTemplateVersion`, then the key's newest active version.
|
|
330
395
|
|
|
331
396
|
`requestedTemplateVersion` is a first-class VintaSend field: pass it to `createNotification`, or
|
|
332
397
|
let the service resolve it for you with `pinTemplateVersions`. This package is what makes it mean
|
|
333
|
-
anything — `getLatestTemplateVersion` is how the service resolves "
|
|
334
|
-
and it is overridden here to
|
|
398
|
+
anything — `getLatestTemplateVersion` is how the service resolves "what would be sent right now",
|
|
399
|
+
and it is overridden here to answer the key's newest active version (or `null` when nothing is
|
|
400
|
+
published).
|
|
335
401
|
|
|
336
402
|
`render` also stamps the version it used onto the payload it returns, as `templateVersion`. An
|
|
337
403
|
adapter returns that payload from `send()`, and the service records it on the notification as
|
|
@@ -370,9 +436,27 @@ application.
|
|
|
370
436
|
Two things the service deliberately does *not* decide for you:
|
|
371
437
|
|
|
372
438
|
* **A key may have several `active` versions at once.** Activating one does not deactivate the
|
|
373
|
-
others
|
|
439
|
+
others. An unpinned send renders the highest-numbered active one.
|
|
374
440
|
* **`changedBy` is passed through untouched, `null` included.** Attribution is never required.
|
|
375
441
|
|
|
442
|
+
### Deleting a version
|
|
443
|
+
|
|
444
|
+
Only a version that was **never published** can be deleted: one still in `draft` whose status
|
|
445
|
+
history records nothing but `draft`. Anything else throws `ManagedTemplateDeletionNotAllowedError`
|
|
446
|
+
— a published version may have rendered a notification that is pinned to it, and its status history
|
|
447
|
+
is the record of who published it. Retire it with `archive` instead.
|
|
448
|
+
|
|
449
|
+
```ts
|
|
450
|
+
await service.deleteTemplate('welcome', 3); // fine while v3 is an unpublished draft
|
|
451
|
+
await service.deleteTemplate('welcome', 1); // throws once v1 has been activated
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
The status history is never deleted, even when the version is. The service checks the rule
|
|
455
|
+
before it calls the backend, so every backend is held to it; the backends shipped with VintaSend
|
|
456
|
+
check it themselves too. If an operator genuinely needs to hard-delete a published version, pass
|
|
457
|
+
`allowDeletingPublishedVersions: true` to both the service and the backend.
|
|
458
|
+
`isTemplateVersionDeletable(template, history)` answers the question without attempting the delete.
|
|
459
|
+
|
|
376
460
|
## Tags
|
|
377
461
|
|
|
378
462
|
Tags are many-to-many with template *versions* and are identified by a slug derived from the text
|
|
@@ -502,15 +586,25 @@ Implement `BaseTemplateManagerBackend`. `InMemoryTemplateManagerBackend` is a co
|
|
|
502
586
|
implementation to read against, and the seam's own test suite
|
|
503
587
|
(`src/__tests__/in-memory-backend.test.ts`) doubles as a conformance checklist.
|
|
504
588
|
|
|
505
|
-
The
|
|
589
|
+
The four rules that are easy to miss:
|
|
506
590
|
|
|
507
591
|
1. **Derive `isAbstract` on every write** that touches a source field, with `isAbstract()` from
|
|
508
592
|
this package, and store the answer. A source whose composition tags are malformed has no answer:
|
|
509
593
|
store `false` rather than letting the syntax error out of the write.
|
|
510
|
-
2. **`updateTemplate` inserts, never updates.** Copy the latest version forward,
|
|
511
|
-
|
|
594
|
+
2. **`updateTemplate` inserts, never updates.** Copy the latest version forward, start the copy in
|
|
595
|
+
`draft`, and leave the version it was copied from untouched. Number it one above the highest
|
|
596
|
+
version the key has *ever* had — a deleted version's number is never reused, or the new version
|
|
597
|
+
would inherit its status history and the notifications pinned to it.
|
|
512
598
|
3. **`mostRecentActiveVersion` is answered against the key, not the row.** "This row is `active` or
|
|
513
599
|
`draft`, and no `active`-or-`draft` row of the same key is numbered higher."
|
|
600
|
+
4. **`deleteTemplate` refuses a published version and keeps the status history.** Use
|
|
601
|
+
`assertTemplateVersionDeletable`, and put any hard delete of a published version behind an
|
|
602
|
+
option that is off by default.
|
|
603
|
+
|
|
604
|
+
`getActiveTemplate` — the newest `active` version, for the send path — is optional. Leave it out
|
|
605
|
+
and the library answers it with `getFilteredTemplates({ key, status: 'active' })`; implement it when
|
|
606
|
+
your store can answer more cheaply, throwing `noActiveVersion(key)` for a key with no active
|
|
607
|
+
version.
|
|
514
608
|
|
|
515
609
|
Slug every tag with `slugifyTag` and keep slugs unique with `nextAvailableSlug`, so your store and
|
|
516
610
|
every other one derive the same identity from the same text.
|
|
@@ -38,11 +38,30 @@ export interface BaseTemplateManagerBackend {
|
|
|
38
38
|
*/
|
|
39
39
|
createTemplate(data: ManagedTemplateCreateInput): Promise<ManagedTemplate>;
|
|
40
40
|
/**
|
|
41
|
-
* One version of a template. `version` absent or `null` returns the latest version
|
|
41
|
+
* One version of a template. `version` absent or `null` returns the latest version, whatever
|
|
42
|
+
* its status.
|
|
43
|
+
*
|
|
44
|
+
* That is the *editing view*: an editor or an API wants the draft someone is working on. A send
|
|
45
|
+
* never renders it — sends go through {@link getActiveTemplate} instead.
|
|
42
46
|
*
|
|
43
47
|
* @throws ManagedTemplateNotFoundError if the key (or that version of it) does not exist.
|
|
44
48
|
*/
|
|
45
49
|
getTemplate(templateKey: string, version?: number | null): Promise<ManagedTemplate>;
|
|
50
|
+
/**
|
|
51
|
+
* The version an unpinned send renders: the highest-numbered version whose status is `active`.
|
|
52
|
+
*
|
|
53
|
+
* Drafts are skipped whatever their number, and so are `inactive` and `archived` versions. A key
|
|
54
|
+
* may hold several active versions at once; the newest of them wins.
|
|
55
|
+
*
|
|
56
|
+
* Optional, so a backend written before it existed keeps compiling. The library answers for a
|
|
57
|
+
* backend that leaves it out by filtering on `{ key, status: 'active' }` — implement it when the
|
|
58
|
+
* store can answer more cheaply. `newestActiveVersion` and `noActiveVersion` are exported so an
|
|
59
|
+
* implementation shares the rule and the error rather than re-deriving them.
|
|
60
|
+
*
|
|
61
|
+
* @throws ManagedTemplateNotFoundError if the key does not exist.
|
|
62
|
+
* @throws ManagedTemplateNoActiveVersionError if the key exists but no version of it is active.
|
|
63
|
+
*/
|
|
64
|
+
getActiveTemplate?(templateKey: string): Promise<ManagedTemplate>;
|
|
46
65
|
/**
|
|
47
66
|
* Create a new version of an existing template, copied forward from its latest one.
|
|
48
67
|
*
|
|
@@ -51,6 +70,12 @@ export interface BaseTemplateManagerBackend {
|
|
|
51
70
|
* went out against v1 renders v1 forever, however many versions follow it, and several
|
|
52
71
|
* versions of one key are live at the same time as a matter of course.
|
|
53
72
|
*
|
|
73
|
+
* The new version number is one above the highest the key has **ever** had, not one above the
|
|
74
|
+
* latest live version. A deleted version's status history survives it and is read by key and
|
|
75
|
+
* version, so a reused number would inherit that history — and a notification pinned to the
|
|
76
|
+
* deleted number would start rendering the newcomer. The same goes for `createTemplate` on a key
|
|
77
|
+
* whose versions were all deleted.
|
|
78
|
+
*
|
|
54
79
|
* The new version starts in `draft` whatever its predecessor's status was, so a copy nobody
|
|
55
80
|
* has reviewed is never published by the act of creating it. Fields left absent on the input —
|
|
56
81
|
* tags included — carry forward from the version copied.
|
|
@@ -64,7 +89,17 @@ export interface BaseTemplateManagerBackend {
|
|
|
64
89
|
/**
|
|
65
90
|
* Delete one version of a template, or its latest version when `version` is absent.
|
|
66
91
|
*
|
|
92
|
+
* Only a version that was never published may be deleted — still in `draft`, with nothing but
|
|
93
|
+
* `draft` in its status history (`isTemplateVersionDeletable`). A backend should refuse anything
|
|
94
|
+
* else with `ManagedTemplateDeletionNotAllowedError` unless its operator explicitly configured
|
|
95
|
+
* it to allow hard deletes; `ManagedTemplateService` checks the rule before calling this either
|
|
96
|
+
* way, so a backend that does not enforce it is still protected behind the service.
|
|
97
|
+
*
|
|
98
|
+
* Never delete the version's status history. It is the audit trail of who published what, and
|
|
99
|
+
* it has to outlive the version it describes.
|
|
100
|
+
*
|
|
67
101
|
* @throws ManagedTemplateNotFoundError if the key (or that version of it) does not exist.
|
|
102
|
+
* @throws ManagedTemplateDeletionNotAllowedError if the version has been published.
|
|
68
103
|
*/
|
|
69
104
|
deleteTemplate(templateKey: string, version?: number | null): Promise<void>;
|
|
70
105
|
/** Record a status change for one version in the audit trail. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"base-template-manager-backend.d.ts","sourceRoot":"","sources":["../src/base-template-manager-backend.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAC;AACtF,OAAO,KAAK,EACV,qBAAqB,EACrB,iCAAiC,EACjC,sBAAsB,EACvB,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EACV,eAAe,EACf,0BAA0B,EAC1B,4BAA4B,EAC5B,kBAAkB,EAClB,0BAA0B,EAC3B,MAAM,YAAY,CAAC;AAEpB;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,WAAW,0BAA0B;IACzC;;;;;;;OAOG;IACH,qBAAqB,CAAC,IAAI,iCAAiC,CAAC;IAE5D;;;;OAIG;IACH,cAAc,CAAC,IAAI,EAAE,0BAA0B,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;IAE3E
|
|
1
|
+
{"version":3,"file":"base-template-manager-backend.d.ts","sourceRoot":"","sources":["../src/base-template-manager-backend.ts"],"names":[],"mappings":"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"}
|
package/dist/errors.d.ts
CHANGED
|
@@ -11,6 +11,27 @@ export declare class ManagedTemplateError extends Error {
|
|
|
11
11
|
/** Raised when a template is not found in the backend. */
|
|
12
12
|
export declare class ManagedTemplateNotFoundError extends ManagedTemplateError {
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Raised when a send resolves a key that has versions, but none of them is `active`.
|
|
16
|
+
*
|
|
17
|
+
* A subclass of {@link ManagedTemplateNotFoundError} on purpose: from a send's point of view a
|
|
18
|
+
* key holding only drafts (or only retired versions) has nothing published to render, which is
|
|
19
|
+
* the same answer as a key with nothing stored. That is what lets a renderer's fallback treat a
|
|
20
|
+
* key nobody has published yet as "not customized", and what lets an API keep mapping it to 404.
|
|
21
|
+
* Catch this subclass to tell "exists but unpublished" apart from "never created".
|
|
22
|
+
*/
|
|
23
|
+
export declare class ManagedTemplateNoActiveVersionError extends ManagedTemplateNotFoundError {
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Raised when deleting a template version the deletion rule protects.
|
|
27
|
+
*
|
|
28
|
+
* Only a version that was never published can be deleted: one still in `draft` whose status
|
|
29
|
+
* history records nothing but its creation. Anything else may have rendered a notification that
|
|
30
|
+
* is pinned to it, and its status history is the record of who published it — retire it with
|
|
31
|
+
* `archive` instead. See `isTemplateVersionDeletable`.
|
|
32
|
+
*/
|
|
33
|
+
export declare class ManagedTemplateDeletionNotAllowedError extends ManagedTemplateError {
|
|
34
|
+
}
|
|
14
35
|
/** Raised when a filter is malformed or names a field the vocabulary does not have. */
|
|
15
36
|
export declare class ManagedTemplateInvalidFilterError extends ManagedTemplateError {
|
|
16
37
|
}
|
package/dist/errors.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,qBAAa,oBAAqB,SAAQ,KAAK;gBACjC,OAAO,EAAE,MAAM;CAK5B;AAED,0DAA0D;AAC1D,qBAAa,4BAA6B,SAAQ,oBAAoB;CAAG;AAEzE,uFAAuF;AACvF,qBAAa,iCAAkC,SAAQ,oBAAoB;CAAG;AAE9E;;;;;;;GAOG;AACH,qBAAa,uCAAwC,SAAQ,oBAAoB;CAAG;AAEpF,sFAAsF;AACtF,qBAAa,sCAAuC,SAAQ,oBAAoB;CAAG;AAEnF,oFAAoF;AACpF,qBAAa,oCAAqC,SAAQ,oBAAoB;CAAG;AAEjF,qDAAqD;AACrD,qBAAa,+BAAgC,SAAQ,oBAAoB;CAAG;AAE5E,gFAAgF;AAChF,qBAAa,oCAAqC,SAAQ,oBAAoB;CAAG;AAEjF,8EAA8E;AAC9E,qBAAa,8BAA+B,SAAQ,oBAAoB;CAAG;AAE3E;;;;;;GAMG;AACH,qBAAa,+BAAgC,SAAQ,oBAAoB;CAAG;AAE5E,qFAAqF;AACrF,qBAAa,qCAAsC,SAAQ,+BAA+B;CAAG;AAE7F;;;;;;;;;GASG;AACH,qBAAa,wCAAyC,SAAQ,+BAA+B;CAAG;AAEhG,wFAAwF;AACxF,qBAAa,oCAAqC,SAAQ,+BAA+B;CAAG;AAE5F,oFAAoF;AACpF,qBAAa,oCAAqC,SAAQ,+BAA+B;CAAG;AAE5F;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAKvD"}
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,qBAAa,oBAAqB,SAAQ,KAAK;gBACjC,OAAO,EAAE,MAAM;CAK5B;AAED,0DAA0D;AAC1D,qBAAa,4BAA6B,SAAQ,oBAAoB;CAAG;AAEzE;;;;;;;;GAQG;AACH,qBAAa,mCAAoC,SAAQ,4BAA4B;CAAG;AAExF;;;;;;;GAOG;AACH,qBAAa,sCAAuC,SAAQ,oBAAoB;CAAG;AAEnF,uFAAuF;AACvF,qBAAa,iCAAkC,SAAQ,oBAAoB;CAAG;AAE9E;;;;;;;GAOG;AACH,qBAAa,uCAAwC,SAAQ,oBAAoB;CAAG;AAEpF,sFAAsF;AACtF,qBAAa,sCAAuC,SAAQ,oBAAoB;CAAG;AAEnF,oFAAoF;AACpF,qBAAa,oCAAqC,SAAQ,oBAAoB;CAAG;AAEjF,qDAAqD;AACrD,qBAAa,+BAAgC,SAAQ,oBAAoB;CAAG;AAE5E,gFAAgF;AAChF,qBAAa,oCAAqC,SAAQ,oBAAoB;CAAG;AAEjF,8EAA8E;AAC9E,qBAAa,8BAA+B,SAAQ,oBAAoB;CAAG;AAE3E;;;;;;GAMG;AACH,qBAAa,+BAAgC,SAAQ,oBAAoB;CAAG;AAE5E,qFAAqF;AACrF,qBAAa,qCAAsC,SAAQ,+BAA+B;CAAG;AAE7F;;;;;;;;;GASG;AACH,qBAAa,wCAAyC,SAAQ,+BAA+B;CAAG;AAEhG,wFAAwF;AACxF,qBAAa,oCAAqC,SAAQ,+BAA+B;CAAG;AAE5F,oFAAoF;AACpF,qBAAa,oCAAqC,SAAQ,+BAA+B;CAAG;AAE5F;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAKvD"}
|
package/dist/errors.js
CHANGED
|
@@ -15,6 +15,27 @@ export class ManagedTemplateError extends Error {
|
|
|
15
15
|
/** Raised when a template is not found in the backend. */
|
|
16
16
|
export class ManagedTemplateNotFoundError extends ManagedTemplateError {
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Raised when a send resolves a key that has versions, but none of them is `active`.
|
|
20
|
+
*
|
|
21
|
+
* A subclass of {@link ManagedTemplateNotFoundError} on purpose: from a send's point of view a
|
|
22
|
+
* key holding only drafts (or only retired versions) has nothing published to render, which is
|
|
23
|
+
* the same answer as a key with nothing stored. That is what lets a renderer's fallback treat a
|
|
24
|
+
* key nobody has published yet as "not customized", and what lets an API keep mapping it to 404.
|
|
25
|
+
* Catch this subclass to tell "exists but unpublished" apart from "never created".
|
|
26
|
+
*/
|
|
27
|
+
export class ManagedTemplateNoActiveVersionError extends ManagedTemplateNotFoundError {
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Raised when deleting a template version the deletion rule protects.
|
|
31
|
+
*
|
|
32
|
+
* Only a version that was never published can be deleted: one still in `draft` whose status
|
|
33
|
+
* history records nothing but its creation. Anything else may have rendered a notification that
|
|
34
|
+
* is pinned to it, and its status history is the record of who published it — retire it with
|
|
35
|
+
* `archive` instead. See `isTemplateVersionDeletable`.
|
|
36
|
+
*/
|
|
37
|
+
export class ManagedTemplateDeletionNotAllowedError extends ManagedTemplateError {
|
|
38
|
+
}
|
|
18
39
|
/** Raised when a filter is malformed or names a field the vocabulary does not have. */
|
|
19
40
|
export class ManagedTemplateInvalidFilterError extends ManagedTemplateError {
|
|
20
41
|
}
|
|
@@ -17,17 +17,28 @@ import type { ManagedTemplate, ManagedTemplateCreateInput, ManagedTemplateStatus
|
|
|
17
17
|
export type InMemoryTemplateManagerBackendOptions = {
|
|
18
18
|
/** Overridden in tests so timestamps are deterministic. */
|
|
19
19
|
now?: () => Date;
|
|
20
|
+
/**
|
|
21
|
+
* When true, `deleteTemplate` removes a version whatever its status. Off by default: only a
|
|
22
|
+
* version that was never published can be deleted (see `isTemplateVersionDeletable`).
|
|
23
|
+
* `ManagedTemplateService` checks the rule too, under its own option of the same name.
|
|
24
|
+
*/
|
|
25
|
+
allowDeletingPublishedVersions?: boolean;
|
|
20
26
|
};
|
|
21
27
|
export declare class InMemoryTemplateManagerBackend implements BaseTemplateManagerBackend {
|
|
22
28
|
private templates;
|
|
23
29
|
private tags;
|
|
24
30
|
private history;
|
|
25
31
|
private nextId;
|
|
32
|
+
/** The highest version number each key has had deleted, so it is never handed out again. */
|
|
33
|
+
private highestDeletedVersion;
|
|
26
34
|
private readonly now;
|
|
35
|
+
private readonly allowDeletingPublishedVersions;
|
|
27
36
|
constructor(options?: InMemoryTemplateManagerBackendOptions);
|
|
28
37
|
createTemplate(data: ManagedTemplateCreateInput): Promise<ManagedTemplate>;
|
|
29
38
|
getTemplate(templateKey: string, version?: number | null): Promise<ManagedTemplate>;
|
|
39
|
+
getActiveTemplate(templateKey: string): Promise<ManagedTemplate>;
|
|
30
40
|
updateTemplate(templateKey: string, data: ManagedTemplateUpdateInput): Promise<ManagedTemplate>;
|
|
41
|
+
/** Delete one never-published version. Its status history is kept either way. */
|
|
31
42
|
deleteTemplate(templateKey: string, version?: number | null): Promise<void>;
|
|
32
43
|
createTemplateStatusUpdate(params: {
|
|
33
44
|
templateKey: string;
|
|
@@ -59,6 +70,16 @@ export declare class InMemoryTemplateManagerBackend implements BaseTemplateManag
|
|
|
59
70
|
getPaginatedTemplates(page: number, pageSize: number, orderBy?: ManagedTemplateOrderBy): Promise<ManagedTemplate[]>;
|
|
60
71
|
getPaginatedFilteredTemplates(filters: ManagedTemplateFilter, page: number, pageSize: number, orderBy?: ManagedTemplateOrderBy): Promise<ManagedTemplate[]>;
|
|
61
72
|
private deriveIsAbstract;
|
|
73
|
+
/**
|
|
74
|
+
* One above the highest version number the key has ever had — live, deleted or only in its
|
|
75
|
+
* status history.
|
|
76
|
+
*
|
|
77
|
+
* A number is never reused. Status history outlives a deleted version and is read by key and
|
|
78
|
+
* version, so a new version that took a deleted one's number would inherit its history: a
|
|
79
|
+
* publish that never happened, and a draft the deletion rule then refuses. A notification pinned
|
|
80
|
+
* to the deleted number would also start rendering the newcomer.
|
|
81
|
+
*/
|
|
82
|
+
private nextVersion;
|
|
62
83
|
private versionsOf;
|
|
63
84
|
private find;
|
|
64
85
|
private requireTag;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"in-memory-template-manager-backend.d.ts","sourceRoot":"","sources":["../src/in-memory-template-manager-backend.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,oCAAoC,CAAC;AAErF,OAAO,KAAK,EAAE,qBAAqB,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAC;AAQtF,OAAO,EAEL,KAAK,qBAAqB,EAC1B,KAAK,iCAAiC,EACtC,KAAK,sBAAsB,EAE5B,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"in-memory-template-manager-backend.d.ts","sourceRoot":"","sources":["../src/in-memory-template-manager-backend.ts"],"names":[],"mappings":"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"}
|
|
@@ -14,6 +14,7 @@ import { isAbstract } from './composition.js';
|
|
|
14
14
|
import { ManagedTemplateInvalidTagError, ManagedTemplateNotFoundError, ManagedTemplateTagAlreadyExistsError, ManagedTemplateTagNotFoundError, } from './errors.js';
|
|
15
15
|
import { matchesTemplateFilter, paginate, sortTemplates } from './filter-evaluation.js';
|
|
16
16
|
import { MANAGED_TEMPLATE_ORDER_BY_FIELDS, orderByCapabilityKey, } from './filters.js';
|
|
17
|
+
import { assertTemplateVersionDeletable, newestActiveVersion, noActiveVersion, } from './lifecycle.js';
|
|
17
18
|
import { nextAvailableSlug, normalizeTagText, slugifyTag } from './tags.js';
|
|
18
19
|
export class InMemoryTemplateManagerBackend {
|
|
19
20
|
constructor(options = {}) {
|
|
@@ -21,7 +22,10 @@ export class InMemoryTemplateManagerBackend {
|
|
|
21
22
|
this.tags = [];
|
|
22
23
|
this.history = [];
|
|
23
24
|
this.nextId = 1;
|
|
25
|
+
/** The highest version number each key has had deleted, so it is never handed out again. */
|
|
26
|
+
this.highestDeletedVersion = new Map();
|
|
24
27
|
this.now = options.now ?? (() => new Date());
|
|
28
|
+
this.allowDeletingPublishedVersions = options.allowDeletingPublishedVersions ?? false;
|
|
25
29
|
}
|
|
26
30
|
// -------------------------------------------------------------------------------------------
|
|
27
31
|
// Templates
|
|
@@ -31,7 +35,8 @@ export class InMemoryTemplateManagerBackend {
|
|
|
31
35
|
const template = {
|
|
32
36
|
id: this.nextId++,
|
|
33
37
|
key: data.key,
|
|
34
|
-
|
|
38
|
+
// A key whose versions were all deleted starts above every number it used before.
|
|
39
|
+
version: this.versionsOf(data.key).length > 0 ? 1 : this.nextVersion(data.key),
|
|
35
40
|
name: data.name,
|
|
36
41
|
description: data.description,
|
|
37
42
|
templateManagedBackend: data.templateManagedBackend,
|
|
@@ -55,6 +60,17 @@ export class InMemoryTemplateManagerBackend {
|
|
|
55
60
|
}
|
|
56
61
|
return structuredCloneTemplate(template);
|
|
57
62
|
}
|
|
63
|
+
async getActiveTemplate(templateKey) {
|
|
64
|
+
const versions = this.versionsOf(templateKey);
|
|
65
|
+
if (versions.length === 0) {
|
|
66
|
+
throw new ManagedTemplateNotFoundError(describeMissing(templateKey, null));
|
|
67
|
+
}
|
|
68
|
+
const active = newestActiveVersion(versions);
|
|
69
|
+
if (active === undefined) {
|
|
70
|
+
throw noActiveVersion(templateKey);
|
|
71
|
+
}
|
|
72
|
+
return structuredCloneTemplate(active);
|
|
73
|
+
}
|
|
58
74
|
async updateTemplate(templateKey, data) {
|
|
59
75
|
const previous = this.find(templateKey, null);
|
|
60
76
|
if (previous === undefined) {
|
|
@@ -74,7 +90,8 @@ export class InMemoryTemplateManagerBackend {
|
|
|
74
90
|
const template = {
|
|
75
91
|
id: this.nextId++,
|
|
76
92
|
key: previous.key,
|
|
77
|
-
|
|
93
|
+
// Not `previous.version + 1`: a deleted version above it keeps its number.
|
|
94
|
+
version: this.nextVersion(previous.key),
|
|
78
95
|
name: data.name || previous.name,
|
|
79
96
|
description: data.description ?? previous.description,
|
|
80
97
|
templateManagedBackend: previous.templateManagedBackend,
|
|
@@ -90,12 +107,17 @@ export class InMemoryTemplateManagerBackend {
|
|
|
90
107
|
this.templates.push(template);
|
|
91
108
|
return structuredCloneTemplate(template);
|
|
92
109
|
}
|
|
110
|
+
/** Delete one never-published version. Its status history is kept either way. */
|
|
93
111
|
async deleteTemplate(templateKey, version = null) {
|
|
94
112
|
const template = this.find(templateKey, version);
|
|
95
113
|
if (template === undefined) {
|
|
96
114
|
throw new ManagedTemplateNotFoundError(describeMissing(templateKey, version));
|
|
97
115
|
}
|
|
116
|
+
if (!this.allowDeletingPublishedVersions) {
|
|
117
|
+
assertTemplateVersionDeletable(template, this.history);
|
|
118
|
+
}
|
|
98
119
|
this.templates = this.templates.filter((candidate) => candidate !== template);
|
|
120
|
+
this.highestDeletedVersion.set(templateKey, Math.max(this.highestDeletedVersion.get(templateKey) ?? 0, template.version));
|
|
99
121
|
}
|
|
100
122
|
async createTemplateStatusUpdate(params) {
|
|
101
123
|
const template = this.find(params.templateKey, params.version);
|
|
@@ -234,6 +256,20 @@ export class InMemoryTemplateManagerBackend {
|
|
|
234
256
|
return false;
|
|
235
257
|
}
|
|
236
258
|
}
|
|
259
|
+
/**
|
|
260
|
+
* One above the highest version number the key has ever had — live, deleted or only in its
|
|
261
|
+
* status history.
|
|
262
|
+
*
|
|
263
|
+
* A number is never reused. Status history outlives a deleted version and is read by key and
|
|
264
|
+
* version, so a new version that took a deleted one's number would inherit its history: a
|
|
265
|
+
* publish that never happened, and a draft the deletion rule then refuses. A notification pinned
|
|
266
|
+
* to the deleted number would also start rendering the newcomer.
|
|
267
|
+
*/
|
|
268
|
+
nextVersion(templateKey) {
|
|
269
|
+
return (Math.max(0, this.highestDeletedVersion.get(templateKey) ?? 0, ...this.versionsOf(templateKey).map((template) => template.version), ...this.history
|
|
270
|
+
.filter((record) => record.templateKey === templateKey)
|
|
271
|
+
.map((record) => record.version)) + 1);
|
|
272
|
+
}
|
|
237
273
|
versionsOf(templateKey) {
|
|
238
274
|
return this.templates.filter((template) => template.key === templateKey);
|
|
239
275
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,14 +3,15 @@ export type { TemplateComposerOptions, TemplateReference, TemplateResolver, } fr
|
|
|
3
3
|
export { DEFAULT_MAX_DEPTH, DEFAULT_TAG_PREFIX, isAbstract, TemplateComposer, } from './composition.js';
|
|
4
4
|
export type { ManagedTemplateStatus, ManagedTemplateTagStatus, TemplateField, } from './constants.js';
|
|
5
5
|
export { MANAGED_TEMPLATE_STATUSES, MANAGED_TEMPLATE_TAG_STATUSES, MOST_RECENT_ACTIVE_VERSION_STATUSES, TEMPLATE_FIELDS, } from './constants.js';
|
|
6
|
-
export { isNotFoundError, ManagedTemplateChangeUserNotFoundError, ManagedTemplateCompositionCycleError, ManagedTemplateCompositionDepthError, ManagedTemplateCompositionError, ManagedTemplateCompositionReferenceError, ManagedTemplateCompositionSyntaxError, ManagedTemplateError, ManagedTemplateInvalidFilterError, ManagedTemplateInvalidTagError, ManagedTemplateNotFoundError, ManagedTemplateStatusTransitionError, ManagedTemplateTagAlreadyExistsError, ManagedTemplateTagNotFoundError, ManagedTemplateUnsupportedOrderingError, } from './errors.js';
|
|
6
|
+
export { isNotFoundError, ManagedTemplateChangeUserNotFoundError, ManagedTemplateCompositionCycleError, ManagedTemplateCompositionDepthError, ManagedTemplateCompositionError, ManagedTemplateCompositionReferenceError, ManagedTemplateCompositionSyntaxError, ManagedTemplateDeletionNotAllowedError, ManagedTemplateError, ManagedTemplateInvalidFilterError, ManagedTemplateInvalidTagError, ManagedTemplateNoActiveVersionError, ManagedTemplateNotFoundError, ManagedTemplateStatusTransitionError, ManagedTemplateTagAlreadyExistsError, ManagedTemplateTagNotFoundError, ManagedTemplateUnsupportedOrderingError, } from './errors.js';
|
|
7
7
|
export type { FilterEvaluationContext } from './filter-evaluation.js';
|
|
8
8
|
export { isMostRecentActiveVersion, matchesDateRange, matchesInteger, matchesStatus, matchesString, matchesTemplateFilter, normalizeSlugs, paginate, sortTemplates, } from './filter-evaluation.js';
|
|
9
9
|
export type { DateRange, IntegerFieldFilter, ManagedTemplateFilter, ManagedTemplateFilterCapabilities, ManagedTemplateFilterFields, ManagedTemplateOrderBy, ManagedTemplateOrderByField, ManagedTemplateOrderDirection, ManagedTemplateStatusFilter, NumericFilterLookup, StringFieldFilter, StringFilterLookup, TagsFieldFilter, } from './filters.js';
|
|
10
10
|
export { DEFAULT_TEMPLATE_BACKEND_FILTER_CAPABILITIES, FLAG_FILTER_FIELDS, isDateRange, isEmptyFilter, isFieldFilter, isNumericFilterLookup, isStatusExactLookup, isStatusInLookup, isStringFilterLookup, isTagsFilter, KNOWN_FILTER_FIELDS, MANAGED_TEMPLATE_ORDER_BY_FIELDS, orderByCapabilityKey, pruneUnsupportedFilters, supportsCapability, TAG_FILTER_FIELDS, } from './filters.js';
|
|
11
11
|
export type { InMemoryTemplateManagerBackendOptions } from './in-memory-template-manager-backend.js';
|
|
12
12
|
export { InMemoryTemplateManagerBackend } from './in-memory-template-manager-backend.js';
|
|
13
|
-
export
|
|
13
|
+
export { assertTemplateVersionDeletable, isTemplateVersionDeletable, newestActiveVersion, noActiveVersion, resolveActiveTemplate, } from './lifecycle.js';
|
|
14
|
+
export type { ManagedEmailTemplateContent, ManagedTemplateFallbackOptions, ManagedTemplateFallbackTemplate, ManagedTemplateRendererOptions, ManagedTemplateRenderResult, TextTemplate, TextTemplateContent, VersionPinnedNotification, } from './managed-template-renderer.js';
|
|
14
15
|
export { ManagedTemplateEmailRenderer, ManagedTemplateRenderer, ManagedTemplateTextRenderer, requestedTemplateVersion, } from './managed-template-renderer.js';
|
|
15
16
|
export type { ManagedTemplateServiceOptions } from './managed-template-service.js';
|
|
16
17
|
export { DEFAULT_STATUS_TRANSITIONS, ManagedTemplateService } from './managed-template-service.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,YAAY,EAAE,0BAA0B,EAAE,MAAM,oCAAoC,CAAC;AACrF,YAAY,EACV,uBAAuB,EACvB,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EACL,iBAAiB,EACjB,kBAAkB,EAClB,UAAU,EACV,gBAAgB,GACjB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACV,qBAAqB,EACrB,wBAAwB,EACxB,aAAa,GACd,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,yBAAyB,EACzB,6BAA6B,EAC7B,mCAAmC,EACnC,eAAe,GAChB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,eAAe,EACf,sCAAsC,EACtC,oCAAoC,EACpC,oCAAoC,EACpC,+BAA+B,EAC/B,wCAAwC,EACxC,qCAAqC,EACrC,oBAAoB,EACpB,iCAAiC,EACjC,8BAA8B,EAC9B,4BAA4B,EAC5B,oCAAoC,EACpC,oCAAoC,EACpC,+BAA+B,EAC/B,uCAAuC,GACxC,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AAEtE,OAAO,EACL,yBAAyB,EACzB,gBAAgB,EAChB,cAAc,EACd,aAAa,EACb,aAAa,EACb,qBAAqB,EACrB,cAAc,EACd,QAAQ,EACR,aAAa,GACd,MAAM,wBAAwB,CAAC;AAChC,YAAY,EACV,SAAS,EACT,kBAAkB,EAClB,qBAAqB,EACrB,iCAAiC,EACjC,2BAA2B,EAC3B,sBAAsB,EACtB,2BAA2B,EAC3B,6BAA6B,EAC7B,2BAA2B,EAC3B,mBAAmB,EACnB,iBAAiB,EACjB,kBAAkB,EAClB,eAAe,GAChB,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,4CAA4C,EAC5C,kBAAkB,EAClB,WAAW,EACX,aAAa,EACb,aAAa,EACb,qBAAqB,EACrB,mBAAmB,EACnB,gBAAgB,EAChB,oBAAoB,EACpB,YAAY,EACZ,mBAAmB,EACnB,gCAAgC,EAChC,oBAAoB,EACpB,uBAAuB,EACvB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,cAAc,CAAC;AACtB,YAAY,EAAE,qCAAqC,EAAE,MAAM,yCAAyC,CAAC;AAErG,OAAO,EAAE,8BAA8B,EAAE,MAAM,yCAAyC,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,YAAY,EAAE,0BAA0B,EAAE,MAAM,oCAAoC,CAAC;AACrF,YAAY,EACV,uBAAuB,EACvB,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EACL,iBAAiB,EACjB,kBAAkB,EAClB,UAAU,EACV,gBAAgB,GACjB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACV,qBAAqB,EACrB,wBAAwB,EACxB,aAAa,GACd,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,yBAAyB,EACzB,6BAA6B,EAC7B,mCAAmC,EACnC,eAAe,GAChB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,eAAe,EACf,sCAAsC,EACtC,oCAAoC,EACpC,oCAAoC,EACpC,+BAA+B,EAC/B,wCAAwC,EACxC,qCAAqC,EACrC,sCAAsC,EACtC,oBAAoB,EACpB,iCAAiC,EACjC,8BAA8B,EAC9B,mCAAmC,EACnC,4BAA4B,EAC5B,oCAAoC,EACpC,oCAAoC,EACpC,+BAA+B,EAC/B,uCAAuC,GACxC,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AAEtE,OAAO,EACL,yBAAyB,EACzB,gBAAgB,EAChB,cAAc,EACd,aAAa,EACb,aAAa,EACb,qBAAqB,EACrB,cAAc,EACd,QAAQ,EACR,aAAa,GACd,MAAM,wBAAwB,CAAC;AAChC,YAAY,EACV,SAAS,EACT,kBAAkB,EAClB,qBAAqB,EACrB,iCAAiC,EACjC,2BAA2B,EAC3B,sBAAsB,EACtB,2BAA2B,EAC3B,6BAA6B,EAC7B,2BAA2B,EAC3B,mBAAmB,EACnB,iBAAiB,EACjB,kBAAkB,EAClB,eAAe,GAChB,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,4CAA4C,EAC5C,kBAAkB,EAClB,WAAW,EACX,aAAa,EACb,aAAa,EACb,qBAAqB,EACrB,mBAAmB,EACnB,gBAAgB,EAChB,oBAAoB,EACpB,YAAY,EACZ,mBAAmB,EACnB,gCAAgC,EAChC,oBAAoB,EACpB,uBAAuB,EACvB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,cAAc,CAAC;AACtB,YAAY,EAAE,qCAAqC,EAAE,MAAM,yCAAyC,CAAC;AAErG,OAAO,EAAE,8BAA8B,EAAE,MAAM,yCAAyC,CAAC;AAEzF,OAAO,EACL,8BAA8B,EAC9B,0BAA0B,EAC1B,mBAAmB,EACnB,eAAe,EACf,qBAAqB,GACtB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EACV,2BAA2B,EAC3B,8BAA8B,EAC9B,+BAA+B,EAC/B,8BAA8B,EAC9B,2BAA2B,EAC3B,YAAY,EACZ,mBAAmB,EACnB,yBAAyB,GAC1B,MAAM,gCAAgC,CAAC;AAExC,OAAO,EACL,4BAA4B,EAC5B,uBAAuB,EACvB,2BAA2B,EAC3B,wBAAwB,GACzB,MAAM,gCAAgC,CAAC;AACxC,YAAY,EAAE,6BAA6B,EAAE,MAAM,+BAA+B,CAAC;AAEnF,OAAO,EAAE,0BAA0B,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AAEnG,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAE7F,YAAY,EACV,eAAe,EACf,0BAA0B,EAC1B,iBAAiB,EACjB,4BAA4B,EAC5B,kBAAkB,EAClB,0BAA0B,GAC3B,MAAM,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -3,13 +3,15 @@ export { DEFAULT_MAX_DEPTH, DEFAULT_TAG_PREFIX, isAbstract, TemplateComposer, }
|
|
|
3
3
|
// Constants
|
|
4
4
|
export { MANAGED_TEMPLATE_STATUSES, MANAGED_TEMPLATE_TAG_STATUSES, MOST_RECENT_ACTIVE_VERSION_STATUSES, TEMPLATE_FIELDS, } from './constants.js';
|
|
5
5
|
// Errors
|
|
6
|
-
export { isNotFoundError, ManagedTemplateChangeUserNotFoundError, ManagedTemplateCompositionCycleError, ManagedTemplateCompositionDepthError, ManagedTemplateCompositionError, ManagedTemplateCompositionReferenceError, ManagedTemplateCompositionSyntaxError, ManagedTemplateError, ManagedTemplateInvalidFilterError, ManagedTemplateInvalidTagError, ManagedTemplateNotFoundError, ManagedTemplateStatusTransitionError, ManagedTemplateTagAlreadyExistsError, ManagedTemplateTagNotFoundError, ManagedTemplateUnsupportedOrderingError, } from './errors.js';
|
|
6
|
+
export { isNotFoundError, ManagedTemplateChangeUserNotFoundError, ManagedTemplateCompositionCycleError, ManagedTemplateCompositionDepthError, ManagedTemplateCompositionError, ManagedTemplateCompositionReferenceError, ManagedTemplateCompositionSyntaxError, ManagedTemplateDeletionNotAllowedError, ManagedTemplateError, ManagedTemplateInvalidFilterError, ManagedTemplateInvalidTagError, ManagedTemplateNoActiveVersionError, ManagedTemplateNotFoundError, ManagedTemplateStatusTransitionError, ManagedTemplateTagAlreadyExistsError, ManagedTemplateTagNotFoundError, ManagedTemplateUnsupportedOrderingError, } from './errors.js';
|
|
7
7
|
// Filter evaluation, for a backend that has to finish a filter its query language cannot express
|
|
8
8
|
export { isMostRecentActiveVersion, matchesDateRange, matchesInteger, matchesStatus, matchesString, matchesTemplateFilter, normalizeSlugs, paginate, sortTemplates, } from './filter-evaluation.js';
|
|
9
9
|
// Filters and capabilities
|
|
10
10
|
export { DEFAULT_TEMPLATE_BACKEND_FILTER_CAPABILITIES, FLAG_FILTER_FIELDS, isDateRange, isEmptyFilter, isFieldFilter, isNumericFilterLookup, isStatusExactLookup, isStatusInLookup, isStringFilterLookup, isTagsFilter, KNOWN_FILTER_FIELDS, MANAGED_TEMPLATE_ORDER_BY_FIELDS, orderByCapabilityKey, pruneUnsupportedFilters, supportsCapability, TAG_FILTER_FIELDS, } from './filters.js';
|
|
11
11
|
// A backend to develop and test against
|
|
12
12
|
export { InMemoryTemplateManagerBackend } from './in-memory-template-manager-backend.js';
|
|
13
|
+
// Lifecycle rules: which version a send renders, which versions may be deleted
|
|
14
|
+
export { assertTemplateVersionDeletable, isTemplateVersionDeletable, newestActiveVersion, noActiveVersion, resolveActiveTemplate, } from './lifecycle.js';
|
|
13
15
|
// Renderers
|
|
14
16
|
export { ManagedTemplateEmailRenderer, ManagedTemplateRenderer, ManagedTemplateTextRenderer, requestedTemplateVersion, } from './managed-template-renderer.js';
|
|
15
17
|
// The service
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two lifecycle rules every backend has to agree on: which version a send renders, and which
|
|
3
|
+
* versions may be deleted.
|
|
4
|
+
*
|
|
5
|
+
* Both live here rather than in one backend so the in-memory store, a FHIR store and the service
|
|
6
|
+
* apply the same rule, and so a backend author has a function to call instead of prose to
|
|
7
|
+
* re-derive.
|
|
8
|
+
*
|
|
9
|
+
* **Which version a send renders.** "The latest version" means two different things, and the
|
|
10
|
+
* seam keeps them apart:
|
|
11
|
+
*
|
|
12
|
+
* * The *editing view* — `getTemplate(key)` with no version — is the newest version whatever its
|
|
13
|
+
* status. An editor or an API listing a key's history wants the draft someone is working on.
|
|
14
|
+
* * The *send path* — {@link resolveActiveTemplate} — is the newest `active` version. A draft is
|
|
15
|
+
* unreviewed by definition, so publishing is the deliberate act that puts a version in front of
|
|
16
|
+
* recipients. When several versions are active at once (the lifecycle allows it), the
|
|
17
|
+
* highest-numbered one wins.
|
|
18
|
+
*
|
|
19
|
+
* **Which versions may be deleted.** Only one that was never published — see
|
|
20
|
+
* {@link isTemplateVersionDeletable}.
|
|
21
|
+
*/
|
|
22
|
+
import type { BaseTemplateManagerBackend } from './base-template-manager-backend.js';
|
|
23
|
+
import { ManagedTemplateNoActiveVersionError } from './errors.js';
|
|
24
|
+
import type { ManagedTemplate, ManagedTemplateStatusHistory } from './types.js';
|
|
25
|
+
/**
|
|
26
|
+
* The highest-numbered `active` version among `versions`, or `undefined` when none is active.
|
|
27
|
+
*
|
|
28
|
+
* `versions` may hold several keys' rows; the caller narrows it to one key first.
|
|
29
|
+
*/
|
|
30
|
+
export declare function newestActiveVersion(versions: readonly ManagedTemplate[]): ManagedTemplate | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* The version an unpinned send of `templateKey` renders: its highest-numbered `active` version.
|
|
33
|
+
*
|
|
34
|
+
* Uses the backend's own `getActiveTemplate` when it has one. A backend written before that method
|
|
35
|
+
* existed is answered through the filter seam instead — an exact key match plus `status: 'active'`
|
|
36
|
+
* — which every backend supports by default.
|
|
37
|
+
*
|
|
38
|
+
* @throws ManagedTemplateNotFoundError if the key does not exist.
|
|
39
|
+
* @throws ManagedTemplateNoActiveVersionError if it exists but no version of it is active.
|
|
40
|
+
*/
|
|
41
|
+
export declare function resolveActiveTemplate(backend: BaseTemplateManagerBackend, templateKey: string): Promise<ManagedTemplate>;
|
|
42
|
+
/** The error a backend throws for a key that exists but has no `active` version. */
|
|
43
|
+
export declare function noActiveVersion(templateKey: string): ManagedTemplateNoActiveVersionError;
|
|
44
|
+
/**
|
|
45
|
+
* Whether a template version may be hard-deleted: it was never published.
|
|
46
|
+
*
|
|
47
|
+
* That means it is still in `draft` and its status history records nothing but `draft` — a
|
|
48
|
+
* backend that writes a creation entry is fine, and one that writes none is too. A version that
|
|
49
|
+
* was ever `active`, `inactive` or `archived` may have rendered a notification that is pinned to
|
|
50
|
+
* it, and its history is the record of who published it; retire it with `archive` instead.
|
|
51
|
+
*/
|
|
52
|
+
export declare function isTemplateVersionDeletable(template: ManagedTemplate, history: readonly ManagedTemplateStatusHistory[]): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Throw unless {@link isTemplateVersionDeletable} allows deleting `template`.
|
|
55
|
+
*
|
|
56
|
+
* @throws ManagedTemplateDeletionNotAllowedError
|
|
57
|
+
*/
|
|
58
|
+
export declare function assertTemplateVersionDeletable(template: ManagedTemplate, history: readonly ManagedTemplateStatusHistory[]): void;
|
|
59
|
+
//# sourceMappingURL=lifecycle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../src/lifecycle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,oCAAoC,CAAC;AACrF,OAAO,EAEL,mCAAmC,EACpC,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,eAAe,EAAE,4BAA4B,EAAE,MAAM,YAAY,CAAC;AAEhF;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,SAAS,eAAe,EAAE,GACnC,eAAe,GAAG,SAAS,CAQ7B;AAED;;;;;;;;;GASG;AACH,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,0BAA0B,EACnC,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,eAAe,CAAC,CAe1B;AAED,oFAAoF;AACpF,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,mCAAmC,CAIxF;AAED;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,eAAe,EACzB,OAAO,EAAE,SAAS,4BAA4B,EAAE,GAC/C,OAAO,CAST;AAED;;;;GAIG;AACH,wBAAgB,8BAA8B,CAC5C,QAAQ,EAAE,eAAe,EACzB,OAAO,EAAE,SAAS,4BAA4B,EAAE,GAC/C,IAAI,CAQN"}
|