/** * Built-in template catalog (Confluence expansion T-143, decision A-005). * * "Global" templates are `isBuiltin` rows seeded into EACH workspace (the * Template.workspaceId FK is required; there is no org-level table). The * catalog below is the single source of truth โ€” the DB seed AND the live * workspace-creation path both call {@link seedBuiltinTemplatesForWorkspace}, * so a new workspace gets the full catalog with no extra migration. * * Snapshots use the canonical uppercase block types + the same shape as * `PageService.captureSubtree` (page + blocks + children), so * `applyTemplate` instantiates them unchanged. */ type TplBlock = { id: string; type: string; content: Record; properties: Record; parentId: string | null; position: string }; type TplChild = { title: string; blocks: TplBlock[] }; export interface BuiltinTemplateDef { name: string; icon: string; category: string; description: string; variables?: { name: string; label: string; type: "text" | "number" | "date" | "select"; options?: string[]; required?: boolean; defaultValue?: string }[]; snapshot: { page: { title: string; icon: string | null; description: string | null }; blocks: TplBlock[]; children: TplChild[] }; } let n = 0; const bid = (p: string) => `${p}_${(n++).toString(36).padStart(2, "0")}`; const P = (id: string, text: string) => ({ id, type: "PARAGRAPH", content: { text }, properties: {}, parentId: null, position: "a0" }); const H1 = (id: string, text: string) => ({ id, type: "HEADING_1", content: { text }, properties: {}, parentId: null, position: "a0" }); const H2 = (id: string, text: string) => ({ id, type: "HEADING_2", content: { text }, properties: {}, parentId: null, position: "a0" }); const TODO = (id: string, text: string) => ({ id, type: "TODO", content: { text }, properties: {}, parentId: null, position: "a0" }); const LIST = (id: string, text: string) => ({ id, type: "BULLETED_LIST", content: { text }, properties: {}, parentId: null, position: "a0" }); const CALLOUT = (id: string, text: string) => ({ id, type: "CALLOUT", content: { text }, properties: {}, parentId: null, position: "a0" }); function page(title: string, icon: string | null, blocks: TplBlock[], children: TplChild[] = []): BuiltinTemplateDef["snapshot"] { return { page: { title, icon, description: null }, blocks, children }; } /** The 15 initial templates (spec ยง6). Positions reset per block list. */ export const BUILTIN_TEMPLATES: BuiltinTemplateDef[] = [ { name: "Meeting Notes", icon: "๐Ÿ—“๏ธ", category: "Meetings", description: "Agenda, decisions, action items for a meeting.", variables: [ { name: "title", label: "Meeting title", type: "text", required: true }, { name: "date", label: "Date", type: "date" }, { name: "attendees", label: "Attendees", type: "text" } ], snapshot: page("{{title}}", "๐Ÿ—“๏ธ", [ H1(bid("m"), "Meeting Notes"), P(bid("m"), "Date: {{date}}"), P(bid("m"), "Attendees: {{attendees}}"), H2(bid("m"), "Agenda"), LIST(bid("m"), "Topic 1"), LIST(bid("m"), "Topic 2"), H2(bid("m"), "Decisions"), P(bid("m"), "-"), H2(bid("m"), "Action items"), TODO(bid("m"), "[ ] Owner โ€” task โ€” due"), H2(bid("m"), "Notes"), P(bid("m"), "-") ]) }, { name: "Project Plan", icon: "๐Ÿ“‹", category: "Planning", description: "Goals, scope, milestones, owners, risks.", variables: [{ name: "project", label: "Project name", type: "text", required: true }], snapshot: page("{{project}} โ€” Project Plan", "๐Ÿ“‹", [ H1(bid("p"), "{{project}}"), H2(bid("p"), "Goals"), P(bid("p"), "What success looks like."), H2(bid("p"), "Scope"), P(bid("p"), "In scope / out of scope."), H2(bid("p"), "Milestones"), LIST(bid("p"), "M1 โ€” date"), LIST(bid("p"), "M2 โ€” date"), H2(bid("p"), "Owners"), P(bid("p"), "Roles and accountabilities."), H2(bid("p"), "Risks"), P(bid("p"), "Likelihood / impact / mitigation.") ]) }, { name: "Product Requirements Document", icon: "๐Ÿ“„", category: "Product", description: "PRD: problem, goals, requirements, non-goals, metrics.", variables: [{ name: "feature", label: "Feature name", type: "text", required: true }], snapshot: page("PRD: {{feature}}", "๐Ÿ“„", [ H1(bid("r"), "{{feature}}"), H2(bid("r"), "Problem"), P(bid("r"), "The problem we are solving and why now."), H2(bid("r"), "Goals & non-goals"), LIST(bid("r"), "Goal 1"), LIST(bid("r"), "Non-goal 1"), H2(bid("r"), "Requirements"), TODO(bid("r"), "[ ] Must-have 1"), TODO(bid("r"), "[ ] Nice-to-have 1"), H2(bid("r"), "Success metrics"), P(bid("r"), "How we measure success."), H2(bid("r"), "Open questions"), P(bid("r"), "-") ]) }, { name: "Technical Design Document", icon: "๐Ÿ› ๏ธ", category: "Engineering", description: "System design: context, proposal, alternatives, risks.", variables: [{ name: "system", label: "System / feature", type: "text", required: true }], snapshot: page("Design: {{system}}", "๐Ÿ› ๏ธ", [ H1(bid("t"), "{{system}}"), H2(bid("t"), "Context & constraints"), P(bid("t"), "-"), H2(bid("t"), "Proposal"), P(bid("t"), "The design, data model, and API surface."), H2(bid("t"), "Alternatives considered"), P(bid("t"), "Option A / Option B and trade-offs."), H2(bid("t"), "Data & schema changes"), P(bid("t"), "Migrations, indexes, backfill."), H2(bid("t"), "Risks & rollback"), P(bid("t"), "Failure modes, monitoring, revert plan.") ]) }, { name: "Architecture Decision Record", icon: "๐Ÿ›๏ธ", category: "Engineering", description: "ADR: context, decision, consequences (MADR-style).", variables: [ { name: "decision", label: "Decision (one line)", type: "text", required: true }, { name: "status", label: "Status", type: "select", options: ["Proposed", "Accepted", "Deprecated", "Superseded"], defaultValue: "Proposed" } ], snapshot: page("ADR: {{decision}}", "๐Ÿ›๏ธ", [ H1(bid("a"), "ADR: {{decision}}"), CALLOUT(bid("a"), `Status: {{status}}`), H2(bid("a"), "Context"), P(bid("a"), "The problem and the forces at play."), H2(bid("a"), "Decision"), P(bid("a"), "We will โ€ฆ"), H2(bid("a"), "Consequences"), P(bid("a"), "Positive / negative / neutral outcomes.") ]) }, { name: "Standard Operating Procedure", icon: "๐Ÿ“‘", category: "Operations", description: "Repeatable SOP: purpose, scope, steps, owners.", variables: [{ name: "task", label: "Procedure for", type: "text", required: true }], snapshot: page("SOP: {{task}}", "๐Ÿ“‘", [ H1(bid("s"), "SOP: {{task}}"), H2(bid("s"), "Purpose & scope"), P(bid("s"), "-"), H2(bid("s"), "Prerequisites"), LIST(bid("s"), "Access / tools required"), H2(bid("s"), "Steps"), TODO(bid("s"), "[ ] 1. "), TODO(bid("s"), "[ ] 2. "), TODO(bid("s"), "[ ] 3. "), H2(bid("s"), "Owners & escalation"), P(bid("s"), "-") ]) }, { name: "Incident Report", icon: "๐Ÿšจ", category: "Incident Management", description: "Live incident: status, impact, timeline, comms.", variables: [ { name: "title", label: "Incident title", type: "text", required: true }, { name: "status", label: "Status", type: "select", options: ["Investigating", "Identified", "Monitoring", "Resolved"], defaultValue: "Investigating" } ], snapshot: page("{{title}}", "๐Ÿšจ", [ H1(bid("i"), "{{title}}"), CALLOUT(bid("i"), `Status: {{status}}`), H2(bid("i"), "Impact"), P(bid("i"), "Who/what is affected and how."), H2(bid("i"), "Timeline"), LIST(bid("i"), "HH:MM โ€” event"), H2(bid("i"), "Actions taken"), LIST(bid("i"), "-"), H2(bid("i"), "Communications"), P(bid("i"), "Customer / stakeholder updates.") ]) }, { name: "Postmortem", icon: "๐Ÿ”", category: "Incident Management", description: "Blameless postmortem: summary, timeline, root cause, actions.", variables: [{ name: "incident", label: "Incident reference", type: "text", required: true }], snapshot: page("Postmortem: {{incident}}", "๐Ÿ”", [ H1(bid("pm"), "Postmortem: {{incident}}"), H2(bid("pm"), "Summary"), P(bid("pm"), "What happened, in one paragraph."), H2(bid("pm"), "Timeline"), LIST(bid("pm"), "HH:MM โ€” event"), H2(bid("pm"), "Root cause"), P(bid("pm"), "Why it happened (5 whys)."), H2(bid("pm"), "Detection & response"), P(bid("pm"), "How we found it and how we responded."), H2(bid("pm"), "Action items"), TODO(bid("pm"), "[ ] Owner โ€” action โ€” due"), H2(bid("pm"), "What went well / poorly"), P(bid("pm"), "-") ]) }, { name: "Troubleshooting Guide", icon: "๐Ÿฉบ", category: "Operations", description: "Symptom โ†’ cause โ†’ fix runbook.", variables: [{ name: "area", label: "System / area", type: "text", required: true }], snapshot: page("Troubleshooting: {{area}}", "๐Ÿฉบ", [ H1(bid("tr"), "Troubleshooting: {{area}}"), H2(bid("tr"), "Symptoms"), LIST(bid("tr"), "-"), H2(bid("tr"), "Diagnosis"), P(bid("tr"), "Commands / checks to run."), H2(bid("tr"), "Known fixes"), LIST(bid("tr"), "-"), H2(bid("tr"), "Escalation"), P(bid("tr"), "When and to whom to escalate.") ]) }, { name: "Release Notes", icon: "๐Ÿ“ฆ", category: "Product", description: "Customer-facing release: highlights, changes, fixes.", variables: [ { name: "version", label: "Version", type: "text", required: true }, { name: "date", label: "Release date", type: "date" } ], snapshot: page("Release {{version}}", "๐Ÿ“ฆ", [ H1(bid("rn"), "Release {{version}}"), P(bid("rn"), "Released {{date}}"), H2(bid("rn"), "Highlights"), LIST(bid("rn"), "-"), H2(bid("rn"), "New features"), LIST(bid("rn"), "-"), H2(bid("rn"), "Improvements & fixes"), LIST(bid("rn"), "-"), H2(bid("rn"), "Known issues"), P(bid("rn"), "-") ]) }, { name: "Retrospective", icon: "๐Ÿ”„", category: "Meetings", description: "What went well / poorly / actions for a team retro.", variables: [{ name: "sprint", label: "Sprint / period", type: "text", required: true }], snapshot: page("Retro: {{sprint}}", "๐Ÿ”„", [ H1(bid("ret"), "Retrospective โ€” {{sprint}}"), H2(bid("ret"), "What went well"), LIST(bid("ret"), "-"), H2(bid("ret"), "What could improve"), LIST(bid("ret"), "-"), H2(bid("ret"), "Action items"), TODO(bid("ret"), "[ ] Owner โ€” action โ€” due"), H2(bid("ret"), "Parking lot"), P(bid("ret"), "-") ]) }, { name: "Weekly Status", icon: "๐Ÿ“Š", category: "Planning", description: "Weekly status: progress, blockers, next week.", variables: [{ name: "team", label: "Team / project", type: "text", required: true }], snapshot: page("Weekly Status โ€” {{team}}", "๐Ÿ“Š", [ H1(bid("ws"), "Weekly Status โ€” {{team}}"), H2(bid("ws"), "Progress this week"), LIST(bid("ws"), "-"), H2(bid("ws"), "Blockers"), P(bid("ws"), "-"), H2(bid("ws"), "Next week"), LIST(bid("ws"), "-"), H2(bid("ws"), "Risks"), P(bid("ws"), "-") ]) }, { name: "Onboarding Guide", icon: "๐Ÿงญ", category: "People", description: "New joiner checklist: setup, accounts, first week.", variables: [{ name: "name", label: "New hire name", type: "text", required: true }], snapshot: page("Onboarding: {{name}}", "๐Ÿงญ", [ H1(bid("on"), "Onboarding: {{name}}"), H2(bid("on"), "Setup (day 1)"), TODO(bid("on"), "[ ] Accounts & access"), TODO(bid("on"), "[ ] Devices & tools"), H2(bid("on"), "First week"), TODO(bid("on"), "[ ] Meet the team"), TODO(bid("on"), "[ ] Read the docs"), H2(bid("on"), "30/60/90 goals"), P(bid("on"), "-") ]) }, { name: "Knowledge Base Article", icon: "๐Ÿ“š", category: "Knowledge", description: "KB article: overview, details, related, owner.", variables: [ { name: "topic", label: "Topic", type: "text", required: true }, { name: "owner", label: "Owner", type: "text" } ], snapshot: page("{{topic}}", "๐Ÿ“š", [ H1(bid("kb"), "{{topic}}"), P(bid("kb"), `Owner: {{owner}}`), H2(bid("kb"), "Overview"), P(bid("kb"), "What this covers and who it is for."), H2(bid("kb"), "Details"), P(bid("kb"), "-"), H2(bid("kb"), "Related"), LIST(bid("kb"), "-") ]) }, { name: "API Documentation", icon: "๐Ÿ”Œ", category: "Engineering", description: "API reference: endpoints, params, errors, examples.", variables: [{ name: "service", label: "Service name", type: "text", required: true }], snapshot: page("API: {{service}}", "๐Ÿ”Œ", [ H1(bid("api"), "API: {{service}}"), H2(bid("api"), "Base URL & auth"), P(bid("api"), "-"), H2(bid("api"), "Endpoints"), LIST(bid("api"), "GET /โ€ฆ"), LIST(bid("api"), "POST /โ€ฆ"), H2(bid("api"), "Parameters"), P(bid("api"), "Query / body fields."), H2(bid("api"), "Errors"), P(bid("api"), "Codes and meanings."), H2(bid("api"), "Examples"), P(bid("api"), "Request/response samples.") ]) } ]; /** * Idempotently seed the built-in catalog into a workspace. Keyed on * (workspaceId, name, isBuiltin) โ€” re-running is a no-op for existing rows * and adds any new catalog entries (catalog is forward-additive). * * `prisma` is a structural interface so this is unit-testable without the * generated client; the caller passes the real PrismaService/client. */ export interface BuiltinTemplatePrisma { template: { findMany(args: object): Promise<{ name: string }[]>; create(args: object): Promise; }; } export async function seedBuiltinTemplatesForWorkspace( prisma: BuiltinTemplatePrisma, workspaceId: string, createdBy?: string ): Promise<{ created: number; total: number }> { // Idempotent: keyed on (workspaceId, name) among builtins. Re-running is a // no-op for existing rows and adds any new catalog entries (forward-additive). const existing = await prisma.template.findMany({ where: { workspaceId, isBuiltin: true } }); const have = new Set(existing.map((r) => r.name)); let created = 0; for (const def of BUILTIN_TEMPLATES) { if (have.has(def.name)) continue; await prisma.template.create({ data: { workspaceId, name: def.name, icon: def.icon, type: "page", isBuiltin: true, category: def.category, description: def.description, variables: def.variables ? (def.variables as object) : undefined, snapshot: def.snapshot as object, createdBy } }); created++; } return { created, total: BUILTIN_TEMPLATES.length }; }