import { ConvexError, v } from "convex/values"; import type { Doc } from "./_generated/dataModel"; import { internalMutation, internalQuery } from "./_generated/server"; const artifactKind = v.union( v.literal("project_setup"), v.literal("summary"), v.literal("plan"), v.literal("preview"), v.literal("blocker"), v.literal("report") ); const buildStatus = v.union( v.literal("pending"), v.literal("building"), v.literal("ready"), v.literal("failed") ); /** * Create an ArtifactBuild keyed idempotently by * `flow::artifact::revision:` (per contract). The * build records the typed source manifest; the rendered artifact + storage * upload are linked via {@link markReady}. */ export const create = internalMutation({ args: { artifactKind, flowRunId: v.optional(v.id("flowRuns")), idempotencyKey: v.string(), organizationId: v.id("organizations"), projectId: v.optional(v.id("projects")), sourceManifest: v.any(), threadId: v.optional(v.id("threads")), workId: v.optional(v.id("works")), }, handler: async (ctx, args): Promise> => { // Idempotency: reuse an existing build for the same flow run + kind. if (args.flowRunId) { const existing = await ctx.db .query("artifactBuilds") .withIndex("by_organizationId_and_flow_run", (q) => q .eq("organizationId", args.organizationId) .eq("flowRunId", args.flowRunId) ) .filter((q) => q.eq(q.field("artifactKind"), args.artifactKind)) .unique(); if (existing) { return existing; } } const timestamp = Date.now(); const buildId = await ctx.db.insert("artifactBuilds", { artifactKind: args.artifactKind, createdAt: timestamp, flowRunId: args.flowRunId, idempotencyKey: args.idempotencyKey, organizationId: args.organizationId, projectId: args.projectId, sourceManifest: args.sourceManifest, status: "pending", threadId: args.threadId, updatedAt: timestamp, workId: args.workId, }); const build = await ctx.db.get(buildId); if (!build) { throw new ConvexError("Artifact build could not be read after insert"); } return build; }, }); /** * Link a finished artifact build to its rendered Convex Storage id and the * created artifact row. A ready build is terminal. */ export const markReady = internalMutation({ args: { artifactId: v.id("artifacts"), buildId: v.id("artifactBuilds"), outputStorageId: v.optional(v.id("_storage")), }, handler: async (ctx, args): Promise> => { const build = await ctx.db.get(args.buildId); if (!build) { throw new ConvexError("Artifact build not found"); } if (build.status === "ready") { return build; } await ctx.db.patch(args.buildId, { artifactId: args.artifactId, outputStorageId: args.outputStorageId, status: "ready", updatedAt: Date.now(), }); const updated = await ctx.db.get(args.buildId); if (!updated) { throw new ConvexError("Artifact build could not be read after update"); } return updated; }, }); export const markFailed = internalMutation({ args: { buildId: v.id("artifactBuilds"), error: v.string(), }, handler: async (ctx, args): Promise> => { const build = await ctx.db.get(args.buildId); if (!build) { throw new ConvexError("Artifact build not found"); } if (build.status === "ready") { return build; } await ctx.db.patch(args.buildId, { lastError: args.error, status: "failed", updatedAt: Date.now(), }); const updated = await ctx.db.get(args.buildId); if (!updated) { throw new ConvexError("Artifact build could not be read after update"); } return updated; }, }); export const get = internalQuery({ args: { buildId: v.id("artifactBuilds") }, handler: async (ctx, args): Promise | null> => await ctx.db.get(args.buildId), }); export { buildStatus };