From f6f52d676ff592136f3704fef1a6f4b2e2475a88 Mon Sep 17 00:00:00 2001 From: Paul Rastoin <45004772+prastoin@users.noreply.github.com> Date: Wed, 29 Oct 2025 19:08:44 +0100 Subject: [PATCH] Explicitly set `workspaceId` column as `uuid` type to ease pg LEFT JOIN (#15430) Pg scan was redundant because historically the workspaceId was `varchar`, even though below migration won't change that we had a look to workspaceId col declaration across the codebase --- ...49599736-workspace-id-uuid-not-nullable.ts | 72 +++++++++++++++++++ .../core-modules/api-key/api-key.entity.ts | 2 +- .../app-token/app-token.entity.ts | 4 +- .../approved-access-domain.entity.ts | 2 +- .../auth/services/auth.service.ts | 2 +- .../token/services/renew-token.service.ts | 5 +- .../auth/types/auth-context.type.ts | 2 +- .../emailing-domain/emailing-domain.entity.ts | 2 +- .../key-value-pair/key-value-pair.entity.ts | 2 +- .../workspace-sso-identity-provider.entity.ts | 2 +- .../user-workspace/user-workspace.entity.ts | 2 +- .../core-modules/webhook/webhook.entity.ts | 2 +- .../agent/agent-chat-thread.entity.ts | 2 +- .../index-metadata/index-metadata.entity.ts | 2 +- 14 files changed, 88 insertions(+), 15 deletions(-) create mode 100644 packages/twenty-server/src/database/typeorm/core/migrations/common/1761749599736-workspace-id-uuid-not-nullable.ts diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/common/1761749599736-workspace-id-uuid-not-nullable.ts b/packages/twenty-server/src/database/typeorm/core/migrations/common/1761749599736-workspace-id-uuid-not-nullable.ts new file mode 100644 index 00000000000..d517a686796 --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/common/1761749599736-workspace-id-uuid-not-nullable.ts @@ -0,0 +1,72 @@ +import { type MigrationInterface, type QueryRunner } from 'typeorm'; + +export class WorkspaceIdUuidNotNullable1761749599736 + implements MigrationInterface +{ + name = 'WorkspaceIdUuidNotNullable1761749599736'; + + public async up(queryRunner: QueryRunner): Promise { + // Delete orphaned rows without workspaceId before migration + await queryRunner.query( + `DELETE FROM "core"."indexMetadata" WHERE "workspaceId" IS NULL`, + ); + + await queryRunner.query( + `DROP INDEX "core"."IDX_INDEX_METADATA_WORKSPACE_ID_OBJECT_METADATA_ID"`, + ); + await queryRunner.query( + `DROP INDEX "core"."IDX_b27c681286ac581f81498c5d4b"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."indexMetadata" DROP CONSTRAINT "IDX_INDEX_METADATA_NAME_WORKSPACE_ID_OBJECT_METADATA_ID_UNIQUE"`, + ); + + // Convert column type from varchar to uuid while preserving data + await queryRunner.query( + `ALTER TABLE "core"."indexMetadata" ALTER COLUMN "workspaceId" TYPE uuid USING "workspaceId"::uuid`, + ); + await queryRunner.query( + `ALTER TABLE "core"."indexMetadata" ALTER COLUMN "workspaceId" SET NOT NULL`, + ); + + await queryRunner.query( + `CREATE UNIQUE INDEX "IDX_b27c681286ac581f81498c5d4b" ON "core"."indexMetadata" ("workspaceId", "universalIdentifier") `, + ); + await queryRunner.query( + `CREATE INDEX "IDX_INDEX_METADATA_WORKSPACE_ID_OBJECT_METADATA_ID" ON "core"."indexMetadata" ("workspaceId", "objectMetadataId") `, + ); + await queryRunner.query( + `ALTER TABLE "core"."indexMetadata" ADD CONSTRAINT "IDX_INDEX_METADATA_NAME_WORKSPACE_ID_OBJECT_METADATA_ID_UNIQUE" UNIQUE ("name", "workspaceId", "objectMetadataId")`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."indexMetadata" DROP CONSTRAINT "IDX_INDEX_METADATA_NAME_WORKSPACE_ID_OBJECT_METADATA_ID_UNIQUE"`, + ); + await queryRunner.query( + `DROP INDEX "core"."IDX_INDEX_METADATA_WORKSPACE_ID_OBJECT_METADATA_ID"`, + ); + await queryRunner.query( + `DROP INDEX "core"."IDX_b27c681286ac581f81498c5d4b"`, + ); + + // Convert column type from uuid back to varchar while preserving data + await queryRunner.query( + `ALTER TABLE "core"."indexMetadata" ALTER COLUMN "workspaceId" DROP NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "core"."indexMetadata" ALTER COLUMN "workspaceId" TYPE character varying USING "workspaceId"::character varying`, + ); + + await queryRunner.query( + `ALTER TABLE "core"."indexMetadata" ADD CONSTRAINT "IDX_INDEX_METADATA_NAME_WORKSPACE_ID_OBJECT_METADATA_ID_UNIQUE" UNIQUE ("name", "workspaceId", "objectMetadataId")`, + ); + await queryRunner.query( + `CREATE UNIQUE INDEX "IDX_b27c681286ac581f81498c5d4b" ON "core"."indexMetadata" ("workspaceId", "universalIdentifier") `, + ); + await queryRunner.query( + `CREATE INDEX "IDX_INDEX_METADATA_WORKSPACE_ID_OBJECT_METADATA_ID" ON "core"."indexMetadata" ("workspaceId", "objectMetadataId") `, + ); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/api-key/api-key.entity.ts b/packages/twenty-server/src/engine/core-modules/api-key/api-key.entity.ts index 24c30620ac3..c03e52d8692 100644 --- a/packages/twenty-server/src/engine/core-modules/api-key/api-key.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/api-key/api-key.entity.ts @@ -37,7 +37,7 @@ export class ApiKeyEntity { revokedAt?: Date | null; @Field(() => UUIDScalarType) - @Column('uuid') + @Column({ nullable: false, type: 'uuid' }) workspaceId: string; @Field(() => Date) diff --git a/packages/twenty-server/src/engine/core-modules/app-token/app-token.entity.ts b/packages/twenty-server/src/engine/core-modules/app-token/app-token.entity.ts index 8cbca7b3ecf..2025b14c27b 100644 --- a/packages/twenty-server/src/engine/core-modules/app-token/app-token.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/app-token/app-token.entity.ts @@ -52,8 +52,8 @@ export class AppTokenEntity { @JoinColumn({ name: 'workspaceId' }) workspace: Relation; - @Column({ nullable: true }) - workspaceId: string; + @Column({ nullable: true, type: 'uuid' }) + workspaceId: string | null; @Field() @Column({ nullable: false, type: 'text', default: AppTokenType.RefreshToken }) diff --git a/packages/twenty-server/src/engine/core-modules/approved-access-domain/approved-access-domain.entity.ts b/packages/twenty-server/src/engine/core-modules/approved-access-domain/approved-access-domain.entity.ts index 9b88a8b21df..cb0d9dcafbd 100644 --- a/packages/twenty-server/src/engine/core-modules/approved-access-domain/approved-access-domain.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/approved-access-domain/approved-access-domain.entity.ts @@ -37,7 +37,7 @@ export class ApprovedAccessDomainEntity { @Column({ type: 'boolean', default: false, nullable: false }) isValidated: boolean; - @Column() + @Column({ nullable: false, type: 'uuid' }) workspaceId: string; @ManyToOne( diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts index 6716a0357ca..29ac1b77878 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts @@ -28,9 +28,9 @@ import { compareHash, hashPassword, } from 'src/engine/core-modules/auth/auth.util'; +import { type AuthTokens } from 'src/engine/core-modules/auth/dto/auth-tokens.dto'; import { type AuthorizeAppOutput } from 'src/engine/core-modules/auth/dto/authorize-app.dto'; import { type AuthorizeAppInput } from 'src/engine/core-modules/auth/dto/authorize-app.input'; -import { type AuthTokens } from 'src/engine/core-modules/auth/dto/auth-tokens.dto'; import { type UpdatePasswordOutput } from 'src/engine/core-modules/auth/dto/update-password.dto'; import { type UserCredentialsInput } from 'src/engine/core-modules/auth/dto/user-credentials.input'; import { type CheckUserExistOutput } from 'src/engine/core-modules/auth/dto/user-exists.dto'; diff --git a/packages/twenty-server/src/engine/core-modules/auth/token/services/renew-token.service.ts b/packages/twenty-server/src/engine/core-modules/auth/token/services/renew-token.service.ts index 4eac69a5f3a..20b17522d15 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/token/services/renew-token.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/token/services/renew-token.service.ts @@ -65,7 +65,8 @@ export class RenewTokenService { const accessToken = isDefined(authProvider) && - targetedTokenType === JwtTokenTypeEnum.WORKSPACE_AGNOSTIC + targetedTokenType === JwtTokenTypeEnum.WORKSPACE_AGNOSTIC && + !isDefined(workspaceId) ? await this.workspaceAgnosticTokenService.generateWorkspaceAgnosticToken( { userId: user.id, @@ -74,7 +75,7 @@ export class RenewTokenService { ) : await this.accessTokenService.generateAccessToken({ userId: user.id, - workspaceId, + workspaceId: workspaceId as string, authProvider: resolvedAuthProvider, isImpersonating, impersonatorUserWorkspaceId, diff --git a/packages/twenty-server/src/engine/core-modules/auth/types/auth-context.type.ts b/packages/twenty-server/src/engine/core-modules/auth/types/auth-context.type.ts index 77459e597a8..af4b0b56e3c 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/types/auth-context.type.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/types/auth-context.type.ts @@ -60,7 +60,7 @@ export type TransientTokenJwtPayload = CommonPropertiesJwtPayload & { export type RefreshTokenJwtPayload = CommonPropertiesJwtPayload & { type: JwtTokenTypeEnum.REFRESH; - workspaceId?: string; + workspaceId?: string | null; userId: string; jti?: string; authProvider?: AuthProviderEnum; diff --git a/packages/twenty-server/src/engine/core-modules/emailing-domain/emailing-domain.entity.ts b/packages/twenty-server/src/engine/core-modules/emailing-domain/emailing-domain.entity.ts index 044c2e3e215..429e887d86a 100644 --- a/packages/twenty-server/src/engine/core-modules/emailing-domain/emailing-domain.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/emailing-domain/emailing-domain.entity.ts @@ -60,7 +60,7 @@ export class EmailingDomainEntity { @Column({ type: 'timestamptz', nullable: true }) verifiedAt: Date | null; - @Column({ nullable: false }) + @Column({ nullable: false, type: 'uuid' }) workspaceId: string; @ManyToOne(() => WorkspaceEntity, (workspace) => workspace.emailingDomains, { diff --git a/packages/twenty-server/src/engine/core-modules/key-value-pair/key-value-pair.entity.ts b/packages/twenty-server/src/engine/core-modules/key-value-pair/key-value-pair.entity.ts index 429e78e9adf..f2a38fee569 100644 --- a/packages/twenty-server/src/engine/core-modules/key-value-pair/key-value-pair.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/key-value-pair/key-value-pair.entity.ts @@ -67,7 +67,7 @@ export class KeyValuePairEntity { @JoinColumn({ name: 'workspaceId' }) workspace: Relation; - @Column({ nullable: true }) + @Column({ nullable: true, type: 'uuid' }) workspaceId: string | null; @Field(() => String) diff --git a/packages/twenty-server/src/engine/core-modules/sso/workspace-sso-identity-provider.entity.ts b/packages/twenty-server/src/engine/core-modules/sso/workspace-sso-identity-provider.entity.ts index 06cbe7b9a97..a1c707412d0 100644 --- a/packages/twenty-server/src/engine/core-modules/sso/workspace-sso-identity-provider.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/sso/workspace-sso-identity-provider.entity.ts @@ -72,7 +72,7 @@ export class WorkspaceSSOIdentityProviderEntity { @JoinColumn({ name: 'workspaceId' }) workspace: Relation; - @Column() + @Column({ nullable: false, type: 'uuid' }) workspaceId: string; @CreateDateColumn({ type: 'timestamptz' }) diff --git a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.entity.ts b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.entity.ts index 57971126587..5847d644748 100644 --- a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.entity.ts @@ -69,7 +69,7 @@ export class UserWorkspaceEntity { workspace: Relation; @Field(() => UUIDScalarType, { nullable: false }) - @Column() + @Column({ nullable: false, type: 'uuid' }) workspaceId: string; @Column({ nullable: true }) diff --git a/packages/twenty-server/src/engine/core-modules/webhook/webhook.entity.ts b/packages/twenty-server/src/engine/core-modules/webhook/webhook.entity.ts index ab0bc962147..840056e5ec0 100644 --- a/packages/twenty-server/src/engine/core-modules/webhook/webhook.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/webhook/webhook.entity.ts @@ -42,7 +42,7 @@ export class WebhookEntity { secret: string; @Field(() => UUIDScalarType) - @Column('uuid') + @Column({ nullable: false, type: 'uuid' }) workspaceId: string; @Field() diff --git a/packages/twenty-server/src/engine/metadata-modules/agent/agent-chat-thread.entity.ts b/packages/twenty-server/src/engine/metadata-modules/agent/agent-chat-thread.entity.ts index 3738f59a279..012fdcaf65f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/agent/agent-chat-thread.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/agent/agent-chat-thread.entity.ts @@ -20,7 +20,7 @@ export class AgentChatThreadEntity { @PrimaryGeneratedColumn('uuid') id: string; - @Column('uuid') + @Column({ nullable: false, type: 'uuid' }) @Index() userWorkspaceId: string; diff --git a/packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.entity.ts b/packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.entity.ts index bd6b38d350a..1e37cb171ac 100644 --- a/packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.entity.ts @@ -44,7 +44,7 @@ export class IndexMetadataEntity @Column({ nullable: false }) name: string; - @Column({ nullable: true }) + @Column({ nullable: false, type: 'uuid' }) workspaceId: string; @Column({ nullable: false, type: 'uuid' })