mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-04 11:01:16 +01:00
Make Clip publish atomic
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
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);
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user