Make Clip publish atomic

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-07-09 18:58:38 +00:00
1 parent fed84a8547
commit 2a9a0cdcf2
2 files changed
+69 -33

No files matched your search

@@ -138,6 +138,39 @@ describeEmbeddedPostgres("clipService", () => {
expect(detail).not.toHaveProperty("sourceObjectId");
});
it("rolls back initial publish when revision dependency insertion fails", async () => {
const companyId = await seedCompany();
const slug = "rollback-test-" + companyId.slice(0, 8);
await expect(svc.publish(companyId, {
creatorProfile: {
handle: "rollback-creator-" + companyId.slice(0, 8),
displayName: "Rollback Creator",
},
slug,
type: "agent",
title: "Rollback Test Agent",
summary: "Exercises transaction rollback on publish.",
visibility: "public",
status: "published",
sourceKind: "paperclip_company_object",
sourceObjectType: "agent",
sourceObjectId: "rollback-agent-id",
revision: {
manifestChecksum: "sha256:manifest-rollback",
artifactChecksum: "sha256:artifact-rollback",
manifestPayload: { schema: "paperclip.clip/v1", rollback: true },
dependencies: [
{ type: "adapter", key: "codex_local" },
{ type: "adapter", key: "codex_local" },
],
},
})).rejects.toThrow();
const rows = await db.select().from(clips).where(eq(clips.slug, slug));
expect(rows).toHaveLength(0);
});
it("stores revisions append-only and serves exact revision manifests", async () => {
const companyId = await seedCompany();
const { clip, revision } = await publishFixture(companyId);
+36 -33
View File
@@ -336,39 +336,42 @@ export function clipService(db: Db) {
const existing = await getClipBySlug(input.slug);
if (existing) throw conflict("Clip slug already exists");
const creatorProfile = await resolveCreatorProfile(companyId, input);
const [clip] = await db
.insert(clips)
.values({
sourceCompanyId: companyId,
creatorProfileId: creatorProfile.id,
slug: input.slug,
type: input.type,
title: input.title,
summary: input.summary,
description: input.description ?? null,
visibility: input.visibility ?? "unlisted",
status: input.status ?? (input.visibility === "public" ? "pending_review" : "published"),
tags: input.tags ?? [],
categories: input.categories ?? [],
useCases: input.useCases ?? [],
requiredProviders: input.requiredProviders ?? [],
compatibility: input.compatibility ?? {},
sourceKind: input.sourceKind ?? null,
sourceObjectType: input.sourceObjectType ?? null,
sourceObjectId: input.sourceObjectId ?? null,
})
.returning();
const revision = await insertRevision(db, clip.id, input.revision, 1);
const visibleRevisionId = clip.status === "published" ? revision.id : null;
const [updated] = await db
.update(clips)
.set({
currentRevisionId: revision.id,
latestApprovedRevisionId: visibleRevisionId,
updatedAt: new Date(),
})
.where(eq(clips.id, clip.id))
.returning();
const { clip: updated, revision } = await db.transaction(async (tx) => {
const [clip] = await tx
.insert(clips)
.values({
sourceCompanyId: companyId,
creatorProfileId: creatorProfile.id,
slug: input.slug,
type: input.type,
title: input.title,
summary: input.summary,
description: input.description ?? null,
visibility: input.visibility ?? "unlisted",
status: input.status ?? (input.visibility === "public" ? "pending_review" : "published"),
tags: input.tags ?? [],
categories: input.categories ?? [],
useCases: input.useCases ?? [],
requiredProviders: input.requiredProviders ?? [],
compatibility: input.compatibility ?? {},
sourceKind: input.sourceKind ?? null,
sourceObjectType: input.sourceObjectType ?? null,
sourceObjectId: input.sourceObjectId ?? null,
})
.returning();
const revision = await insertRevision(tx, clip.id, input.revision, 1);
const visibleRevisionId = clip.status === "published" ? revision.id : null;
const [updated] = await tx
.update(clips)
.set({
currentRevisionId: revision.id,
latestApprovedRevisionId: visibleRevisionId,
updatedAt: new Date(),
})
.where(eq(clips.id, clip.id))
.returning();
return { clip: updated, revision };
});
await rebuildRankingSnapshot(updated);
return { clip: updated, revision, creatorProfile };
}