diff --git a/apps/server/src/core/attachment/services/attachment.service.ts b/apps/server/src/core/attachment/services/attachment.service.ts index b71eb593..db1f2a92 100644 --- a/apps/server/src/core/attachment/services/attachment.service.ts +++ b/apps/server/src/core/attachment/services/attachment.service.ts @@ -7,7 +7,7 @@ import { prepareFile, validateFileType, } from '../attachment.utils'; -import { v4 as uuid4 } from 'uuid'; +import { v4 as uuid4, v7 as uuid7 } from 'uuid'; import { AttachmentRepo } from '@docmost/db/repos/attachment/attachment.repo'; import { AttachmentType, validImageExtensions } from '../attachment.constants'; import { KyselyDB, KyselyTransaction } from '@docmost/db/types/kysely.types'; @@ -40,7 +40,7 @@ export class AttachmentService { const { filePromise, pageId, spaceId, userId, workspaceId } = opts; const preparedFile: PreparedFile = await prepareFile(filePromise); - const attachmentId = uuid4(); + const attachmentId = uuid7(); const filePath = `${getAttachmentFolderPath(AttachmentType.File, workspaceId)}/${attachmentId}/${preparedFile.fileName}`; await this.uploadToDrive(filePath, preparedFile.buffer); diff --git a/apps/server/src/core/group/dto/add-group-user.dto.ts b/apps/server/src/core/group/dto/add-group-user.dto.ts index 5bd497ad..6eea5509 100644 --- a/apps/server/src/core/group/dto/add-group-user.dto.ts +++ b/apps/server/src/core/group/dto/add-group-user.dto.ts @@ -7,6 +7,6 @@ export class AddGroupUserDto extends GroupIdDto { message: 'you cannot add more than 50 users at a time', }) @ArrayMinSize(1) - @IsUUID(4, { each: true }) + @IsUUID('all', { each: true }) userIds: string[]; } diff --git a/apps/server/src/core/group/dto/create-group.dto.ts b/apps/server/src/core/group/dto/create-group.dto.ts index a2b93bec..c49a2709 100644 --- a/apps/server/src/core/group/dto/create-group.dto.ts +++ b/apps/server/src/core/group/dto/create-group.dto.ts @@ -21,7 +21,7 @@ export class CreateGroupDto { @IsOptional() @IsArray() @ArrayMaxSize(50) - @IsUUID(4, { each: true }) + @IsUUID('all', { each: true }) userIds?: string[]; } diff --git a/apps/server/src/core/space/dto/add-space-members.dto.ts b/apps/server/src/core/space/dto/add-space-members.dto.ts index d21338cb..d827f3c8 100644 --- a/apps/server/src/core/space/dto/add-space-members.dto.ts +++ b/apps/server/src/core/space/dto/add-space-members.dto.ts @@ -14,13 +14,13 @@ export class AddSpaceMembersDto extends SpaceIdDto { @ArrayMaxSize(25, { message: 'userIds must an array with no more than 25 elements', }) - @IsUUID(4, { each: true }) + @IsUUID('all', { each: true }) userIds: string[]; @IsArray() @ArrayMaxSize(25, { message: 'userIds must an array with no more than 25 elements', }) - @IsUUID(4, { each: true }) + @IsUUID('all', { each: true }) groupIds: string[]; } diff --git a/apps/server/src/core/space/dto/create-space.dto.ts b/apps/server/src/core/space/dto/create-space.dto.ts index 73e86317..09773235 100644 --- a/apps/server/src/core/space/dto/create-space.dto.ts +++ b/apps/server/src/core/space/dto/create-space.dto.ts @@ -1,4 +1,10 @@ -import { IsOptional, IsString, MaxLength, MinLength } from 'class-validator'; +import { + IsAlphanumeric, + IsOptional, + IsString, + MaxLength, + MinLength, +} from 'class-validator'; export class CreateSpaceDto { @MinLength(2) @@ -12,6 +18,6 @@ export class CreateSpaceDto { @MinLength(2) @MaxLength(50) - @IsString() + @IsAlphanumeric() slug: string; } diff --git a/apps/server/src/core/workspace/dto/invitation.dto.ts b/apps/server/src/core/workspace/dto/invitation.dto.ts index 269fdb96..8e5cccac 100644 --- a/apps/server/src/core/workspace/dto/invitation.dto.ts +++ b/apps/server/src/core/workspace/dto/invitation.dto.ts @@ -28,7 +28,7 @@ export class InviteUserDto { message: 'you cannot add invited users to more than 25 groups at a time', }) @ArrayMinSize(0) - @IsUUID(4, { each: true }) + @IsUUID('all', { each: true }) groupIds: string[]; @IsEnum(UserRole) diff --git a/apps/server/src/database/migrations/20240324T085400-uuid_v7_fn.ts b/apps/server/src/database/migrations/20240324T085400-uuid_v7_fn.ts new file mode 100644 index 00000000..ec5d7714 --- /dev/null +++ b/apps/server/src/database/migrations/20240324T085400-uuid_v7_fn.ts @@ -0,0 +1,53 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql` + /* + * MIT License - Copyright (c) 2023-2024 Fabio Lima + * Source: https://gist.githubusercontent.com/fabiolimace/515a0440e3e40efeb234e12644a6a346/ + */ + CREATE OR REPLACE FUNCTION gen_uuid_v7() RETURNS uuid AS $$ + declare + v_time numeric := null; + + v_unix_t numeric := null; + v_rand_a numeric := null; + v_rand_b numeric := null; + + v_unix_t_hex varchar := null; + v_rand_a_hex varchar := null; + v_rand_b_hex varchar := null; + + v_output_bytes bytea := null; + + c_milli_factor numeric := 10^3::numeric; -- 1000 + c_micro_factor numeric := 10^6::numeric; -- 1000000 + c_scale_factor numeric := 4.096::numeric; -- 4.0 * (1024 / 1000) + + c_version bit(64) := x'0000000000007000'; -- RFC-4122 version: b'0111...' + c_variant bit(64) := x'8000000000000000'; -- RFC-4122 variant: b'10xx...' + + begin + v_time := extract(epoch from clock_timestamp()); + + v_unix_t := trunc(v_time * c_milli_factor); + v_rand_a := ((v_time * c_micro_factor) - (v_unix_t * c_milli_factor)) * c_scale_factor; + v_rand_b := random()::numeric * 2^62::numeric; + + v_unix_t_hex := lpad(to_hex(v_unix_t::bigint), 12, '0'); + v_rand_a_hex := lpad(to_hex((v_rand_a::bigint::bit(64) | c_version)::bigint), 4, '0'); + v_rand_b_hex := lpad(to_hex((v_rand_b::bigint::bit(64) | c_variant)::bigint), 16, '0'); + + v_output_bytes := decode(v_unix_t_hex || v_rand_a_hex || v_rand_b_hex, 'hex'); + + return encode(v_output_bytes, 'hex')::uuid; + + v_output_bytes := decode(v_unix_t_hex || v_rand_a_hex || v_rand_b_hex, 'hex'); + + return encode(v_output_bytes, 'hex')::uuid; + end $$ LANGUAGE plpgsql;`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`DROP FUNCTION gen_uuid_v7`.execute(db); +} diff --git a/apps/server/src/database/migrations/20240324T085500-workspaces.ts b/apps/server/src/database/migrations/20240324T085500-workspaces.ts index cc5702ac..9bd9323d 100644 --- a/apps/server/src/database/migrations/20240324T085500-workspaces.ts +++ b/apps/server/src/database/migrations/20240324T085500-workspaces.ts @@ -5,7 +5,7 @@ export async function up(db: Kysely): Promise { await db.schema .createTable('workspaces') .addColumn('id', 'uuid', (col) => - col.primaryKey().defaultTo(sql`gen_random_uuid()`), + col.primaryKey().defaultTo(sql`gen_uuid_v7()`), ) .addColumn('name', 'varchar', (col) => col) .addColumn('description', 'varchar', (col) => col) diff --git a/apps/server/src/database/migrations/20240324T085600-users.ts b/apps/server/src/database/migrations/20240324T085600-users.ts index fbf00435..d4b531fa 100644 --- a/apps/server/src/database/migrations/20240324T085600-users.ts +++ b/apps/server/src/database/migrations/20240324T085600-users.ts @@ -4,7 +4,7 @@ export async function up(db: Kysely): Promise { await db.schema .createTable('users') .addColumn('id', 'uuid', (col) => - col.primaryKey().defaultTo(sql`gen_random_uuid()`), + col.primaryKey().defaultTo(sql`gen_uuid_v7()`), ) .addColumn('name', 'varchar', (col) => col) .addColumn('email', 'varchar', (col) => col.notNull()) diff --git a/apps/server/src/database/migrations/20240324T085700-groups.ts b/apps/server/src/database/migrations/20240324T085700-groups.ts index a4072dfb..1de73362 100644 --- a/apps/server/src/database/migrations/20240324T085700-groups.ts +++ b/apps/server/src/database/migrations/20240324T085700-groups.ts @@ -4,7 +4,7 @@ export async function up(db: Kysely): Promise { await db.schema .createTable('groups') .addColumn('id', 'uuid', (col) => - col.primaryKey().defaultTo(sql`gen_random_uuid()`), + col.primaryKey().defaultTo(sql`gen_uuid_v7()`), ) .addColumn('name', 'varchar', (col) => col.notNull()) .addColumn('description', 'text', (col) => col) @@ -19,6 +19,7 @@ export async function up(db: Kysely): Promise { .addColumn('updated_at', 'timestamptz', (col) => col.notNull().defaultTo(sql`now()`), ) + .addColumn('deleted_at', 'timestamptz', (col) => col) .addUniqueConstraint('groups_name_workspace_id_unique', [ 'name', 'workspace_id', @@ -29,7 +30,7 @@ export async function up(db: Kysely): Promise { await db.schema .createTable('group_users') .addColumn('id', 'uuid', (col) => - col.primaryKey().defaultTo(sql`gen_random_uuid()`), + col.primaryKey().defaultTo(sql`gen_uuid_v7()`), ) .addColumn('user_id', 'uuid', (col) => col.references('users.id').onDelete('cascade').notNull(), diff --git a/apps/server/src/database/migrations/20240324T085900-spaces.ts b/apps/server/src/database/migrations/20240324T085900-spaces.ts index a4271a10..1b92b57f 100644 --- a/apps/server/src/database/migrations/20240324T085900-spaces.ts +++ b/apps/server/src/database/migrations/20240324T085900-spaces.ts @@ -8,7 +8,7 @@ export async function up(db: Kysely): Promise { await db.schema .createTable('spaces') .addColumn('id', 'uuid', (col) => - col.primaryKey().defaultTo(sql`gen_random_uuid()`), + col.primaryKey().defaultTo(sql`gen_uuid_v7()`), ) .addColumn('name', 'varchar', (col) => col) .addColumn('description', 'text', (col) => col) @@ -40,7 +40,7 @@ export async function up(db: Kysely): Promise { await db.schema .createTable('space_members') .addColumn('id', 'uuid', (col) => - col.primaryKey().defaultTo(sql`gen_random_uuid()`), + col.primaryKey().defaultTo(sql`gen_uuid_v7()`), ) .addColumn('user_id', 'uuid', (col) => col.references('users.id').onDelete('cascade'), @@ -59,6 +59,7 @@ export async function up(db: Kysely): Promise { .addColumn('updated_at', 'timestamptz', (col) => col.notNull().defaultTo(sql`now()`), ) + .addColumn('deleted_at', 'timestamptz', (col) => col) .addUniqueConstraint('space_members_space_id_user_id_unique', [ 'space_id', 'user_id', diff --git a/apps/server/src/database/migrations/20240324T086200-workspace_invitations.ts b/apps/server/src/database/migrations/20240324T086200-workspace_invitations.ts index d2186364..bfa62a1e 100644 --- a/apps/server/src/database/migrations/20240324T086200-workspace_invitations.ts +++ b/apps/server/src/database/migrations/20240324T086200-workspace_invitations.ts @@ -4,7 +4,7 @@ export async function up(db: Kysely): Promise { await db.schema .createTable('workspace_invitations') .addColumn('id', 'uuid', (col) => - col.primaryKey().defaultTo(sql`gen_random_uuid()`), + col.primaryKey().defaultTo(sql`gen_uuid_v7()`), ) .addColumn('email', 'varchar', (col) => col) .addColumn('role', 'varchar', (col) => col.notNull()) diff --git a/apps/server/src/database/migrations/20240324T086300-pages.ts b/apps/server/src/database/migrations/20240324T086300-pages.ts index ba41c224..cbe239c7 100644 --- a/apps/server/src/database/migrations/20240324T086300-pages.ts +++ b/apps/server/src/database/migrations/20240324T086300-pages.ts @@ -4,7 +4,7 @@ export async function up(db: Kysely): Promise { await db.schema .createTable('pages') .addColumn('id', 'uuid', (col) => - col.primaryKey().defaultTo(sql`gen_random_uuid()`), + col.primaryKey().defaultTo(sql`gen_uuid_v7()`), ) .addColumn('slug_id', 'varchar', (col) => col.notNull()) .addColumn('title', 'varchar', (col) => col) diff --git a/apps/server/src/database/migrations/20240324T086400-page_history.ts b/apps/server/src/database/migrations/20240324T086400-page_history.ts index f385645e..36dd1f24 100644 --- a/apps/server/src/database/migrations/20240324T086400-page_history.ts +++ b/apps/server/src/database/migrations/20240324T086400-page_history.ts @@ -4,7 +4,7 @@ export async function up(db: Kysely): Promise { await db.schema .createTable('page_history') .addColumn('id', 'uuid', (col) => - col.primaryKey().defaultTo(sql`gen_random_uuid()`), + col.primaryKey().defaultTo(sql`gen_uuid_v7()`), ) .addColumn('page_id', 'uuid', (col) => col.references('pages.id').onDelete('cascade').notNull(), diff --git a/apps/server/src/database/migrations/20240324T086600-comments.ts b/apps/server/src/database/migrations/20240324T086600-comments.ts index 59d0f535..fcd94150 100644 --- a/apps/server/src/database/migrations/20240324T086600-comments.ts +++ b/apps/server/src/database/migrations/20240324T086600-comments.ts @@ -4,7 +4,7 @@ export async function up(db: Kysely): Promise { await db.schema .createTable('comments') .addColumn('id', 'uuid', (col) => - col.primaryKey().defaultTo(sql`gen_random_uuid()`), + col.primaryKey().defaultTo(sql`gen_uuid_v7()`), ) .addColumn('content', 'jsonb', (col) => col) .addColumn('selection', 'varchar', (col) => col) diff --git a/apps/server/src/database/migrations/20240324T086700-attachments.ts b/apps/server/src/database/migrations/20240324T086700-attachments.ts index a813f238..7d914439 100644 --- a/apps/server/src/database/migrations/20240324T086700-attachments.ts +++ b/apps/server/src/database/migrations/20240324T086700-attachments.ts @@ -4,7 +4,7 @@ export async function up(db: Kysely): Promise { await db.schema .createTable('attachments') .addColumn('id', 'uuid', (col) => - col.primaryKey().defaultTo(sql`gen_random_uuid()`), + col.primaryKey().defaultTo(sql`gen_uuid_v7()`), ) .addColumn('file_name', 'varchar', (col) => col.notNull()) .addColumn('file_path', 'varchar', (col) => col.notNull()) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fd018fd2..3057a1a7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3914,8 +3914,8 @@ packages: '@types/uuid@10.0.0': resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} - '@types/validator@13.11.9': - resolution: {integrity: sha512-FCTsikRozryfayPuiI46QzH3fnrOoctTjvOYZkho9BTFLCOZ2rgZJHMOVgCOfttjPJcgOx52EpkY0CMfy87MIw==} + '@types/validator@13.12.0': + resolution: {integrity: sha512-nH45Lk7oPIJ1RVOF6JgFI6Dy0QpHEzq4QecZhvguxYPDwT8c93prCMqAtiIttm39voZ+DDR+qkNnMpJmMBRqag==} '@types/webpack@5.28.5': resolution: {integrity: sha512-wR87cgvxj3p6D0Crt1r5avwqffqPXUkNlnQ1mjU93G7gCuFjufZR4I6j8cz5g1F1tTYpfOOFvly+cmIQwL9wvw==} @@ -7749,8 +7749,8 @@ packages: resolution: {integrity: sha512-YuKoXDAhBYxY7SfOKxHBDoSyENFeW5VvIIQp2TGQuit8gpK6MnWaQelBKxso72DoxTZfZdcP3W90LqpSkgPzLQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - validator@13.11.0: - resolution: {integrity: sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==} + validator@13.12.0: + resolution: {integrity: sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==} engines: {node: '>= 0.10'} vary@1.1.2: @@ -12134,7 +12134,7 @@ snapshots: '@types/uuid@10.0.0': {} - '@types/validator@13.11.9': {} + '@types/validator@13.12.0': {} '@types/webpack@5.28.5(@swc/core@1.3.101(@swc/helpers@0.5.11))(esbuild@0.19.11)': dependencies: @@ -12843,9 +12843,9 @@ snapshots: class-validator@0.14.1: dependencies: - '@types/validator': 13.11.9 + '@types/validator': 13.12.0 libphonenumber-js: 1.10.58 - validator: 13.11.0 + validator: 13.12.0 cli-cursor@3.1.0: dependencies: @@ -16539,7 +16539,7 @@ snapshots: dependencies: builtins: 5.0.1 - validator@13.11.0: {} + validator@13.12.0: {} vary@1.1.2: {}