8ce647552a
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>
50 lines
1.9 KiB
TypeScript
50 lines
1.9 KiB
TypeScript
import { Injectable, NotFoundException } from '@nestjs/common';
|
|
import { IiosMessageTemplate, IiosTemplateChannel } from '@prisma/client';
|
|
import { PrismaService } from '../prisma/prisma.service';
|
|
|
|
export class TemplateNotFoundError extends NotFoundException {
|
|
constructor(key: string, channel: string, locale: string) {
|
|
super(`no active template for key="${key}" channel=${channel} locale=${locale}`);
|
|
this.name = 'TemplateNotFoundError';
|
|
}
|
|
}
|
|
|
|
@Injectable()
|
|
export class TemplateRepository {
|
|
constructor(private readonly prisma: PrismaService) {}
|
|
|
|
/**
|
|
* Resolve the template to use: the highest active version for (key, channel, locale), preferring
|
|
* a row owned by `scopeId` (a tenant override) and falling back to the global default (scopeId
|
|
* NULL). Throws if neither exists.
|
|
*/
|
|
async resolve(
|
|
key: string,
|
|
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.pick({ key, channel, locale, scopeId }, version);
|
|
if (scoped) return scoped;
|
|
}
|
|
const global = await this.pick({ key, channel, locale, scopeId: null }, version);
|
|
if (global) return global;
|
|
throw new TemplateNotFoundError(key, channel, locale);
|
|
}
|
|
|
|
/** 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: version != null ? { ...where, version } : { ...where, active: true },
|
|
orderBy: { version: 'desc' },
|
|
});
|
|
}
|
|
}
|