feat(templates): T3 render() service (resolve+render, inline, version pin)

TemplateService.render(source, vars, opts) → { content, provenance }. Stored
key → resolve+render; inline → render without a DB row (key/version null);
explicit version pins. Provenance = {templateKey, version, locale, sha256(content)}.
10 tests (svc + repo).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-18 12:25:27 +05:30
parent b3afd44d00
commit 8ce647552a
4 changed files with 137 additions and 4 deletions
@@ -23,19 +23,26 @@ export class TemplateRepository {
channel: IiosTemplateChannel,
locale = 'en',
scopeId?: string,
version?: number,
): Promise<IiosMessageTemplate> {
// Scoped override first, then the global default — for both the pinned and highest-active paths.
if (scopeId) {
const scoped = await this.highest({ key, channel, locale, scopeId });
const scoped = await this.pick({ key, channel, locale, scopeId }, version);
if (scoped) return scoped;
}
const global = await this.highest({ key, channel, locale, scopeId: null });
const global = await this.pick({ key, channel, locale, scopeId: null }, version);
if (global) return global;
throw new TemplateNotFoundError(key, channel, locale);
}
private highest(where: { key: string; channel: IiosTemplateChannel; locale: string; scopeId: string | null }) {
/** A pinned `version` fetches that exact version (the caller was explicit); otherwise the highest
* active version — a retracted (inactive) newest version falls back to the last good one. */
private pick(
where: { key: string; channel: IiosTemplateChannel; locale: string; scopeId: string | null },
version?: number,
) {
return this.prisma.iiosMessageTemplate.findFirst({
where: { ...where, active: true },
where: version != null ? { ...where, version } : { ...where, active: true },
orderBy: { version: 'desc' },
});
}