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.
Files changed (38) hide show
  1. package/README.md +529 -0
  2. package/dist/base-template-manager-backend.d.ts +202 -0
  3. package/dist/base-template-manager-backend.d.ts.map +1 -0
  4. package/dist/base-template-manager-backend.js +1 -0
  5. package/dist/composition.d.ts +238 -0
  6. package/dist/composition.d.ts.map +1 -0
  7. package/dist/composition.js +0 -0
  8. package/dist/constants.d.ts +32 -0
  9. package/dist/constants.d.ts.map +1 -0
  10. package/dist/constants.js +31 -0
  11. package/dist/errors.d.ts +80 -0
  12. package/dist/errors.d.ts.map +1 -0
  13. package/dist/errors.js +86 -0
  14. package/dist/filter-evaluation.d.ts +69 -0
  15. package/dist/filter-evaluation.d.ts.map +1 -0
  16. package/dist/filter-evaluation.js +252 -0
  17. package/dist/filters.d.ts +192 -0
  18. package/dist/filters.d.ts.map +1 -0
  19. package/dist/filters.js +252 -0
  20. package/dist/in-memory-template-manager-backend.d.ts +85 -0
  21. package/dist/in-memory-template-manager-backend.d.ts.map +1 -0
  22. package/dist/in-memory-template-manager-backend.js +323 -0
  23. package/dist/index.d.ts +19 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +18 -0
  26. package/dist/managed-template-renderer.d.ts +153 -0
  27. package/dist/managed-template-renderer.d.ts.map +1 -0
  28. package/dist/managed-template-renderer.js +152 -0
  29. package/dist/managed-template-service.d.ts +415 -0
  30. package/dist/managed-template-service.d.ts.map +1 -0
  31. package/dist/managed-template-service.js +712 -0
  32. package/dist/tags.d.ts +54 -0
  33. package/dist/tags.d.ts.map +1 -0
  34. package/dist/tags.js +108 -0
  35. package/dist/types.d.ts +100 -0
  36. package/dist/types.d.ts.map +1 -0
  37. package/dist/types.js +1 -0
  38. package/package.json +41 -0
@@ -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
+ }