Extract emails into separate package with clean interface

- Create packages/emails/ with EmailClient interface abstraction
- Wrap Postmark ServerClient in adapter for clean typing
- Add createLoggingEmailClient for dev mode (logs to console)
- Split email templates into individual files with full test coverage
- Update api-server to use new package via context injection
- Remove EMAIL_DEV_MODE - now uses POSTMARK_API_KEY presence
- Delete apps/api-server/src/utils/email.ts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
igm
2026-01-12 15:45:40 +08:00
parent f9f1dc7403
commit c2b815dd6a
34 changed files with 1626 additions and 442 deletions

View File

@@ -0,0 +1,54 @@
import { DurationFormat } from "@formatjs/intl-durationformat";
import type { OrgRole } from "@reviq/db-schema";
export function buildUrl(
baseUrl: string,
path: string,
params: Record<string, string>,
): string {
const url = new URL(path, baseUrl);
for (const [key, value] of Object.entries(params)) {
url.searchParams.set(key, value);
}
return url.toString();
}
export function escapeHtml(unsafe: string): string {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
const durationFormatter = new DurationFormat("en", { style: "long" });
export function formatExpiryHours(hours: number): string {
return durationFormatter.format({ hours });
}
export function formatExpiryMinutes(minutes: number): string {
return durationFormatter.format({ minutes });
}
export function formatExpiryDays(days: number): string {
return durationFormatter.format({ days });
}
const roleLabels: Record<OrgRole, string> = {
owner: "Owner",
admin: "Admin",
member: "Member",
};
export function formatRoleDisplay(role: OrgRole): string {
return roleLabels[role];
}
export function getArticleForRole(role: OrgRole): string {
if (role === "owner" || role === "admin") {
return "an";
}
return "a";
}