diff --git a/.github/workflows/ci-server.yaml b/.github/workflows/ci-server.yaml index 82540a48a40c..54e0fd112da1 100644 --- a/.github/workflows/ci-server.yaml +++ b/.github/workflows/ci-server.yaml @@ -75,9 +75,30 @@ jobs: run: | PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";' PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";' + npx nx run twenty-server:database:init:prod + npx nx run twenty-server:database:migrate:prod - name: Worker / Run if: steps.changed-files.outputs.any_changed == 'true' run: npx nx run twenty-server:worker:ci + - name: Server / Check for Pending Migrations + if: steps.changed-files.outputs.any_changed == 'true' + run: | + METADATA_MIGRATION_OUTPUT=$(npx nx run twenty-server:typeorm migration:generate metadata-migration-check -d src/database/typeorm/metadata/metadata.datasource.ts || true) + + CORE_MIGRATION_OUTPUT=$(npx nx run twenty-server:typeorm migration:generate core-migration-check -d src/database/typeorm/core/core.datasource.ts || true) + + METADATA_MIGRATION_FILE=$(ls packages/twenty-server/*metadata-migration-check.ts 2>/dev/null || echo "") + CORE_MIGRATION_FILE=$(ls packages/twenty-server/*core-migration-check.ts 2>/dev/null || echo "") + + if [ -n "$METADATA_MIGRATION_FILE" ] || [ -n "$CORE_MIGRATION_FILE" ]; then + echo "::error::Unexpected migration files were generated. Please create a proper migration manually." + echo "$METADATA_MIGRATION_OUTPUT" + echo "$CORE_MIGRATION_OUTPUT" + + rm -f packages/twenty-server/*metadata-migration-check.ts packages/twenty-server/*core-migration-check.ts + + exit 1 + fi server-test: timeout-minutes: 30 diff --git a/packages/twenty-server/src/database/typeorm/core/core.datasource.ts b/packages/twenty-server/src/database/typeorm/core/core.datasource.ts index 0c052261c71a..1a6bd9ec6aa2 100644 --- a/packages/twenty-server/src/database/typeorm/core/core.datasource.ts +++ b/packages/twenty-server/src/database/typeorm/core/core.datasource.ts @@ -11,9 +11,12 @@ export const typeORMCoreModuleOptions: TypeOrmModuleOptions = { type: 'postgres', logging: ['error'], schema: 'core', - entities: [ - `${isJest ? '' : 'dist/'}src/engine/core-modules/**/*.entity{.ts,.js}`, - ], + entities: + process.env.IS_BILLING_ENABLED === 'true' + ? [`${isJest ? '' : 'dist/'}src/engine/core-modules/**/*.entity{.ts,.js}`] + : [ + `${isJest ? '' : 'dist/'}src/engine/core-modules/**/!(billing-*).entity{.ts,.js}`, + ], synchronize: false, migrationsRun: false, migrationsTableName: '_typeorm_migrations', diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/1733753649142-removeBillingFKWithCore.ts b/packages/twenty-server/src/database/typeorm/core/migrations/1733753649142-removeBillingFKWithCore.ts new file mode 100644 index 000000000000..1892627b6145 --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/1733753649142-removeBillingFKWithCore.ts @@ -0,0 +1,31 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RemoveBillingFKWithCore1733753649142 + implements MigrationInterface +{ + name = 'RemoveBillingFKWithCore1733753649142'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."billingEntitlement" DROP CONSTRAINT "FK_599121a93d8177b5d713b941982"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."billingSubscription" DROP CONSTRAINT "FK_4abfb70314c18da69e1bee1954d"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."billingCustomer" DROP CONSTRAINT "FK_53c2ef50e9611082f83d760897d"`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."billingCustomer" ADD CONSTRAINT "FK_53c2ef50e9611082f83d760897d" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "core"."billingSubscription" ADD CONSTRAINT "FK_4abfb70314c18da69e1bee1954d" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "core"."billingEntitlement" ADD CONSTRAINT "FK_599121a93d8177b5d713b941982" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + } +} diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/common/1733318043626-sso-missing-migration.ts b/packages/twenty-server/src/database/typeorm/core/migrations/common/1733318043626-sso-missing-migration.ts new file mode 100644 index 000000000000..e89fd3453a59 --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/common/1733318043626-sso-missing-migration.ts @@ -0,0 +1,171 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class SsoMissingMigration1733318043626 implements MigrationInterface { + name = 'SsoMissingMigration1733318043626'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."workspaceSSOIdentityProvider" DROP CONSTRAINT "FK_workspaceId"`, + ); + await queryRunner.query( + `DROP INDEX "core"."apptoken_unique_invitation_by_user_workspace"`, + ); + await queryRunner.query( + `DROP INDEX "core"."workspace_subdomain_unique_index"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."appToken" DROP CONSTRAINT "userIdIsNullWhenTypeIsInvitation"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."appToken" DROP CONSTRAINT "userIdNotNullWhenTypeIsNotInvitation"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."workspaceSSOIdentityProvider" DROP CONSTRAINT "CHK_OIDC"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."workspaceSSOIdentityProvider" DROP CONSTRAINT "CHK_SAML"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."workspaceSSOIdentityProvider" ALTER COLUMN "name" SET NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "core"."workspaceSSOIdentityProvider" DROP COLUMN "status"`, + ); + await queryRunner.query( + `CREATE TYPE "core"."workspaceSSOIdentityProvider_status_enum" AS ENUM('Active', 'Inactive', 'Error')`, + ); + await queryRunner.query( + `ALTER TABLE "core"."workspaceSSOIdentityProvider" ADD "status" "core"."workspaceSSOIdentityProvider_status_enum" NOT NULL DEFAULT 'Active'`, + ); + await queryRunner.query( + `ALTER TYPE "core"."idp_type_enum" RENAME TO "idp_type_enum_old"`, + ); + await queryRunner.query( + `CREATE TYPE "core"."workspaceSSOIdentityProvider_type_enum" AS ENUM('OIDC', 'SAML')`, + ); + await queryRunner.query( + `ALTER TABLE "core"."workspaceSSOIdentityProvider" ALTER COLUMN "type" DROP DEFAULT`, + ); + await queryRunner.query( + `ALTER TABLE "core"."workspaceSSOIdentityProvider" ALTER COLUMN "type" TYPE "core"."workspaceSSOIdentityProvider_type_enum" USING "type"::"text"::"core"."workspaceSSOIdentityProvider_type_enum"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."workspaceSSOIdentityProvider" ALTER COLUMN "type" SET DEFAULT 'OIDC'`, + ); + await queryRunner.query(`DROP TYPE "core"."idp_type_enum_old"`); + await queryRunner.query( + `ALTER TYPE "core"."workspace_activationStatus_enum" RENAME TO "workspace_activationStatus_enum_old"`, + ); + await queryRunner.query( + `CREATE TYPE "core"."workspace_activationstatus_enum" AS ENUM('ONGOING_CREATION', 'PENDING_CREATION', 'ACTIVE', 'INACTIVE')`, + ); + await queryRunner.query( + `ALTER TABLE "core"."workspace" ALTER COLUMN "activationStatus" DROP DEFAULT`, + ); + await queryRunner.query( + `ALTER TABLE "core"."workspace" ALTER COLUMN "activationStatus" TYPE "core"."workspace_activationstatus_enum" USING "activationStatus"::"text"::"core"."workspace_activationstatus_enum"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."workspace" ALTER COLUMN "activationStatus" SET DEFAULT 'INACTIVE'`, + ); + await queryRunner.query( + `DROP TYPE "core"."workspace_activationStatus_enum_old"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."workspace" ALTER COLUMN "isGoogleAuthEnabled" SET NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "core"."workspace" ALTER COLUMN "isPasswordAuthEnabled" SET NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "core"."workspace" ALTER COLUMN "isMicrosoftAuthEnabled" SET NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "core"."workspaceSSOIdentityProvider" ADD CONSTRAINT "FK_bc8d8855198de1fbc32fba8df93" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."workspaceSSOIdentityProvider" DROP CONSTRAINT "FK_bc8d8855198de1fbc32fba8df93"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."workspace" ALTER COLUMN "isMicrosoftAuthEnabled" DROP NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "core"."workspace" ALTER COLUMN "isPasswordAuthEnabled" DROP NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "core"."workspace" ALTER COLUMN "isGoogleAuthEnabled" DROP NOT NULL`, + ); + await queryRunner.query( + `CREATE TYPE "core"."workspace_activationStatus_enum_old" AS ENUM('PENDING_CREATION', 'ONGOING_CREATION', 'ACTIVE', 'INACTIVE')`, + ); + await queryRunner.query( + `ALTER TABLE "core"."workspace" ALTER COLUMN "activationStatus" DROP DEFAULT`, + ); + await queryRunner.query( + `ALTER TABLE "core"."workspace" ALTER COLUMN "activationStatus" TYPE "core"."workspace_activationStatus_enum_old" USING "activationStatus"::"text"::"core"."workspace_activationStatus_enum_old"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."workspace" ALTER COLUMN "activationStatus" SET DEFAULT 'INACTIVE'`, + ); + await queryRunner.query( + `DROP TYPE "core"."workspace_activationstatus_enum"`, + ); + await queryRunner.query( + `ALTER TYPE "core"."workspace_activationStatus_enum_old" RENAME TO "workspace_activationStatus_enum"`, + ); + await queryRunner.query( + `CREATE TYPE "core"."idp_type_enum_old" AS ENUM('OIDC', 'SAML')`, + ); + await queryRunner.query( + `ALTER TABLE "core"."workspaceSSOIdentityProvider" ALTER COLUMN "type" DROP DEFAULT`, + ); + await queryRunner.query( + `ALTER TABLE "core"."workspaceSSOIdentityProvider" ALTER COLUMN "type" TYPE "core"."idp_type_enum_old" USING "type"::"text"::"core"."idp_type_enum_old"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."workspaceSSOIdentityProvider" ALTER COLUMN "type" SET DEFAULT 'OIDC'`, + ); + await queryRunner.query( + `DROP TYPE "core"."workspaceSSOIdentityProvider_type_enum"`, + ); + await queryRunner.query( + `ALTER TYPE "core"."idp_type_enum_old" RENAME TO "idp_type_enum"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."workspaceSSOIdentityProvider" DROP COLUMN "status"`, + ); + await queryRunner.query( + `DROP TYPE "core"."workspaceSSOIdentityProvider_status_enum"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."workspaceSSOIdentityProvider" ADD "status" character varying NOT NULL DEFAULT 'Active'`, + ); + await queryRunner.query( + `ALTER TABLE "core"."workspaceSSOIdentityProvider" ALTER COLUMN "name" DROP NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "core"."workspaceSSOIdentityProvider" ADD CONSTRAINT "CHK_SAML" CHECK ((((type = 'SAML'::core.idp_type_enum) AND ("ssoURL" IS NOT NULL) AND (certificate IS NOT NULL)) OR (type = 'OIDC'::core.idp_type_enum)))`, + ); + await queryRunner.query( + `ALTER TABLE "core"."workspaceSSOIdentityProvider" ADD CONSTRAINT "CHK_OIDC" CHECK ((((type = 'OIDC'::core.idp_type_enum) AND ("clientID" IS NOT NULL) AND ("clientSecret" IS NOT NULL)) OR (type = 'SAML'::core.idp_type_enum)))`, + ); + await queryRunner.query( + `ALTER TABLE "core"."appToken" ADD CONSTRAINT "userIdNotNullWhenTypeIsNotInvitation" CHECK (((type = 'INVITATION_TOKEN'::text) OR ("userId" IS NOT NULL)))`, + ); + await queryRunner.query( + `ALTER TABLE "core"."appToken" ADD CONSTRAINT "userIdIsNullWhenTypeIsInvitation" CHECK (((type <> 'INVITATION_TOKEN'::text) OR ("userId" IS NULL)))`, + ); + await queryRunner.query( + `CREATE UNIQUE INDEX "workspace_subdomain_unique_index" ON "core"."workspace" ("subdomain") `, + ); + await queryRunner.query( + `CREATE UNIQUE INDEX "apptoken_unique_invitation_by_user_workspace" ON "core"."appToken" ("workspaceId") WHERE (type = 'INVITATION_TOKEN'::text)`, + ); + await queryRunner.query( + `ALTER TABLE "core"."workspaceSSOIdentityProvider" ADD CONSTRAINT "FK_workspaceId" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + } +} diff --git a/packages/twenty-server/src/database/typeorm/metadata/migrations/1733318004066-missing-shortcut-migration.ts b/packages/twenty-server/src/database/typeorm/metadata/migrations/1733318004066-missing-shortcut-migration.ts new file mode 100644 index 000000000000..d0b7ea867207 --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/metadata/migrations/1733318004066-missing-shortcut-migration.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class MissingShortcutMigration1733318004066 + implements MigrationInterface +{ + name = 'MissingShortcutMigration1733318004066'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `DROP INDEX "metadata"."IDX_objectMetadata_shortcut_upper_workspace"`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE UNIQUE INDEX "IDX_objectMetadata_shortcut_upper_workspace" ON "metadata"."objectMetadata" ("workspaceId") `, + ); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/billing/entities/billing-customer.entity.ts b/packages/twenty-server/src/engine/core-modules/billing/entities/billing-customer.entity.ts index f1b237b7ccc5..3b0b8be801ea 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/entities/billing-customer.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/entities/billing-customer.entity.ts @@ -5,8 +5,6 @@ import { Column, CreateDateColumn, Entity, - JoinColumn, - ManyToOne, OneToMany, PrimaryGeneratedColumn, Relation, @@ -17,7 +15,6 @@ import { import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; import { BillingEntitlement } from 'src/engine/core-modules/billing/entities/billing-entitlement.entity'; import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; -import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; @Entity({ name: 'billingCustomer', schema: 'core' }) @ObjectType('billingCustomer') @@ -39,12 +36,6 @@ export class BillingCustomer { @UpdateDateColumn({ type: 'timestamptz' }) updatedAt: Date; - @ManyToOne(() => Workspace, (workspace) => workspace.billingCustomers, { - onDelete: 'CASCADE', - }) - @JoinColumn() - workspace: Relation; - @Column({ nullable: false, type: 'uuid' }) workspaceId: string; diff --git a/packages/twenty-server/src/engine/core-modules/billing/entities/billing-entitlement.entity.ts b/packages/twenty-server/src/engine/core-modules/billing/entities/billing-entitlement.entity.ts index 927281f3ef0d..84ed85260b08 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/entities/billing-entitlement.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/entities/billing-entitlement.entity.ts @@ -16,7 +16,6 @@ import { import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; import { BillingCustomer } from 'src/engine/core-modules/billing/entities/billing-customer.entity'; import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum'; -import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; @Entity({ name: 'billingEntitlement', schema: 'core' }) @ObjectType('billingEntitlement') @Unique('IndexOnFeatureKeyAndWorkspaceIdUnique', ['key', 'workspaceId']) @@ -33,12 +32,6 @@ export class BillingEntitlement { @Column({ nullable: false, type: 'uuid' }) workspaceId: string; - @ManyToOne(() => Workspace, (workspace) => workspace.billingEntitlements, { - onDelete: 'CASCADE', - }) - @JoinColumn() - workspace: Relation; - @Column({ nullable: false }) stripeCustomerId: string; diff --git a/packages/twenty-server/src/engine/core-modules/billing/entities/billing-subscription.entity.ts b/packages/twenty-server/src/engine/core-modules/billing/entities/billing-subscription.entity.ts index e80d97bd27f2..d2ed20e5bc5c 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/entities/billing-subscription.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/entities/billing-subscription.entity.ts @@ -20,7 +20,6 @@ import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entitie import { BillingSubscriptionCollectionMethod } from 'src/engine/core-modules/billing/enums/billing-subscription-collection-method.enum'; import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum'; import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum'; -import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; registerEnumType(SubscriptionStatus, { name: 'SubscriptionStatus' }); registerEnumType(SubscriptionInterval, { name: 'SubscriptionInterval' }); @@ -41,12 +40,6 @@ export class BillingSubscription { @UpdateDateColumn({ type: 'timestamptz' }) updatedAt: Date; - @ManyToOne(() => Workspace, (workspace) => workspace.billingSubscriptions, { - onDelete: 'CASCADE', - }) - @JoinColumn() - workspace: Relation; - @Column({ nullable: false, type: 'uuid' }) workspaceId: string; 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 b346001067c3..d9f947255784 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,8 +67,8 @@ export class KeyValuePair { value: JSON; @Field(() => String) - @Column({ nullable: false, type: 'text' }) - textValueDeprecated: string; + @Column({ nullable: true, type: 'text' }) + textValueDeprecated: string | null; @Field(() => KeyValuePairType) @Column({ diff --git a/packages/twenty-server/src/engine/core-modules/workspace/workspace.entity.ts b/packages/twenty-server/src/engine/core-modules/workspace/workspace.entity.ts index 0380f4243ac6..b80ecad5ecce 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/workspace.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/workspace.entity.ts @@ -119,24 +119,6 @@ export class Workspace { }) activationStatus: WorkspaceActivationStatus; - @OneToMany( - () => BillingSubscription, - (billingSubscription) => billingSubscription.workspace, - ) - billingSubscriptions: Relation; - - @OneToMany( - () => BillingCustomer, - (billingCustomer) => billingCustomer.workspace, - ) - billingCustomers: Relation; - - @OneToMany( - () => BillingEntitlement, - (billingEntitlement) => billingEntitlement.workspace, - ) - billingEntitlements: Relation; - @OneToMany( () => PostgresCredentials, (postgresCredentials) => postgresCredentials.workspace,