Compare commits

...

1 Commits

Author SHA1 Message Date
Henry
33677f720c fix: update board slug availability check to be unique to workspace 2025-07-26 21:17:25 +01:00
3 changed files with 43 additions and 27 deletions

View File

@@ -167,7 +167,8 @@ export function UpdateBoardSlugForm({
!isDirty || !isDirty ||
updateBoardSlug.isPending || updateBoardSlug.isPending ||
errors.slug?.message !== undefined || errors.slug?.message !== undefined ||
isBoardSlugAvailable?.isReserved isBoardSlugAvailable?.isReserved ||
checkBoardSlugAvailability.isLoading
} }
> >
{t`Update`} {t`Update`}

View File

@@ -276,6 +276,21 @@ export const boardRouter = createTRPCRouter({
await assertUserInWorkspace(ctx.db, userId, board.workspaceId); await assertUserInWorkspace(ctx.db, userId, board.workspaceId);
if (input.slug) {
const isBoardSlugAvailable = await boardRepo.isBoardSlugAvailable(
ctx.db,
input.slug,
board.workspaceId,
);
if (!isBoardSlugAvailable) {
throw new TRPCError({
message: `Board slug ${input.slug} is not available`,
code: "BAD_REQUEST",
});
}
}
const result = await boardRepo.update(ctx.db, { const result = await boardRepo.update(ctx.db, {
name: input.name, name: input.name,
slug: input.slug, slug: input.slug,
@@ -407,11 +422,23 @@ export const boardRouter = createTRPCRouter({
}), }),
) )
.query(async ({ ctx, input }) => { .query(async ({ ctx, input }) => {
const board = await boardRepo.getWorkspaceAndBoardIdByBoardPublicId(
ctx.db,
input.boardPublicId,
);
if (!board)
throw new TRPCError({
message: `Board with public ID ${input.boardPublicId} not found`,
code: "NOT_FOUND",
});
const isBoardSlugAvailable = await boardRepo.isBoardSlugAvailable( const isBoardSlugAvailable = await boardRepo.isBoardSlugAvailable(
ctx.db, ctx.db,
input.boardSlug, input.boardSlug,
input.boardPublicId, board.workspaceId,
); );
return { return {
isReserved: !isBoardSlugAvailable, isReserved: !isBoardSlugAvailable,
}; };

View File

@@ -1,4 +1,4 @@
import { and, asc, desc, eq, exists, inArray, isNull, or, sql } from "drizzle-orm"; import { and, asc, desc, eq, inArray, isNull, or } from "drizzle-orm";
import type { dbClient } from "@kan/db/client"; import type { dbClient } from "@kan/db/client";
import type { BoardVisibilityStatus } from "@kan/db/schema"; import type { BoardVisibilityStatus } from "@kan/db/schema";
@@ -481,30 +481,18 @@ export const getWorkspaceAndBoardIdByBoardPublicId = async (
export const isBoardSlugAvailable = async ( export const isBoardSlugAvailable = async (
db: dbClient, db: dbClient,
boardSlug: string, boardSlug: string,
boardPublicId: string, workspaceId: number,
) => { ) => {
const result = await db const result = await db.query.boards.findFirst({
.select({ id: boards.id }) columns: {
.from(boards) id: true,
.where( },
and( where: and(
eq(boards.publicId, boardPublicId), eq(boards.slug, boardSlug),
exists( eq(boards.workspaceId, workspaceId),
db isNull(boards.deletedAt),
.select({ id: boards.id }) ),
.from(boards) });
.where(
and(
eq(boards.slug, boardSlug),
eq(boards.workspaceId, sql`${boards.workspaceId}`), // Reference outer query's workspaceId
isNull(boards.deletedAt),
),
)
.limit(1),
),
),
)
.limit(1);
return result.length === 0; return result === undefined;
}; };