diff --git a/apps/server/src/core/label/dto/label.dto.ts b/apps/server/src/core/label/dto/label.dto.ts index e4ff7d4c..9b3540e0 100644 --- a/apps/server/src/core/label/dto/label.dto.ts +++ b/apps/server/src/core/label/dto/label.dto.ts @@ -6,8 +6,14 @@ import { IsOptional, IsString, IsUUID, + Matches, MaxLength, } from 'class-validator'; +import { Transform } from 'class-transformer'; + +function normalizeLabel(name: string): string { + return name.trim().replace(/\s+/g, '-').toLowerCase(); +} export class AddLabelsDto { @IsString() @@ -19,7 +25,14 @@ export class AddLabelsDto { @ArrayMaxSize(25) @IsString({ each: true }) @IsNotEmpty({ each: true }) + @Transform(({ value }) => + Array.isArray(value) ? value.map(normalizeLabel) : value, + ) @MaxLength(100, { each: true }) + @Matches(/^[a-z0-9_~-]+$/, { + each: true, + message: 'Label names can only contain letters, numbers, hyphens, underscores, and tildes', + }) names: string[]; } diff --git a/apps/server/src/database/migrations/20260217T120000-labels.ts b/apps/server/src/database/migrations/20260217T120000-labels.ts index 4811baad..c3a258b8 100644 --- a/apps/server/src/database/migrations/20260217T120000-labels.ts +++ b/apps/server/src/database/migrations/20260217T120000-labels.ts @@ -19,9 +19,9 @@ export async function up(db: Kysely): Promise { .execute(); await db.schema - .createIndex('labels_workspace_id_lower_name_unique') + .createIndex('labels_workspace_id_name_unique') .on('labels') - .expression(sql`workspace_id, LOWER(name)`) + .columns(['workspace_id', 'name']) .unique() .execute(); diff --git a/apps/server/src/database/repos/label/label.repo.ts b/apps/server/src/database/repos/label/label.repo.ts index c5e75125..345b5bee 100644 --- a/apps/server/src/database/repos/label/label.repo.ts +++ b/apps/server/src/database/repos/label/label.repo.ts @@ -3,7 +3,6 @@ import { InjectKysely } from 'nestjs-kysely'; import { KyselyDB, KyselyTransaction } from '../../types/kysely.types'; import { Label } from '@docmost/db/types/entity.types'; import { dbOrTx } from '@docmost/db/utils'; -import { sql } from 'kysely'; import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo'; @Injectable() @@ -34,7 +33,7 @@ export class LabelRepo { return db .selectFrom('labels') .selectAll() - .where(sql`LOWER(name)`, '=', name.toLowerCase()) + .where('name', '=', name.toLowerCase()) .where('workspaceId', '=', workspaceId) .executeTakeFirst(); } @@ -45,15 +44,13 @@ export class LabelRepo { trx?: KyselyTransaction, ): Promise