fix(rename): surface duplicate-slug 409 instead of 500 (v1.21.1)
mesh.slug actually carries a UNIQUE constraint (mesh_slug_unique)
even though the schema comment claimed otherwise. Trying to rename
to a slug another mesh already owns blew up as a generic 500.
Now: caught at the route, surfaced as 409 with body
{"error":"slug \"<x>\" is already taken"}; CLI maps it to
EXIT.ALREADY_EXISTS and prints the message.
Schema comment corrected to match DB reality.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claudemesh-cli",
|
||||
"version": "1.21.0",
|
||||
"version": "1.21.1",
|
||||
"description": "Peer mesh for Claude Code sessions — CLI + MCP server.",
|
||||
"keywords": [
|
||||
"claude-code",
|
||||
|
||||
@@ -63,6 +63,7 @@ export async function rename(oldSlug: string, newSlug: string): Promise<number>
|
||||
if (err.status === 401) return EXIT.AUTH_FAILED;
|
||||
if (err.status === 403) return EXIT.PERMISSION_DENIED;
|
||||
if (err.status === 404) return EXIT.NOT_FOUND;
|
||||
if (err.status === 409) return EXIT.ALREADY_EXISTS;
|
||||
if (err.status === 400) return EXIT.INVALID_ARGS;
|
||||
return EXIT.INTERNAL_ERROR;
|
||||
}
|
||||
|
||||
@@ -130,11 +130,26 @@ export async function PATCH(
|
||||
// Soft-collapse: name and slug are a single concept user-facing,
|
||||
// so we always sync name = slug. mesh.name column stays for now
|
||||
// (avoids touching ~25 reader sites); a future migration drops it.
|
||||
//
|
||||
// The DB has a unique constraint on mesh.slug (mesh_slug_unique)
|
||||
// even though the schema comment incorrectly claims "NOT unique" —
|
||||
// catch the duplicate-key error and surface it as 409 instead of
|
||||
// a generic 500.
|
||||
try {
|
||||
const [updated] = await db
|
||||
.update(mesh)
|
||||
.set({ slug: newSlug, name: newSlug })
|
||||
.where(eq(mesh.slug, slug))
|
||||
.returning({ slug: mesh.slug });
|
||||
|
||||
return NextResponse.json(updated);
|
||||
} catch (e) {
|
||||
const err = e as { cause?: { constraint_name?: string; code?: string } };
|
||||
if (err.cause?.constraint_name === "mesh_slug_unique" || err.cause?.code === "23505") {
|
||||
return NextResponse.json(
|
||||
{ error: `slug "${newSlug}" is already taken` },
|
||||
{ status: 409 },
|
||||
);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,12 +78,14 @@ export const mesh = meshSchema.table("mesh", {
|
||||
id: text().primaryKey().notNull().$defaultFn(generateId),
|
||||
name: text().notNull(),
|
||||
/**
|
||||
* Cosmetic slug derived from name at creation. NOT unique, NOT used for
|
||||
* identity — `mesh.id` is the canonical identifier everywhere (URLs,
|
||||
* invites, broker lookups). Kept for display/debugging only. Two meshes
|
||||
* can freely share a slug.
|
||||
* URL-safe identifier — globally unique across all meshes (constraint
|
||||
* `mesh_slug_unique` on the DB). `mesh.id` (UUID) remains the canonical
|
||||
* primary key for FKs and broker routing, but slug is the identifier
|
||||
* users see and type (CLI picker, --mesh flag, dashboard sidebar).
|
||||
* v0.7.0 collapsed this with mesh.name; both columns now hold the
|
||||
* same value, and a follow-up migration drops mesh.name.
|
||||
*/
|
||||
slug: text().notNull(),
|
||||
slug: text().notNull().unique(),
|
||||
ownerUserId: text()
|
||||
.references(() => user.id, { onDelete: "cascade", onUpdate: "cascade" })
|
||||
.notNull(),
|
||||
|
||||
Reference in New Issue
Block a user