Stabilize Clip import and slug conflicts

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-07-09 19:28:00 +00:00
1 parent 24d056623d
commit c0b194ed4a
3 files changed
+37 -6

No files matched your search

@@ -138,6 +138,32 @@ describeEmbeddedPostgres("clipService", () => {
expect(detail).not.toHaveProperty("sourceObjectId");
});
it("returns a clean conflict when publishing a duplicate slug", async () => {
const companyId = await seedCompany();
const { clip } = await publishFixture(companyId);
await expect(svc.publish(companyId, {
creatorProfile: {
handle: "duplicate-creator-" + companyId.slice(0, 8),
displayName: "Duplicate Creator",
},
slug: clip.slug,
type: "agent",
title: "Duplicate Slug Agent",
summary: "Attempts to reuse an existing slug.",
visibility: "public",
status: "published",
sourceKind: "paperclip_company_object",
sourceObjectType: "agent",
sourceObjectId: "duplicate-agent-id",
revision: {
manifestChecksum: "sha256:manifest-duplicate",
artifactChecksum: "sha256:artifact-duplicate",
manifestPayload: { schema: "paperclip.clip/v1", duplicate: true },
},
})).rejects.toThrow("Clip slug already exists");
});
it("rolls back initial publish when revision dependency insertion fails", async () => {
const companyId = await seedCompany();
const slug = "rollback-test-" + companyId.slice(0, 8);
+2 -2
View File
@@ -351,8 +351,6 @@ export function clipService(db: Db) {
}
async function publish(companyId: string, input: PublishClip) {
const existing = await getClipBySlug(input.slug);
if (existing) throw conflict("Clip slug already exists");
const { clip: updated, revision, creatorProfile } = await db.transaction(async (tx) => {
const creatorProfile = await resolveCreatorProfile(tx, companyId, input);
const [clip] = await tx
@@ -376,7 +374,9 @@ export function clipService(db: Db) {
sourceObjectType: input.sourceObjectType ?? null,
sourceObjectId: input.sourceObjectId ?? null,
})
.onConflictDoNothing({ target: clips.slug })
.returning();
if (!clip) throw conflict("Clip slug already exists");
const revision = await insertRevision(tx, clip.id, input.revision, 1);
const visibleRevisionId = clip.status === "published" ? revision.id : null;
const [updated] = await tx
+9 -4
View File
@@ -15,6 +15,7 @@ import {
Upload,
} from "lucide-react";
import type {
ClipImportPreviewResult,
ClipSharePreviewResult,
ClipType,
ClipVisibility,
@@ -225,10 +226,10 @@ export function Clips() {
});
const importMutation = useMutation({
mutationFn: () => {
mutationFn: (preview: ClipImportPreviewResult) => {
if (!selectedCompanyId) throw new Error("Select a company first.");
return clipsApi.importClip(selectedCompanyId, {
url: importUrl.trim(),
url: preview.source.url,
collisionStrategy: "rename",
selectedOptions: { routineTriggers: "review_required" },
});
@@ -301,7 +302,11 @@ export function Clips() {
<div className="flex flex-col gap-2 sm:flex-row">
<Input
value={importUrl}
onChange={(event) => setImportUrl(event.target.value)}
onChange={(event) => {
setImportUrl(event.target.value);
importPreviewMutation.reset();
importMutation.reset();
}}
placeholder="https://paperclip.ing/clips/support-triage or support-triage"
/>
<Button
@@ -333,7 +338,7 @@ export function Clips() {
Import is blocked if preview errors exist. Recurring and webhook triggers stay disabled until reviewed.
</div>
<Button
onClick={() => importMutation.mutate()}
onClick={() => importMutation.mutate(activePreview)}
disabled={importMutation.isPending || ((asPreviewRecord(activePreview.preview).errors as unknown[] | undefined)?.length ?? 0) > 0}
>
<Check className="mr-2 h-4 w-4" />