Files

142 lines
4.1 KiB
TypeScript

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:<flowRunId>:artifact:<kind>:revision:<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<Doc<"artifactBuilds">> => {
// 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<Doc<"artifactBuilds">> => {
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<Doc<"artifactBuilds">> => {
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<Doc<"artifactBuilds"> | null> =>
await ctx.db.get(args.buildId),
});
export { buildStatus };