vintasend-managed-templates 1.0.0-alpha2
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 +529 -0
- package/dist/base-template-manager-backend.d.ts +202 -0
- package/dist/base-template-manager-backend.d.ts.map +1 -0
- package/dist/base-template-manager-backend.js +1 -0
- package/dist/composition.d.ts +238 -0
- package/dist/composition.d.ts.map +1 -0
- package/dist/composition.js +0 -0
- package/dist/constants.d.ts +32 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +31 -0
- package/dist/errors.d.ts +80 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +86 -0
- package/dist/filter-evaluation.d.ts +69 -0
- package/dist/filter-evaluation.d.ts.map +1 -0
- package/dist/filter-evaluation.js +252 -0
- package/dist/filters.d.ts +192 -0
- package/dist/filters.d.ts.map +1 -0
- package/dist/filters.js +252 -0
- package/dist/in-memory-template-manager-backend.d.ts +85 -0
- package/dist/in-memory-template-manager-backend.d.ts.map +1 -0
- package/dist/in-memory-template-manager-backend.js +323 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/managed-template-renderer.d.ts +153 -0
- package/dist/managed-template-renderer.d.ts.map +1 -0
- package/dist/managed-template-renderer.js +152 -0
- package/dist/managed-template-service.d.ts +415 -0
- package/dist/managed-template-service.d.ts.map +1 -0
- package/dist/managed-template-service.js +712 -0
- package/dist/tags.d.ts +54 -0
- package/dist/tags.d.ts.map +1 -0
- package/dist/tags.js +108 -0
- package/dist/types.d.ts +100 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +41 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type { BaseTemplateManagerBackend } from './base-template-manager-backend.js';
|
|
2
|
+
export type { TemplateComposerOptions, TemplateReference, TemplateResolver, } from './composition.js';
|
|
3
|
+
export { DEFAULT_MAX_DEPTH, DEFAULT_TAG_PREFIX, isAbstract, TemplateComposer, } from './composition.js';
|
|
4
|
+
export type { ManagedTemplateStatus, ManagedTemplateTagStatus, TemplateField, } from './constants.js';
|
|
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';
|
|
7
|
+
export type { FilterEvaluationContext } from './filter-evaluation.js';
|
|
8
|
+
export { isMostRecentActiveVersion, matchesDateRange, matchesInteger, matchesStatus, matchesString, matchesTemplateFilter, normalizeSlugs, paginate, sortTemplates, } from './filter-evaluation.js';
|
|
9
|
+
export type { DateRange, IntegerFieldFilter, ManagedTemplateFilter, ManagedTemplateFilterCapabilities, ManagedTemplateFilterFields, ManagedTemplateOrderBy, ManagedTemplateOrderByField, ManagedTemplateOrderDirection, ManagedTemplateStatusFilter, NumericFilterLookup, StringFieldFilter, StringFilterLookup, TagsFieldFilter, } from './filters.js';
|
|
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
|
+
export type { InMemoryTemplateManagerBackendOptions } from './in-memory-template-manager-backend.js';
|
|
12
|
+
export { InMemoryTemplateManagerBackend } from './in-memory-template-manager-backend.js';
|
|
13
|
+
export type { ManagedEmailTemplateContent, ManagedTemplateRendererOptions, ManagedTemplateRenderResult, TextTemplate, TextTemplateContent, VersionPinnedNotification, } from './managed-template-renderer.js';
|
|
14
|
+
export { ManagedTemplateEmailRenderer, ManagedTemplateRenderer, ManagedTemplateTextRenderer, requestedTemplateVersion, } from './managed-template-renderer.js';
|
|
15
|
+
export type { ManagedTemplateServiceOptions } from './managed-template-service.js';
|
|
16
|
+
export { DEFAULT_STATUS_TRANSITIONS, ManagedTemplateService } from './managed-template-service.js';
|
|
17
|
+
export { MAX_SLUG_LENGTH, nextAvailableSlug, normalizeTagText, slugifyTag } from './tags.js';
|
|
18
|
+
export type { ManagedTemplate, ManagedTemplateCreateInput, ManagedTemplateId, ManagedTemplateStatusHistory, ManagedTemplateTag, ManagedTemplateUpdateInput, } from './types.js';
|
|
19
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +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;AACzF,YAAY,EACV,2BAA2B,EAC3B,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
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Composition
|
|
2
|
+
export { DEFAULT_MAX_DEPTH, DEFAULT_TAG_PREFIX, isAbstract, TemplateComposer, } from './composition.js';
|
|
3
|
+
// Constants
|
|
4
|
+
export { MANAGED_TEMPLATE_STATUSES, MANAGED_TEMPLATE_TAG_STATUSES, MOST_RECENT_ACTIVE_VERSION_STATUSES, TEMPLATE_FIELDS, } from './constants.js';
|
|
5
|
+
// Errors
|
|
6
|
+
export { isNotFoundError, ManagedTemplateChangeUserNotFoundError, ManagedTemplateCompositionCycleError, ManagedTemplateCompositionDepthError, ManagedTemplateCompositionError, ManagedTemplateCompositionReferenceError, ManagedTemplateCompositionSyntaxError, ManagedTemplateError, ManagedTemplateInvalidFilterError, ManagedTemplateInvalidTagError, ManagedTemplateNotFoundError, ManagedTemplateStatusTransitionError, ManagedTemplateTagAlreadyExistsError, ManagedTemplateTagNotFoundError, ManagedTemplateUnsupportedOrderingError, } from './errors.js';
|
|
7
|
+
// Filter evaluation, for a backend that has to finish a filter its query language cannot express
|
|
8
|
+
export { isMostRecentActiveVersion, matchesDateRange, matchesInteger, matchesStatus, matchesString, matchesTemplateFilter, normalizeSlugs, paginate, sortTemplates, } from './filter-evaluation.js';
|
|
9
|
+
// Filters and capabilities
|
|
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
|
+
// A backend to develop and test against
|
|
12
|
+
export { InMemoryTemplateManagerBackend } from './in-memory-template-manager-backend.js';
|
|
13
|
+
// Renderers
|
|
14
|
+
export { ManagedTemplateEmailRenderer, ManagedTemplateRenderer, ManagedTemplateTextRenderer, requestedTemplateVersion, } from './managed-template-renderer.js';
|
|
15
|
+
// The service
|
|
16
|
+
export { DEFAULT_STATUS_TRANSITIONS, ManagedTemplateService } from './managed-template-service.js';
|
|
17
|
+
// Tag slugging
|
|
18
|
+
export { MAX_SLUG_LENGTH, nextAvailableSlug, normalizeTagText, slugifyTag } from './tags.js';
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The renderer that feeds a stored template to an ordinary VintaSend renderer.
|
|
3
|
+
*
|
|
4
|
+
* `ManagedTemplateRenderer` wraps another renderer and swaps out where the template comes from:
|
|
5
|
+
* instead of a path an engine's loader resolves, the notification's `bodyTemplate` is a key this
|
|
6
|
+
* package's storage seam looks up. What the inner renderer receives is template *source*, so it
|
|
7
|
+
* has to implement `renderFromTemplateContent` — which every renderer in the VintaSend ecosystem
|
|
8
|
+
* does, because that is the seam VintaSend already uses to render content it holds rather than
|
|
9
|
+
* loads.
|
|
10
|
+
*
|
|
11
|
+
* Templates are composed before they reach the inner renderer. A stored template can extend a
|
|
12
|
+
* base and include shared fragments (see `composition`), and none of that survives into what the
|
|
13
|
+
* engine sees: it gets one flat string. Composition is on by default and can be turned off per
|
|
14
|
+
* renderer with `composeTemplates: false`, which is the right call only if a store predates
|
|
15
|
+
* composition and holds `managed_`-prefixed text meant to be passed through.
|
|
16
|
+
*/
|
|
17
|
+
import type { AnyNotification, BaseLogger, BaseNotificationTemplateRenderer, BaseNotificationTypeConfig, EmailTemplate, EmailTemplateContent, JsonObject } from 'vintasend';
|
|
18
|
+
import type { BaseTemplateManagerBackend } from './base-template-manager-backend.js';
|
|
19
|
+
import { TemplateComposer, type TemplateComposerOptions } from './composition.js';
|
|
20
|
+
import type { ManagedTemplate } from './types.js';
|
|
21
|
+
/**
|
|
22
|
+
* A notification that names which version of its template it was recorded against.
|
|
23
|
+
*
|
|
24
|
+
* VintaSend declares `requestedTemplateVersion` on its own notification types, so this is only
|
|
25
|
+
* the shape {@link requestedTemplateVersion} needs — kept exported for a host with a notification
|
|
26
|
+
* type of its own, and for reading a pin off a record that came from somewhere else.
|
|
27
|
+
*/
|
|
28
|
+
export type VersionPinnedNotification = {
|
|
29
|
+
requestedTemplateVersion?: number | null;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Read a notification's template-version pin, if it carries one.
|
|
33
|
+
*
|
|
34
|
+
* Takes `unknown` rather than a notification type because it is also pointed at records that
|
|
35
|
+
* predate the field — a backend that never stored it hands back a notification with nothing
|
|
36
|
+
* there, and `null` is the right answer for those rather than a type error.
|
|
37
|
+
*/
|
|
38
|
+
export declare function requestedTemplateVersion(notification: unknown): number | null;
|
|
39
|
+
/**
|
|
40
|
+
* What rendering a managed template produced, and which version produced it.
|
|
41
|
+
*
|
|
42
|
+
* `render` also stamps the version onto the rendered payload itself, which is how VintaSend's
|
|
43
|
+
* service picks it up — see {@link ManagedTemplateRenderer.render}. This richer result is for a
|
|
44
|
+
* caller driving the render directly, where reading a documented field beats fishing an optional
|
|
45
|
+
* one off the payload.
|
|
46
|
+
*/
|
|
47
|
+
export type ManagedTemplateRenderResult<RenderedType> = {
|
|
48
|
+
key: string;
|
|
49
|
+
version: number;
|
|
50
|
+
rendered: RenderedType;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* The email content a managed template produces.
|
|
54
|
+
*
|
|
55
|
+
* `EmailTemplateContent` as VintaSend defines it, plus the preheader managed templates carry.
|
|
56
|
+
* A renderer that knows about preheaders can read it; one that does not ignores the extra field,
|
|
57
|
+
* which is why it is added rather than replacing the shape.
|
|
58
|
+
*/
|
|
59
|
+
export type ManagedEmailTemplateContent = EmailTemplateContent & {
|
|
60
|
+
preheader: string | null;
|
|
61
|
+
};
|
|
62
|
+
export type ManagedTemplateRendererOptions = {
|
|
63
|
+
/**
|
|
64
|
+
* When true (the default), `managed_*` inheritance and inclusion tags are resolved before the
|
|
65
|
+
* inner renderer sees the template.
|
|
66
|
+
*/
|
|
67
|
+
composeTemplates?: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* The composer to resolve them with. Defaults to one reading through the template manager
|
|
70
|
+
* backend; pass your own to change the tag prefix or the depth limit.
|
|
71
|
+
*/
|
|
72
|
+
composer?: TemplateComposer;
|
|
73
|
+
/** Options for the default composer. Ignored when `composer` is given. */
|
|
74
|
+
composerOptions?: TemplateComposerOptions;
|
|
75
|
+
};
|
|
76
|
+
export declare abstract class ManagedTemplateRenderer<Config extends BaseNotificationTypeConfig, RenderedType, ContentType> implements BaseNotificationTemplateRenderer<Config, RenderedType> {
|
|
77
|
+
readonly managerBackend: BaseTemplateManagerBackend;
|
|
78
|
+
readonly renderer: BaseNotificationTemplateRenderer<Config, RenderedType>;
|
|
79
|
+
logger: BaseLogger | null;
|
|
80
|
+
readonly composeTemplates: boolean;
|
|
81
|
+
readonly composer: TemplateComposer;
|
|
82
|
+
constructor(managerBackend: BaseTemplateManagerBackend, renderer: BaseNotificationTemplateRenderer<Config, RenderedType>, options?: ManagedTemplateRendererOptions);
|
|
83
|
+
injectLogger(logger: BaseLogger): void;
|
|
84
|
+
/** Build the inner renderer's template content from a stored template. */
|
|
85
|
+
abstract createTemplateContent(template: ManagedTemplate): ContentType;
|
|
86
|
+
/**
|
|
87
|
+
* Resolve a template's composition tags, unless this renderer was told not to.
|
|
88
|
+
*
|
|
89
|
+
* @throws ManagedTemplateCompositionError if the template cannot be assembled.
|
|
90
|
+
*/
|
|
91
|
+
compose(template: ManagedTemplate): Promise<ManagedTemplate>;
|
|
92
|
+
/**
|
|
93
|
+
* The newest version of a stored template, for a host to pin a notification to.
|
|
94
|
+
*
|
|
95
|
+
* Answers with whatever version the backend considers current for that key, which is the same
|
|
96
|
+
* version `render` would resolve to if the notification were left unpinned. A key with nothing
|
|
97
|
+
* behind it answers `null` rather than throwing: a missing template is the send's problem to
|
|
98
|
+
* report, and failing here would fail the *creation* of a notification over a template that
|
|
99
|
+
* might well exist by the time it is sent.
|
|
100
|
+
*/
|
|
101
|
+
getLatestTemplateVersion(templateKey: string): Promise<number | null>;
|
|
102
|
+
renderFromTemplateContent(notification: AnyNotification<Config>, templateContent: ContentType, context: JsonObject): Promise<RenderedType>;
|
|
103
|
+
/**
|
|
104
|
+
* Render a notification against a template already in hand, with no backend read.
|
|
105
|
+
*
|
|
106
|
+
* The template is composed first, so one already fetched and edited in memory renders the same
|
|
107
|
+
* way a stored one does.
|
|
108
|
+
*/
|
|
109
|
+
renderTemplate(notification: AnyNotification<Config>, template: ManagedTemplate, context: JsonObject): Promise<ManagedTemplateRenderResult<RenderedType>>;
|
|
110
|
+
/**
|
|
111
|
+
* Render a notification against a specific version of its template, reporting which version
|
|
112
|
+
* was used.
|
|
113
|
+
*
|
|
114
|
+
* The notification's `bodyTemplate` is the template key. Which version renders is decided in
|
|
115
|
+
* this order: the `version` argument, then the notification's own `requestedTemplateVersion`,
|
|
116
|
+
* then whatever the backend considers current.
|
|
117
|
+
*
|
|
118
|
+
* The argument is there to render a version the notification is *not* pinned to — previewing
|
|
119
|
+
* an unpublished draft, or reproducing what an old notification looked like. Leave it off and
|
|
120
|
+
* this renders what a real send would.
|
|
121
|
+
*/
|
|
122
|
+
renderManaged(notification: AnyNotification<Config>, context: JsonObject, version?: number | null): Promise<ManagedTemplateRenderResult<RenderedType>>;
|
|
123
|
+
/**
|
|
124
|
+
* Render a notification against the version it is pinned to, or the current one.
|
|
125
|
+
*
|
|
126
|
+
* This is the `BaseNotificationTemplateRenderer` seam VintaSend itself calls, so the rendered
|
|
127
|
+
* payload is all it can return — and the version that produced it is stamped onto that payload
|
|
128
|
+
* as `templateVersion`. That is the channel VintaSend reads: an adapter returns the payload from
|
|
129
|
+
* `send()`, and the service records the version on the notification as `usedTemplateVersion`.
|
|
130
|
+
* On an unpinned notification it is the only record of which version went out, since the
|
|
131
|
+
* template has moved on by the time anyone asks.
|
|
132
|
+
*
|
|
133
|
+
* Call {@link renderManaged} instead when driving the render yourself and the version matters.
|
|
134
|
+
*/
|
|
135
|
+
render(notification: AnyNotification<Config>, context: JsonObject): Promise<RenderedType>;
|
|
136
|
+
}
|
|
137
|
+
/** A managed-template renderer for email, feeding subject, body and preheader downstream. */
|
|
138
|
+
export declare class ManagedTemplateEmailRenderer<Config extends BaseNotificationTypeConfig> extends ManagedTemplateRenderer<Config, EmailTemplate, ManagedEmailTemplateContent> {
|
|
139
|
+
createTemplateContent(template: ManagedTemplate): ManagedEmailTemplateContent;
|
|
140
|
+
}
|
|
141
|
+
/** The rendered payload of a text-only channel, as VintaSend's text renderers produce it. */
|
|
142
|
+
export type TextTemplate = {
|
|
143
|
+
text: string;
|
|
144
|
+
};
|
|
145
|
+
/** The content a text-only renderer is fed. */
|
|
146
|
+
export type TextTemplateContent = {
|
|
147
|
+
text: string;
|
|
148
|
+
};
|
|
149
|
+
/** A managed-template renderer for SMS and other text-only channels. */
|
|
150
|
+
export declare class ManagedTemplateTextRenderer<Config extends BaseNotificationTypeConfig> extends ManagedTemplateRenderer<Config, TextTemplate, TextTemplateContent> {
|
|
151
|
+
createTemplateContent(template: ManagedTemplate): TextTemplateContent;
|
|
152
|
+
}
|
|
153
|
+
//# sourceMappingURL=managed-template-renderer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"managed-template-renderer.d.ts","sourceRoot":"","sources":["../src/managed-template-renderer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The renderer that feeds a stored template to an ordinary VintaSend renderer.
|
|
3
|
+
*
|
|
4
|
+
* `ManagedTemplateRenderer` wraps another renderer and swaps out where the template comes from:
|
|
5
|
+
* instead of a path an engine's loader resolves, the notification's `bodyTemplate` is a key this
|
|
6
|
+
* package's storage seam looks up. What the inner renderer receives is template *source*, so it
|
|
7
|
+
* has to implement `renderFromTemplateContent` — which every renderer in the VintaSend ecosystem
|
|
8
|
+
* does, because that is the seam VintaSend already uses to render content it holds rather than
|
|
9
|
+
* loads.
|
|
10
|
+
*
|
|
11
|
+
* Templates are composed before they reach the inner renderer. A stored template can extend a
|
|
12
|
+
* base and include shared fragments (see `composition`), and none of that survives into what the
|
|
13
|
+
* engine sees: it gets one flat string. Composition is on by default and can be turned off per
|
|
14
|
+
* renderer with `composeTemplates: false`, which is the right call only if a store predates
|
|
15
|
+
* composition and holds `managed_`-prefixed text meant to be passed through.
|
|
16
|
+
*/
|
|
17
|
+
import { TemplateComposer } from './composition.js';
|
|
18
|
+
import { ManagedTemplateNotFoundError } from './errors.js';
|
|
19
|
+
/**
|
|
20
|
+
* Read a notification's template-version pin, if it carries one.
|
|
21
|
+
*
|
|
22
|
+
* Takes `unknown` rather than a notification type because it is also pointed at records that
|
|
23
|
+
* predate the field — a backend that never stored it hands back a notification with nothing
|
|
24
|
+
* there, and `null` is the right answer for those rather than a type error.
|
|
25
|
+
*/
|
|
26
|
+
export function requestedTemplateVersion(notification) {
|
|
27
|
+
const pin = notification?.requestedTemplateVersion;
|
|
28
|
+
return typeof pin === 'number' ? pin : null;
|
|
29
|
+
}
|
|
30
|
+
export class ManagedTemplateRenderer {
|
|
31
|
+
constructor(managerBackend, renderer, options = {}) {
|
|
32
|
+
this.managerBackend = managerBackend;
|
|
33
|
+
this.renderer = renderer;
|
|
34
|
+
this.logger = null;
|
|
35
|
+
this.composeTemplates = options.composeTemplates ?? true;
|
|
36
|
+
this.composer =
|
|
37
|
+
options.composer ?? TemplateComposer.fromBackend(managerBackend, options.composerOptions);
|
|
38
|
+
}
|
|
39
|
+
injectLogger(logger) {
|
|
40
|
+
this.logger = logger;
|
|
41
|
+
this.renderer.injectLogger?.(logger);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Resolve a template's composition tags, unless this renderer was told not to.
|
|
45
|
+
*
|
|
46
|
+
* @throws ManagedTemplateCompositionError if the template cannot be assembled.
|
|
47
|
+
*/
|
|
48
|
+
async compose(template) {
|
|
49
|
+
if (!this.composeTemplates) {
|
|
50
|
+
return template;
|
|
51
|
+
}
|
|
52
|
+
return this.composer.compose(template);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The newest version of a stored template, for a host to pin a notification to.
|
|
56
|
+
*
|
|
57
|
+
* Answers with whatever version the backend considers current for that key, which is the same
|
|
58
|
+
* version `render` would resolve to if the notification were left unpinned. A key with nothing
|
|
59
|
+
* behind it answers `null` rather than throwing: a missing template is the send's problem to
|
|
60
|
+
* report, and failing here would fail the *creation* of a notification over a template that
|
|
61
|
+
* might well exist by the time it is sent.
|
|
62
|
+
*/
|
|
63
|
+
async getLatestTemplateVersion(templateKey) {
|
|
64
|
+
try {
|
|
65
|
+
const template = await this.managerBackend.getTemplate(templateKey);
|
|
66
|
+
return template.version;
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (error instanceof ManagedTemplateNotFoundError) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
renderFromTemplateContent(notification, templateContent, context) {
|
|
76
|
+
return this.renderer.renderFromTemplateContent(notification, templateContent, context);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Render a notification against a template already in hand, with no backend read.
|
|
80
|
+
*
|
|
81
|
+
* The template is composed first, so one already fetched and edited in memory renders the same
|
|
82
|
+
* way a stored one does.
|
|
83
|
+
*/
|
|
84
|
+
async renderTemplate(notification, template, context) {
|
|
85
|
+
const content = this.createTemplateContent(await this.compose(template));
|
|
86
|
+
const rendered = await this.renderFromTemplateContent(notification, content, context);
|
|
87
|
+
return { key: template.key, version: template.version, rendered };
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Render a notification against a specific version of its template, reporting which version
|
|
91
|
+
* was used.
|
|
92
|
+
*
|
|
93
|
+
* The notification's `bodyTemplate` is the template key. Which version renders is decided in
|
|
94
|
+
* this order: the `version` argument, then the notification's own `requestedTemplateVersion`,
|
|
95
|
+
* then whatever the backend considers current.
|
|
96
|
+
*
|
|
97
|
+
* The argument is there to render a version the notification is *not* pinned to — previewing
|
|
98
|
+
* an unpublished draft, or reproducing what an old notification looked like. Leave it off and
|
|
99
|
+
* this renders what a real send would.
|
|
100
|
+
*/
|
|
101
|
+
async renderManaged(notification, context, version = null) {
|
|
102
|
+
const resolved = version ?? requestedTemplateVersion(notification);
|
|
103
|
+
const template = await this.managerBackend.getTemplate(notification.bodyTemplate, resolved);
|
|
104
|
+
return this.renderTemplate(notification, template, context);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Render a notification against the version it is pinned to, or the current one.
|
|
108
|
+
*
|
|
109
|
+
* This is the `BaseNotificationTemplateRenderer` seam VintaSend itself calls, so the rendered
|
|
110
|
+
* payload is all it can return — and the version that produced it is stamped onto that payload
|
|
111
|
+
* as `templateVersion`. That is the channel VintaSend reads: an adapter returns the payload from
|
|
112
|
+
* `send()`, and the service records the version on the notification as `usedTemplateVersion`.
|
|
113
|
+
* On an unpinned notification it is the only record of which version went out, since the
|
|
114
|
+
* template has moved on by the time anyone asks.
|
|
115
|
+
*
|
|
116
|
+
* Call {@link renderManaged} instead when driving the render yourself and the version matters.
|
|
117
|
+
*/
|
|
118
|
+
async render(notification, context) {
|
|
119
|
+
const result = await this.renderManaged(notification, context);
|
|
120
|
+
return withTemplateVersion(result.rendered, result.version);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Stamp the version that rendered onto the payload, without mutating what the inner renderer
|
|
125
|
+
* returned.
|
|
126
|
+
*
|
|
127
|
+
* A renderer producing something that is not an object — nothing shipped does, but the seam is
|
|
128
|
+
* generic — is handed back untouched rather than wrapped, since there is nowhere to put the field
|
|
129
|
+
* and losing the payload would be the worse trade.
|
|
130
|
+
*/
|
|
131
|
+
function withTemplateVersion(rendered, version) {
|
|
132
|
+
if (rendered === null || typeof rendered !== 'object') {
|
|
133
|
+
return rendered;
|
|
134
|
+
}
|
|
135
|
+
return { ...rendered, templateVersion: version };
|
|
136
|
+
}
|
|
137
|
+
/** A managed-template renderer for email, feeding subject, body and preheader downstream. */
|
|
138
|
+
export class ManagedTemplateEmailRenderer extends ManagedTemplateRenderer {
|
|
139
|
+
createTemplateContent(template) {
|
|
140
|
+
return {
|
|
141
|
+
subject: template.subjectTemplate,
|
|
142
|
+
body: template.bodyTemplate,
|
|
143
|
+
preheader: template.preheaderTemplate,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/** A managed-template renderer for SMS and other text-only channels. */
|
|
148
|
+
export class ManagedTemplateTextRenderer extends ManagedTemplateRenderer {
|
|
149
|
+
createTemplateContent(template) {
|
|
150
|
+
return { text: template.bodyTemplate };
|
|
151
|
+
}
|
|
152
|
+
}
|