115 lines
3.9 KiB
TypeScript
115 lines
3.9 KiB
TypeScript
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<Doc<"artifacts"> | 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<readonly Doc<"artifacts">[]> => {
|
|
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<Doc<"artifacts">> => {
|
|
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;
|
|
},
|
|
});
|