import { ConvexError, v } from "convex/values"; import type { Doc } from "./_generated/dataModel"; import { internalMutation, query } from "./_generated/server"; import { requireArtifactMember, requireWorkMember } from "./authz"; export const get = query({ args: { artifactId: v.id("artifacts") }, handler: async (ctx, args): Promise | null> => { await requireArtifactMember(ctx, args.artifactId); return await ctx.db.get(args.artifactId); }, }); export const listForWork = query({ args: { limit: v.optional(v.number()), workId: v.id("works") }, handler: async (ctx, args): Promise[]> => { const { organizationId } = await requireWorkMember(ctx, args.workId); const limit = Math.min(Math.max(Math.floor(args.limit ?? 50), 1), 100); return await ctx.db .query("artifacts") .withIndex("by_workId_and_createdAt", (q) => q.eq("workId", args.workId)) .filter((q) => q.eq(q.field("organizationId"), organizationId)) .order("desc") .take(limit); }, }); /** * Publish (or supersede) a typed artifact revision for a project/work/thread. * * Versioning: the new artifact's `version` is one greater than the latest * existing artifact sharing the same `logicalKey` within the org. The previous * latest artifact (if any) is marked `superseded` and linked via * `supersedesArtifactId` on the new row. * * The optional `storageId` links the rendered static HTML (or other binary) in * Convex Storage to the artifact row, keeping the card/manifest inline for the * timeline. * * Idempotent by `logicalKey` + `version`: a re-publish of the same version * returns the existing artifact. `createdByEventId` ties the artifact to the * event that produced it (exact-once via the event's idempotency key). */ export const publishRevision = internalMutation({ args: { card: v.any(), content: v.any(), createdByEventId: v.id("events"), kind: v.union( v.literal("project_setup"), v.literal("summary"), v.literal("plan"), v.literal("preview"), v.literal("blocker"), v.literal("report") ), logicalKey: v.string(), organizationId: v.id("organizations"), projectId: v.optional(v.id("projects")), status: v.union( v.literal("working"), v.literal("ready"), v.literal("blocked"), v.literal("failed"), v.literal("superseded") ), storageId: v.optional(v.id("_storage")), summary: v.string(), threadId: v.optional(v.id("threads")), title: v.string(), workId: v.optional(v.id("works")), }, handler: async (ctx, args): Promise> => { const previous = await ctx.db .query("artifacts") .withIndex("by_organizationId_and_logicalKey_and_version", (q) => q .eq("organizationId", args.organizationId) .eq("logicalKey", args.logicalKey) ) .order("desc") .first(); const version = (previous?.version ?? 0) + 1; const now = Date.now(); const artifactId = await ctx.db.insert("artifacts", { card: args.card, content: args.content, createdAt: now, createdByEventId: args.createdByEventId, kind: args.kind, logicalKey: args.logicalKey, organizationId: args.organizationId, projectId: args.projectId, status: args.status, storageId: args.storageId, summary: args.summary, supersedesArtifactId: previous?._id, threadId: args.threadId, title: args.title, version, workId: args.workId, }); // Mark the prior revision superseded so only the latest is "ready". if (previous && previous.status !== "superseded") { await ctx.db.patch(previous._id, { status: "superseded" }); } const artifact = await ctx.db.get(artifactId); if (!artifact) { throw new ConvexError("Artifact could not be read after publish"); } return artifact; }, });