import { createHash } from "crypto"; import { prisma } from "~/db.server"; import { BaseService, ServiceValidationError } from "./baseService.server"; import { normalizePromptOverrideSource } from "./promptOverrideSource"; export class PromptService extends BaseService { async promoteVersion(promptId: string, versionId: string, options?: { sourceGuard?: boolean }) { const target = await this._prisma.promptVersion.findUnique({ where: { id: versionId }, }); if (!target) { throw new ServiceValidationError("Version not found", 404); } if (target.promptId !== promptId) { throw new ServiceValidationError("Version does not belong to this prompt", 400); } if (options?.sourceGuard && target.source !== "code") { throw new ServiceValidationError( "Only code-sourced versions can be promoted. Use the override API instead.", 400 ); } await prisma.$transaction(async (tx) => { await tx.$executeRaw` UPDATE "prompt_versions" SET "labels" = array_remove("labels", 'current') WHERE "promptId" = ${promptId} AND 'current' = ANY("labels") `; await tx.$executeRaw` UPDATE "prompt_versions" SET "labels" = array_append("labels", 'current') WHERE "id" = ${versionId} AND NOT ('current' = ANY("labels")) `; }); } async createOverride( promptId: string, data: { textContent: string; model?: string; commitMessage?: string; source?: string; createdBy?: string; } ) { const contentHash = createHash("sha256").update(data.textContent).digest("hex").slice(0, 16); const result = await prisma.$transaction(async (tx) => { const latest = await tx.promptVersion.findFirst({ where: { promptId }, orderBy: { version: "desc" }, }); const nextVersion = (latest?.version ?? 0) + 1; await tx.$executeRaw` UPDATE "prompt_versions" SET "labels" = array_remove("labels", 'override') WHERE "promptId" = ${promptId} AND 'override' = ANY("labels") `; // Defence in depth: reject the reserved `code` source regardless of // caller, in case a future caller skips the route-layer check. const safeSource = normalizePromptOverrideSource(data.source); await tx.promptVersion.create({ data: { promptId, version: nextVersion, textContent: data.textContent, model: data.model || null, source: safeSource, commitMessage: data.commitMessage || null, contentHash, labels: ["override"], createdBy: data.createdBy, }, }); return { version: nextVersion }; }); return result; } async updateOverride( promptId: string, data: { textContent?: string; model?: string; commitMessage?: string; } ) { const overrideVer = await this._prisma.promptVersion.findFirst({ where: { promptId, labels: { has: "override" } }, orderBy: { version: "desc" }, }); if (!overrideVer) { throw new ServiceValidationError("No active override to update", 400); } const contentString = data.textContent ?? overrideVer.textContent ?? ""; const contentHash = createHash("sha256").update(contentString).digest("hex").slice(0, 16); await this._prisma.promptVersion.update({ where: { id: overrideVer.id }, data: { textContent: contentString, model: data.model ?? overrideVer.model, commitMessage: data.commitMessage ?? overrideVer.commitMessage, contentHash, }, }); } async removeOverride(promptId: string) { await this.#removeLabel(promptId, "override"); } async reactivateOverride(promptId: string, versionId: string) { const target = await this._prisma.promptVersion.findUnique({ where: { id: versionId }, }); if (!target) { throw new ServiceValidationError("Version not found", 404); } if (target.promptId !== promptId) { throw new ServiceValidationError("Version does not belong to this prompt", 400); } if (target.source === "code") { throw new ServiceValidationError( "Code-sourced versions cannot be reactivated as overrides", 400 ); } await prisma.$transaction(async (tx) => { await tx.$executeRaw` UPDATE "prompt_versions" SET "labels" = array_remove("labels", 'override') WHERE "promptId" = ${promptId} AND 'override' = ANY("labels") `; await tx.$executeRaw` UPDATE "prompt_versions" SET "labels" = array_append("labels", 'override') WHERE "id" = ${versionId} AND NOT ('override' = ANY("labels")) `; }); } async #removeLabel(promptId: string, label: string) { await this._prisma.$executeRaw` UPDATE "prompt_versions" SET "labels" = array_remove("labels", ${label}) WHERE "promptId" = ${promptId} AND ${label} = ANY("labels") `; } }