mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-30 21:49:18 +01:00
Stabilize Clip import and slug conflicts
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Reference in new issue
Block a user