diff --git a/server/src/ability/ability.module.ts b/server/src/ability/ability.module.ts index f1aaa1d6b9..2ff46a8446 100644 --- a/server/src/ability/ability.module.ts +++ b/server/src/ability/ability.module.ts @@ -1,4 +1,4 @@ -import { Global, Module } from '@nestjs/common'; +import { Module } from '@nestjs/common'; import { AbilityFactory } from 'src/ability/ability.factory'; import { PrismaService } from 'src/database/prisma.service'; @@ -129,7 +129,6 @@ import { ReadApiKeyAbilityHandler, } from './handlers/api-key.ability-handler'; -@Global() @Module({ providers: [ AbilityFactory, diff --git a/server/src/core/activity/activity.module.ts b/server/src/core/activity/activity.module.ts index 2c161f022f..e058fd45f6 100644 --- a/server/src/core/activity/activity.module.ts +++ b/server/src/core/activity/activity.module.ts @@ -1,10 +1,14 @@ import { Module } from '@nestjs/common'; +import { AbilityModule } from 'src/ability/ability.module'; +import { PrismaModule } from 'src/database/prisma.module'; + import { ActivityResolver } from './resolvers/activity.resolver'; import { ActivityService } from './services/activity.service'; import { ActivityTargetService } from './services/activity-target.service'; @Module({ + imports: [AbilityModule, PrismaModule], providers: [ActivityResolver, ActivityService, ActivityTargetService], exports: [ActivityService, ActivityTargetService], }) diff --git a/server/src/core/api-key/api-key.module.ts b/server/src/core/api-key/api-key.module.ts index c716a412b5..4f0073d458 100644 --- a/server/src/core/api-key/api-key.module.ts +++ b/server/src/core/api-key/api-key.module.ts @@ -2,11 +2,14 @@ import { Module } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { TokenService } from 'src/core/auth/services/token.service'; +import { AbilityModule } from 'src/ability/ability.module'; +import { PrismaModule } from 'src/database/prisma.module'; import { ApiKeyResolver } from './api-key.resolver'; import { ApiKeyService } from './api-key.service'; @Module({ + imports: [AbilityModule, PrismaModule], providers: [ApiKeyResolver, ApiKeyService, TokenService, JwtService], }) export class ApiKeyModule {} diff --git a/server/src/core/attachment/attachment.module.ts b/server/src/core/attachment/attachment.module.ts index 1350ddf952..59595bd06e 100644 --- a/server/src/core/attachment/attachment.module.ts +++ b/server/src/core/attachment/attachment.module.ts @@ -1,11 +1,14 @@ import { Module } from '@nestjs/common'; import { FileUploadService } from 'src/core/file/services/file-upload.service'; +import { AbilityModule } from 'src/ability/ability.module'; +import { PrismaModule } from 'src/database/prisma.module'; import { AttachmentResolver } from './resolvers/attachment.resolver'; import { AttachmentService } from './services/attachment.service'; @Module({ + imports: [AbilityModule, PrismaModule], providers: [AttachmentService, AttachmentResolver, FileUploadService], exports: [AttachmentService], }) diff --git a/server/src/core/comment/comment.module.ts b/server/src/core/comment/comment.module.ts index 9b23cebc6e..8a2937584f 100644 --- a/server/src/core/comment/comment.module.ts +++ b/server/src/core/comment/comment.module.ts @@ -1,9 +1,13 @@ import { Module } from '@nestjs/common'; +import { AbilityModule } from 'src/ability/ability.module'; +import { PrismaModule } from 'src/database/prisma.module'; + import { CommentService } from './comment.service'; import { CommentResolver } from './comment.resolver'; @Module({ + imports: [AbilityModule, PrismaModule], providers: [CommentService, CommentResolver], exports: [CommentService], }) diff --git a/server/src/core/company/company.module.ts b/server/src/core/company/company.module.ts index 95c498547a..fd757a8ffa 100644 --- a/server/src/core/company/company.module.ts +++ b/server/src/core/company/company.module.ts @@ -2,13 +2,15 @@ import { Module } from '@nestjs/common'; import { CommentModule } from 'src/core/comment/comment.module'; import { ActivityModule } from 'src/core/activity/activity.module'; +import { AbilityModule } from 'src/ability/ability.module'; +import { PrismaModule } from 'src/database/prisma.module'; import { CompanyService } from './company.service'; import { CompanyResolver } from './company.resolver'; import { CompanyRelationsResolver } from './company-relations.resolver'; @Module({ - imports: [CommentModule, ActivityModule], + imports: [CommentModule, ActivityModule, AbilityModule, PrismaModule], providers: [CompanyService, CompanyResolver, CompanyRelationsResolver], exports: [CompanyService], }) diff --git a/server/src/core/favorite/favorite.module.ts b/server/src/core/favorite/favorite.module.ts index 07e68561e3..eb7349bf33 100644 --- a/server/src/core/favorite/favorite.module.ts +++ b/server/src/core/favorite/favorite.module.ts @@ -1,9 +1,13 @@ import { Module } from '@nestjs/common'; +import { AbilityModule } from 'src/ability/ability.module'; +import { PrismaModule } from 'src/database/prisma.module'; + import { FavoriteResolver } from './resolvers/favorite.resolver'; import { FavoriteService } from './services/favorite.service'; @Module({ + imports: [AbilityModule, PrismaModule], providers: [FavoriteService, FavoriteResolver], exports: [FavoriteService], }) diff --git a/server/src/core/person/person.module.ts b/server/src/core/person/person.module.ts index 42faed52f9..45d153bea2 100644 --- a/server/src/core/person/person.module.ts +++ b/server/src/core/person/person.module.ts @@ -3,13 +3,21 @@ import { Module } from '@nestjs/common'; import { CommentModule } from 'src/core/comment/comment.module'; import { ActivityModule } from 'src/core/activity/activity.module'; import { FileModule } from 'src/core/file/file.module'; +import { AbilityModule } from 'src/ability/ability.module'; +import { PrismaModule } from 'src/database/prisma.module'; import { PersonService } from './person.service'; import { PersonResolver } from './person.resolver'; import { PersonRelationsResolver } from './person-relations.resolver'; @Module({ - imports: [CommentModule, ActivityModule, FileModule], + imports: [ + CommentModule, + ActivityModule, + FileModule, + AbilityModule, + PrismaModule, + ], providers: [PersonService, PersonResolver, PersonRelationsResolver], exports: [PersonService], }) diff --git a/server/src/core/pipeline/pipeline.module.ts b/server/src/core/pipeline/pipeline.module.ts index 820c0998d6..b04c3a0306 100644 --- a/server/src/core/pipeline/pipeline.module.ts +++ b/server/src/core/pipeline/pipeline.module.ts @@ -1,5 +1,8 @@ import { Module } from '@nestjs/common'; +import { AbilityModule } from 'src/ability/ability.module'; +import { PrismaModule } from 'src/database/prisma.module'; + import { PipelineService } from './services/pipeline.service'; import { PipelineResolver } from './resolvers/pipeline.resolver'; import { PipelineStageResolver } from './resolvers/pipeline-stage.resolver'; @@ -8,7 +11,7 @@ import { PipelineStageService } from './services/pipeline-stage.service'; import { PipelineProgressService } from './services/pipeline-progress.service'; @Module({ - imports: [], + imports: [AbilityModule, PrismaModule], providers: [ PipelineService, PipelineStageService, diff --git a/server/src/core/user/user.module.ts b/server/src/core/user/user.module.ts index 3c0baa51f4..cbefde321b 100644 --- a/server/src/core/user/user.module.ts +++ b/server/src/core/user/user.module.ts @@ -3,12 +3,20 @@ import { Module } from '@nestjs/common'; import { FileModule } from 'src/core/file/file.module'; import { WorkspaceModule } from 'src/core/workspace/workspace.module'; import { EnvironmentModule } from 'src/integrations/environment/environment.module'; +import { AbilityModule } from 'src/ability/ability.module'; +import { PrismaModule } from 'src/database/prisma.module'; import { UserService } from './user.service'; import { UserResolver } from './user.resolver'; @Module({ - imports: [FileModule, WorkspaceModule, EnvironmentModule], + imports: [ + FileModule, + WorkspaceModule, + EnvironmentModule, + AbilityModule, + PrismaModule, + ], providers: [UserService, UserResolver], exports: [UserService], }) diff --git a/server/src/core/view/view.module.ts b/server/src/core/view/view.module.ts index 9cd8d16c3d..b5b5fbce70 100644 --- a/server/src/core/view/view.module.ts +++ b/server/src/core/view/view.module.ts @@ -1,5 +1,8 @@ import { Module } from '@nestjs/common'; +import { AbilityModule } from 'src/ability/ability.module'; +import { PrismaModule } from 'src/database/prisma.module'; + import { ViewFieldService } from './services/view-field.service'; import { ViewFieldResolver } from './resolvers/view-field.resolver'; import { ViewSortService } from './services/view-sort.service'; @@ -10,6 +13,7 @@ import { ViewFilterService } from './services/view-filter.service'; import { ViewFilterResolver } from './resolvers/view-filter.resolver'; @Module({ + imports: [AbilityModule, PrismaModule], providers: [ ViewService, ViewFieldService, diff --git a/server/src/core/workspace/services/workspace.service.spec.ts b/server/src/core/workspace/services/workspace.service.spec.ts index 0f8718160a..e6c511ddc4 100644 --- a/server/src/core/workspace/services/workspace.service.spec.ts +++ b/server/src/core/workspace/services/workspace.service.spec.ts @@ -8,7 +8,7 @@ import { PersonService } from 'src/core/person/person.service'; import { CompanyService } from 'src/core/company/company.service'; import { PipelineProgressService } from 'src/core/pipeline/services/pipeline-progress.service'; import { ViewService } from 'src/core/view/services/view.service'; -import { DataSourceService } from 'src/metadata/data-source/data-source.service'; +import { TenantInitialisationService } from 'src/metadata/tenant-initialisation/tenant-initialisation.service'; import { WorkspaceService } from './workspace.service'; @@ -48,7 +48,7 @@ describe('WorkspaceService', () => { useValue: {}, }, { - provide: DataSourceService, + provide: TenantInitialisationService, useValue: {}, }, ], diff --git a/server/src/core/workspace/services/workspace.service.ts b/server/src/core/workspace/services/workspace.service.ts index f35fd9b7cb..ab4f4d1a4a 100644 --- a/server/src/core/workspace/services/workspace.service.ts +++ b/server/src/core/workspace/services/workspace.service.ts @@ -11,7 +11,7 @@ import { PipelineService } from 'src/core/pipeline/services/pipeline.service'; import { ViewService } from 'src/core/view/services/view.service'; import { PrismaService } from 'src/database/prisma.service'; import { assert } from 'src/utils/assert'; -import { DataSourceService } from 'src/metadata/data-source/data-source.service'; +import { TenantInitialisationService } from 'src/metadata/tenant-initialisation/tenant-initialisation.service'; @Injectable() export class WorkspaceService { @@ -23,7 +23,7 @@ export class WorkspaceService { private readonly pipelineStageService: PipelineStageService, private readonly pipelineProgressService: PipelineProgressService, private readonly viewService: ViewService, - private readonly dataSourceService: DataSourceService, + private readonly tenantInitialisationService: TenantInitialisationService, ) {} // Find @@ -66,7 +66,7 @@ export class WorkspaceService { }); // Create workspace schema - await this.dataSourceService.createWorkspaceSchema(workspace.id); + await this.tenantInitialisationService.init(workspace.id); // Create default companies const companies = await this.companyService.createDefaultCompanies({ diff --git a/server/src/core/workspace/workspace.module.ts b/server/src/core/workspace/workspace.module.ts index b4c2c3af7d..12639e20c3 100644 --- a/server/src/core/workspace/workspace.module.ts +++ b/server/src/core/workspace/workspace.module.ts @@ -5,7 +5,9 @@ import { PipelineModule } from 'src/core/pipeline/pipeline.module'; import { CompanyModule } from 'src/core/company/company.module'; import { PersonModule } from 'src/core/person/person.module'; import { ViewModule } from 'src/core/view/view.module'; -import { DataSourceModule } from 'src/metadata/data-source/data-source.module'; +import { TenantInitialisationModule } from 'src/metadata/tenant-initialisation/tenant-initialisation.module'; +import { AbilityModule } from 'src/ability/ability.module'; +import { PrismaModule } from 'src/database/prisma.module'; import { WorkspaceService } from './services/workspace.service'; import { WorkspaceMemberService } from './services/workspace-member.service'; @@ -14,11 +16,13 @@ import { WorkspaceResolver } from './resolvers/workspace.resolver'; @Module({ imports: [ + AbilityModule, PipelineModule, CompanyModule, PersonModule, ViewModule, - DataSourceModule, + TenantInitialisationModule, + PrismaModule, ], providers: [ WorkspaceService, diff --git a/server/src/database/prisma.module.ts b/server/src/database/prisma.module.ts index d30ce68b3b..54327ba02b 100644 --- a/server/src/database/prisma.module.ts +++ b/server/src/database/prisma.module.ts @@ -1,8 +1,7 @@ -import { Global, Module } from '@nestjs/common'; +import { Module } from '@nestjs/common'; import { PrismaService } from './prisma.service'; -@Global() @Module({ providers: [PrismaService], exports: [PrismaService], diff --git a/server/src/database/seeds/workspaces.ts b/server/src/database/seeds/workspaces.ts index 025dbcbb49..6b770b1239 100644 --- a/server/src/database/seeds/workspaces.ts +++ b/server/src/database/seeds/workspaces.ts @@ -33,14 +33,6 @@ export const seedWorkspaces = async (prisma: PrismaClient) => { '80f5e1e3-574a-4bf9-b5bc-98aedd2b76e6', 'workspace_twenty', 'postgres', 'twenty-dev-7ed9d212-1c25-4d02-bf25-6aeccf7ea420' ) ON CONFLICT DO NOTHING`, ); - await prisma.$queryRawUnsafe(` - CREATE TABLE IF NOT EXISTS workspace_twenty.tenant_migrations ( - id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, - migrations JSONB, - applied_at TIMESTAMP NULL, - created_at TIMESTAMP DEFAULT NOW() - ); - `); await prisma.$queryRawUnsafe( 'CREATE SCHEMA IF NOT EXISTS workspace_twenty_7icsva0r6s00mpcp6cwg4w4rd', @@ -53,12 +45,4 @@ export const seedWorkspaces = async (prisma: PrismaClient) => { 'b37b2163-7f63-47a9-b1b3-6c7290ca9fb1', 'workspace_twenty_7icsva0r6s00mpcp6cwg4w4rd', 'postgres', 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419' ) ON CONFLICT DO NOTHING`, ); - await prisma.$queryRawUnsafe(` - CREATE TABLE IF NOT EXISTS workspace_twenty_7icsva0r6s00mpcp6cwg4w4rd.tenant_migrations ( - id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, - migrations JSONB, - applied_at TIMESTAMP NULL, - created_at TIMESTAMP DEFAULT NOW() - ); - `); }; diff --git a/server/src/health/health.module.ts b/server/src/health/health.module.ts index 0aa2275820..17bbcc6ad3 100644 --- a/server/src/health/health.module.ts +++ b/server/src/health/health.module.ts @@ -1,11 +1,12 @@ import { Module } from '@nestjs/common'; import { TerminusModule } from '@nestjs/terminus'; +import { PrismaModule } from 'src/database/prisma.module'; import { HealthController } from 'src/health/health.controller'; import { PrismaHealthIndicator } from 'src/health/indicators/prisma-health-indicator'; @Module({ - imports: [TerminusModule], + imports: [TerminusModule, PrismaModule], controllers: [HealthController], providers: [PrismaHealthIndicator], }) diff --git a/server/src/metadata/data-source/data-source.service.ts b/server/src/metadata/data-source/data-source.service.ts index 5db3a800ab..cf60a5110c 100644 --- a/server/src/metadata/data-source/data-source.service.ts +++ b/server/src/metadata/data-source/data-source.service.ts @@ -1,6 +1,6 @@ import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common'; -import { DataSource, QueryRunner, Table } from 'typeorm'; +import { DataSource } from 'typeorm'; import { EnvironmentService } from 'src/integrations/environment/environment.service'; import { DataSourceMetadataService } from 'src/metadata/data-source-metadata/data-source-metadata.service'; @@ -37,55 +37,14 @@ export class DataSourceService implements OnModuleInit, OnModuleDestroy { const schemaAlreadyExists = await queryRunner.hasSchema(schemaName); if (schemaAlreadyExists) { - return schemaName; + throw new Error(`Schema ${schemaName} already exists`); } await queryRunner.createSchema(schemaName, true); - await this.createMigrationTable(queryRunner, schemaName); - await queryRunner.release(); - - await this.dataSourceMetadataService.createDataSourceMetadata( - workspaceId, - schemaName, - ); return schemaName; } - private async createMigrationTable( - queryRunner: QueryRunner, - schemaName: string, - ) { - await queryRunner.createTable( - new Table({ - name: 'tenant_migrations', - schema: schemaName, - columns: [ - { - name: 'id', - type: 'uuid', - isPrimary: true, - default: 'uuid_generate_v4()', - }, - { - name: 'migrations', - type: 'jsonb', - }, - { - name: 'applied_at', - type: 'timestamp', - isNullable: true, - }, - { - name: 'created_at', - type: 'timestamp', - default: 'now()', - }, - ], - }), - ); - } - /** * Connects to a workspace data source using the workspace metadata. Returns a cached connection if it exists. * @param workspaceId diff --git a/server/src/metadata/field-metadata/field-metadata.entity.ts b/server/src/metadata/field-metadata/field-metadata.entity.ts index 9bf9e94e91..89f68ee3ca 100644 --- a/server/src/metadata/field-metadata/field-metadata.entity.ts +++ b/server/src/metadata/field-metadata/field-metadata.entity.ts @@ -8,6 +8,7 @@ import { JoinColumn, ManyToOne, PrimaryGeneratedColumn, + Unique, UpdateDateColumn, } from 'typeorm'; import { @@ -38,6 +39,7 @@ export type FieldMetadataTargetColumnMap = { disableFilter: true, disableSort: true, }) +@Unique('IndexOnNameAndWorkspaceIdUnique', ['name', 'objectId', 'workspaceId']) export class FieldMetadata { @IDField(() => ID) @PrimaryGeneratedColumn('uuid') diff --git a/server/src/metadata/field-metadata/services/field-metadata.service.ts b/server/src/metadata/field-metadata/services/field-metadata.service.ts index 051adce369..83f8fcf431 100644 --- a/server/src/metadata/field-metadata/services/field-metadata.service.ts +++ b/server/src/metadata/field-metadata/services/field-metadata.service.ts @@ -10,13 +10,13 @@ import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity'; import { - convertFieldMetadataToColumnChanges, + convertFieldMetadataToColumnActions, generateTargetColumnMap, } from 'src/metadata/field-metadata/utils/field-metadata.util'; import { MigrationRunnerService } from 'src/metadata/migration-runner/migration-runner.service'; import { TenantMigrationService } from 'src/metadata/tenant-migration/tenant-migration.service'; import { ObjectMetadataService } from 'src/metadata/object-metadata/services/object-metadata.service'; -import { TenantMigrationTableChange } from 'src/metadata/tenant-migration/tenant-migration.entity'; +import { TenantMigrationTableAction } from 'src/metadata/tenant-migration/tenant-migration.entity'; @Injectable() export class FieldMetadataService extends TypeOrmQueryService { @@ -59,13 +59,16 @@ export class FieldMetadataService extends TypeOrmQueryService { targetColumnMap: generateTargetColumnMap(record.type), }); - await this.tenantMigrationService.createMigration(record.workspaceId, [ - { - name: objectMetadata.targetTableName, - change: 'alter', - columns: convertFieldMetadataToColumnChanges(createdFieldMetadata), - } satisfies TenantMigrationTableChange, - ]); + await this.tenantMigrationService.createCustomMigration( + record.workspaceId, + [ + { + name: objectMetadata.targetTableName, + action: 'alter', + columns: convertFieldMetadataToColumnActions(createdFieldMetadata), + } satisfies TenantMigrationTableAction, + ], + ); await this.migrationRunnerService.executeMigrationFromPendingMigrations( record.workspaceId, diff --git a/server/src/metadata/field-metadata/utils/field-metadata.util.ts b/server/src/metadata/field-metadata/utils/field-metadata.util.ts index 65e0c48998..1fd0ab5875 100644 --- a/server/src/metadata/field-metadata/utils/field-metadata.util.ts +++ b/server/src/metadata/field-metadata/utils/field-metadata.util.ts @@ -5,7 +5,7 @@ import { FieldMetadata, FieldMetadataTargetColumnMap, } from 'src/metadata/field-metadata/field-metadata.entity'; -import { TenantMigrationColumnChange } from 'src/metadata/tenant-migration/tenant-migration.entity'; +import { TenantMigrationColumnAction } from 'src/metadata/tenant-migration/tenant-migration.entity'; /** * Generate a column name from a field name removing unsupported characters. @@ -52,15 +52,15 @@ export function generateTargetColumnMap( } } -export function convertFieldMetadataToColumnChanges( +export function convertFieldMetadataToColumnActions( fieldMetadata: FieldMetadata, -): TenantMigrationColumnChange[] { +): TenantMigrationColumnAction[] { switch (fieldMetadata.type) { case 'text': return [ { name: fieldMetadata.targetColumnMap.value, - change: 'create', + action: 'create', type: 'text', }, ]; @@ -69,7 +69,7 @@ export function convertFieldMetadataToColumnChanges( return [ { name: fieldMetadata.targetColumnMap.value, - change: 'create', + action: 'create', type: 'varchar', }, ]; @@ -77,7 +77,7 @@ export function convertFieldMetadataToColumnChanges( return [ { name: fieldMetadata.targetColumnMap.value, - change: 'create', + action: 'create', type: 'integer', }, ]; @@ -85,7 +85,7 @@ export function convertFieldMetadataToColumnChanges( return [ { name: fieldMetadata.targetColumnMap.value, - change: 'create', + action: 'create', type: 'boolean', }, ]; @@ -93,7 +93,7 @@ export function convertFieldMetadataToColumnChanges( return [ { name: fieldMetadata.targetColumnMap.value, - change: 'create', + action: 'create', type: 'timestamp', }, ]; @@ -101,12 +101,12 @@ export function convertFieldMetadataToColumnChanges( return [ { name: fieldMetadata.targetColumnMap.text, - change: 'create', + action: 'create', type: 'varchar', }, { name: fieldMetadata.targetColumnMap.link, - change: 'create', + action: 'create', type: 'varchar', }, ]; @@ -114,12 +114,12 @@ export function convertFieldMetadataToColumnChanges( return [ { name: fieldMetadata.targetColumnMap.amount, - change: 'create', + action: 'create', type: 'integer', }, { name: fieldMetadata.targetColumnMap.currency, - change: 'create', + action: 'create', type: 'varchar', }, ]; diff --git a/server/src/metadata/metadata.datasource.ts b/server/src/metadata/metadata.datasource.ts index 6960dc150d..7f9d9a49e0 100644 --- a/server/src/metadata/metadata.datasource.ts +++ b/server/src/metadata/metadata.datasource.ts @@ -11,6 +11,8 @@ import { MetadataNameLabelRefactoring1697126636202 } from './migrations/16971266 import { RemoveFieldMetadataPlaceholder1697471445015 } from './migrations/1697471445015-removeFieldMetadataPlaceholder'; import { AddSoftDelete1697474804403 } from './migrations/1697474804403-addSoftDelete'; import { RemoveSingularPluralFromFieldLabelAndName1697534910933 } from './migrations/1697534910933-removeSingularPluralFromFieldLabelAndName'; +import { AddNameAndIsCustomToTenantMigration1697622715467 } from './migrations/1697622715467-addNameAndIsCustomToTenantMigration'; +import { AddUniqueConstraintsOnFieldObjectMetadata1697630766924 } from './migrations/1697630766924-addUniqueConstraintsOnFieldObjectMetadata'; config(); @@ -33,6 +35,8 @@ export const typeORMMetadataModuleOptions: TypeOrmModuleOptions = { RemoveFieldMetadataPlaceholder1697471445015, AddSoftDelete1697474804403, RemoveSingularPluralFromFieldLabelAndName1697534910933, + AddNameAndIsCustomToTenantMigration1697622715467, + AddUniqueConstraintsOnFieldObjectMetadata1697630766924, ], }; diff --git a/server/src/metadata/migration-runner/migration-runner.service.ts b/server/src/metadata/migration-runner/migration-runner.service.ts index 45a7b340b3..c93c793018 100644 --- a/server/src/metadata/migration-runner/migration-runner.service.ts +++ b/server/src/metadata/migration-runner/migration-runner.service.ts @@ -4,8 +4,8 @@ import { QueryRunner, Table, TableColumn } from 'typeorm'; import { DataSourceService } from 'src/metadata/data-source/data-source.service'; import { - TenantMigrationTableChange, - TenantMigrationColumnChange, + TenantMigrationTableAction, + TenantMigrationColumnAction, } from 'src/metadata/tenant-migration/tenant-migration.entity'; import { TenantMigrationService } from 'src/metadata/tenant-migration/tenant-migration.service'; @@ -20,11 +20,11 @@ export class MigrationRunnerService { * Executes pending migrations for a given workspace * * @param workspaceId string - * @returns Promise + * @returns Promise */ public async executeMigrationFromPendingMigrations( workspaceId: string, - ): Promise { + ): Promise { const workspaceDataSource = await this.dataSourceService.connectToWorkspaceDataSource(workspaceId); @@ -35,7 +35,7 @@ export class MigrationRunnerService { const pendingMigrations = await this.tenantMigrationService.getPendingMigrations(workspaceId); - const flattenedPendingMigrations: TenantMigrationTableChange[] = + const flattenedPendingMigrations: TenantMigrationTableAction[] = pendingMigrations.reduce((acc, pendingMigration) => { return [...acc, ...pendingMigration.migrations]; }, []); @@ -71,9 +71,9 @@ export class MigrationRunnerService { private async handleTableChanges( queryRunner: QueryRunner, schemaName: string, - tableMigration: TenantMigrationTableChange, + tableMigration: TenantMigrationTableAction, ) { - switch (tableMigration.change) { + switch (tableMigration.action) { case 'create': await this.createTable(queryRunner, schemaName, tableMigration.name); break; @@ -87,7 +87,7 @@ export class MigrationRunnerService { break; default: throw new Error( - `Migration table change ${tableMigration.change} not supported`, + `Migration table action ${tableMigration.action} not supported`, ); } } @@ -142,21 +142,21 @@ export class MigrationRunnerService { * @param queryRunner QueryRunner * @param schemaName string * @param tableName string - * @param columnMigrations TenantMigrationColumnChange[] + * @param columnMigrations TenantMigrationColumnAction[] * @returns */ private async handleColumnChanges( queryRunner: QueryRunner, schemaName: string, tableName: string, - columnMigrations?: TenantMigrationColumnChange[], + columnMigrations?: TenantMigrationColumnAction[], ) { if (!columnMigrations || columnMigrations.length === 0) { return; } for (const columnMigration of columnMigrations) { - switch (columnMigration.change) { + switch (columnMigration.action) { case 'create': await this.createColumn( queryRunner, @@ -165,13 +165,9 @@ export class MigrationRunnerService { columnMigration, ); break; - case 'alter': - throw new Error( - `Migration column change ${columnMigration.change} not supported`, - ); default: throw new Error( - `Migration column change ${columnMigration.change} not supported`, + `Migration column action ${columnMigration.action} not supported`, ); } } @@ -183,13 +179,13 @@ export class MigrationRunnerService { * @param queryRunner QueryRunner * @param schemaName string * @param tableName string - * @param migrationColumn TenantMigrationColumnChange + * @param migrationColumn TenantMigrationColumnAction */ private async createColumn( queryRunner: QueryRunner, schemaName: string, tableName: string, - migrationColumn: TenantMigrationColumnChange, + migrationColumn: TenantMigrationColumnAction, ) { await queryRunner.addColumn( `${schemaName}.${tableName}`, diff --git a/server/src/metadata/migrations/1697622715467-addNameAndIsCustomToTenantMigration.ts b/server/src/metadata/migrations/1697622715467-addNameAndIsCustomToTenantMigration.ts new file mode 100644 index 0000000000..7e828d9f2a --- /dev/null +++ b/server/src/metadata/migrations/1697622715467-addNameAndIsCustomToTenantMigration.ts @@ -0,0 +1,55 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddNameAndIsCustomToTenantMigration1697622715467 + implements MigrationInterface +{ + name = 'AddNameAndIsCustomToTenantMigration1697622715467'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "metadata"."tenant_migrations" DROP COLUMN "applied_at"`, + ); + await queryRunner.query( + `ALTER TABLE "metadata"."tenant_migrations" DROP COLUMN "created_at"`, + ); + await queryRunner.query( + `ALTER TABLE "metadata"."tenant_migrations" ADD "name" character varying`, + ); + await queryRunner.query( + `ALTER TABLE "metadata"."tenant_migrations" ADD "isCustom" boolean NOT NULL DEFAULT false`, + ); + await queryRunner.query( + `ALTER TABLE "metadata"."tenant_migrations" ADD "appliedAt" TIMESTAMP`, + ); + await queryRunner.query( + `ALTER TABLE "metadata"."tenant_migrations" ADD "workspaceId" character varying NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "metadata"."tenant_migrations" ADD "createdAt" TIMESTAMP NOT NULL DEFAULT now()`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "metadata"."tenant_migrations" DROP COLUMN "createdAt"`, + ); + await queryRunner.query( + `ALTER TABLE "metadata"."tenant_migrations" DROP COLUMN "workspaceId"`, + ); + await queryRunner.query( + `ALTER TABLE "metadata"."tenant_migrations" DROP COLUMN "appliedAt"`, + ); + await queryRunner.query( + `ALTER TABLE "metadata"."tenant_migrations" DROP COLUMN "isCustom"`, + ); + await queryRunner.query( + `ALTER TABLE "metadata"."tenant_migrations" DROP COLUMN "name"`, + ); + await queryRunner.query( + `ALTER TABLE "metadata"."tenant_migrations" ADD "created_at" TIMESTAMP NOT NULL DEFAULT now()`, + ); + await queryRunner.query( + `ALTER TABLE "metadata"."tenant_migrations" ADD "applied_at" TIMESTAMP`, + ); + } +} diff --git a/server/src/metadata/migrations/1697630766924-addUniqueConstraintsOnFieldObjectMetadata.ts b/server/src/metadata/migrations/1697630766924-addUniqueConstraintsOnFieldObjectMetadata.ts new file mode 100644 index 0000000000..cf1b580372 --- /dev/null +++ b/server/src/metadata/migrations/1697630766924-addUniqueConstraintsOnFieldObjectMetadata.ts @@ -0,0 +1,43 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddUniqueConstraintsOnFieldObjectMetadata1697630766924 + implements MigrationInterface +{ + name = 'AddUniqueConstraintsOnFieldObjectMetadata1697630766924'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "metadata"."object_metadata" DROP CONSTRAINT "UQ_8b063d2a685474dbae56cd685d2"`, + ); + await queryRunner.query( + `ALTER TABLE "metadata"."object_metadata" DROP CONSTRAINT "UQ_a2387e1b21120110b7e3db83da1"`, + ); + await queryRunner.query( + `ALTER TABLE "metadata"."field_metadata" ADD CONSTRAINT "IndexOnNameObjectIdAndWorkspaceIdUnique" UNIQUE ("name", "object_id", "workspace_id")`, + ); + await queryRunner.query( + `ALTER TABLE "metadata"."object_metadata" ADD CONSTRAINT "IndexOnNamePluralAndWorkspaceIdUnique" UNIQUE ("name_plural", "workspace_id")`, + ); + await queryRunner.query( + `ALTER TABLE "metadata"."object_metadata" ADD CONSTRAINT "IndexOnNameSingularAndWorkspaceIdUnique" UNIQUE ("name_singular", "workspace_id")`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "metadata"."object_metadata" DROP CONSTRAINT "IndexOnNameSingularAndWorkspaceIdUnique"`, + ); + await queryRunner.query( + `ALTER TABLE "metadata"."object_metadata" DROP CONSTRAINT "IndexOnNamePluralAndWorkspaceIdUnique"`, + ); + await queryRunner.query( + `ALTER TABLE "metadata"."field_metadata" DROP CONSTRAINT "IndexOnNameAndWorkspaceIdUnique"`, + ); + await queryRunner.query( + `ALTER TABLE "metadata"."object_metadata" ADD CONSTRAINT "UQ_a2387e1b21120110b7e3db83da1" UNIQUE ("name_plural")`, + ); + await queryRunner.query( + `ALTER TABLE "metadata"."object_metadata" ADD CONSTRAINT "UQ_8b063d2a685474dbae56cd685d2" UNIQUE ("name_singular")`, + ); + } +} diff --git a/server/src/metadata/object-metadata/object-metadata.entity.ts b/server/src/metadata/object-metadata/object-metadata.entity.ts index a26c3ab9ed..e501ae762d 100644 --- a/server/src/metadata/object-metadata/object-metadata.entity.ts +++ b/server/src/metadata/object-metadata/object-metadata.entity.ts @@ -7,6 +7,7 @@ import { Entity, OneToMany, PrimaryGeneratedColumn, + Unique, UpdateDateColumn, } from 'typeorm'; import { @@ -36,6 +37,11 @@ import { BeforeCreateOneObject } from './hooks/before-create-one-object.hook'; disableSort: true, }) @CursorConnection('fields', () => FieldMetadata) +@Unique('IndexOnNameSingularAndWorkspaceIdUnique', [ + 'nameSingular', + 'workspaceId', +]) +@Unique('IndexOnNamePluralAndWorkspaceIdUnique', ['namePlural', 'workspaceId']) export class ObjectMetadata { @IDField(() => ID) @PrimaryGeneratedColumn('uuid') @@ -46,11 +52,11 @@ export class ObjectMetadata { dataSourceId: string; @Field() - @Column({ nullable: false, name: 'name_singular', unique: true }) + @Column({ nullable: false, name: 'name_singular' }) nameSingular: string; @Field() - @Column({ nullable: false, name: 'name_plural', unique: true }) + @Column({ nullable: false, name: 'name_plural' }) namePlural: string; @Field() diff --git a/server/src/metadata/object-metadata/services/object-metadata.service.ts b/server/src/metadata/object-metadata/services/object-metadata.service.ts index f88ef5d36b..f1648a29cc 100644 --- a/server/src/metadata/object-metadata/services/object-metadata.service.ts +++ b/server/src/metadata/object-metadata/services/object-metadata.service.ts @@ -5,7 +5,7 @@ import { Repository } from 'typeorm'; import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; import { TenantMigrationService } from 'src/metadata/tenant-migration/tenant-migration.service'; -import { TenantMigrationTableChange } from 'src/metadata/tenant-migration/tenant-migration.entity'; +import { TenantMigrationTableAction } from 'src/metadata/tenant-migration/tenant-migration.entity'; import { MigrationRunnerService } from 'src/metadata/migration-runner/migration-runner.service'; import { ObjectMetadata } from 'src/metadata/object-metadata/object-metadata.entity'; @@ -24,13 +24,13 @@ export class ObjectMetadataService extends TypeOrmQueryService { override async createOne(record: ObjectMetadata): Promise { const createdObjectMetadata = await super.createOne(record); - await this.tenantMigrationService.createMigration( + await this.tenantMigrationService.createCustomMigration( createdObjectMetadata.workspaceId, [ { name: createdObjectMetadata.targetTableName, - change: 'create', - } satisfies TenantMigrationTableChange, + action: 'create', + } satisfies TenantMigrationTableAction, ], ); diff --git a/server/src/metadata/tenant-initialisation/standard-objects/companies.metadata.json b/server/src/metadata/tenant-initialisation/standard-objects/companies.metadata.json new file mode 100644 index 0000000000..3838af5e35 --- /dev/null +++ b/server/src/metadata/tenant-initialisation/standard-objects/companies.metadata.json @@ -0,0 +1,55 @@ +{ + "nameSingular": "companyV2", + "namePlural": "companiesV2", + "labelSingular": "Company", + "labelPlural": "Companies", + "targetTableName": "company", + "description": "A company", + "icon": "business", + "fields": [ + { + "type": "text", + "name": "name", + "label": "Name", + "targetColumnMap": { + "value": "name" + }, + "description": "Name of the company", + "icon": null, + "isNullable": false + }, + { + "type": "text", + "name": "domainName", + "label": "Domain Name", + "targetColumnMap": { + "value": "domainName" + }, + "description": "Domain name of the company", + "icon": "url", + "isNullable": true + }, + { + "type": "text", + "name": "address", + "label": "Address", + "targetColumnMap": { + "value": "address" + }, + "description": "Address of the company", + "icon": "location", + "isNullable": true + }, + { + "type": "number", + "name": "employees", + "label": "Employees", + "targetColumnMap": { + "value": "employees" + }, + "description": "Number of employees", + "icon": "people", + "isNullable": true + } + ] +} \ No newline at end of file diff --git a/server/src/metadata/tenant-initialisation/standard-objects/standard-object-metadata.ts b/server/src/metadata/tenant-initialisation/standard-objects/standard-object-metadata.ts new file mode 100644 index 0000000000..04166f63fd --- /dev/null +++ b/server/src/metadata/tenant-initialisation/standard-objects/standard-object-metadata.ts @@ -0,0 +1,5 @@ +import companyObject from './companies.metadata.json'; + +export const standardObjectsMetadata = { + companyV2: companyObject, +}; diff --git a/server/src/metadata/tenant-initialisation/tenant-initialisation.module.ts b/server/src/metadata/tenant-initialisation/tenant-initialisation.module.ts new file mode 100644 index 0000000000..61b976ebb7 --- /dev/null +++ b/server/src/metadata/tenant-initialisation/tenant-initialisation.module.ts @@ -0,0 +1,24 @@ +import { Module } from '@nestjs/common'; + +import { DataSourceModule } from 'src/metadata/data-source/data-source.module'; +import { MigrationRunnerModule } from 'src/metadata/migration-runner/migration-runner.module'; +import { TenantMigrationModule } from 'src/metadata/tenant-migration/tenant-migration.module'; +import { FieldMetadataModule } from 'src/metadata/field-metadata/field-metadata.module'; +import { ObjectMetadataModule } from 'src/metadata/object-metadata/object-metadata.module'; +import { DataSourceMetadataModule } from 'src/metadata/data-source-metadata/data-source-metadata.module'; + +import { TenantInitialisationService } from './tenant-initialisation.service'; + +@Module({ + imports: [ + DataSourceModule, + TenantMigrationModule, + MigrationRunnerModule, + ObjectMetadataModule, + FieldMetadataModule, + DataSourceMetadataModule, + ], + exports: [TenantInitialisationService], + providers: [TenantInitialisationService], +}) +export class TenantInitialisationModule {} diff --git a/server/src/metadata/tenant-initialisation/tenant-initialisation.service.ts b/server/src/metadata/tenant-initialisation/tenant-initialisation.service.ts new file mode 100644 index 0000000000..02dfdea362 --- /dev/null +++ b/server/src/metadata/tenant-initialisation/tenant-initialisation.service.ts @@ -0,0 +1,93 @@ +import { Injectable } from '@nestjs/common'; + +import { TenantMigrationService } from 'src/metadata/tenant-migration/tenant-migration.service'; +import { MigrationRunnerService } from 'src/metadata/migration-runner/migration-runner.service'; +import { DataSourceService } from 'src/metadata/data-source/data-source.service'; +import { FieldMetadataService } from 'src/metadata/field-metadata/services/field-metadata.service'; +import { ObjectMetadataService } from 'src/metadata/object-metadata/services/object-metadata.service'; +import { DataSourceMetadataService } from 'src/metadata/data-source-metadata/data-source-metadata.service'; +import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity'; +import { ObjectMetadata } from 'src/metadata/object-metadata/object-metadata.entity'; + +import { standardObjectsMetadata } from './standard-objects/standard-object-metadata'; + +@Injectable() +export class TenantInitialisationService { + constructor( + private readonly dataSourceService: DataSourceService, + private readonly tenantMigrationService: TenantMigrationService, + private readonly migrationRunnerService: MigrationRunnerService, + private readonly objectMetadataService: ObjectMetadataService, + private readonly fieldMetadataService: FieldMetadataService, + private readonly dataSourceMetadataService: DataSourceMetadataService, + ) {} + + /** + * Init a workspace by creating a new data source and running all migrations + * @param workspaceId + * @returns Promise + */ + public async init(workspaceId: string): Promise { + const schemaName = await this.dataSourceService.createWorkspaceSchema( + workspaceId, + ); + + const dataSourceMetadata = + await this.dataSourceMetadataService.createDataSourceMetadata( + workspaceId, + schemaName, + ); + + await this.tenantMigrationService.insertStandardMigrations(workspaceId); + + // Todo: keep in mind that we don't handle concurrency issues such as migrations being created at the same time + // but it shouldn't be the role of this service to handle this kind of issues for now. + // To check later when we run this in a job. + await this.migrationRunnerService.executeMigrationFromPendingMigrations( + workspaceId, + ); + + await this.createObjectsAndFieldsMetadata( + dataSourceMetadata.id, + workspaceId, + ); + } + + /** + * + * Create all standard objects and fields metadata for a given workspace + * + * @param dataSourceMetadataId + * @param workspaceId + */ + private async createObjectsAndFieldsMetadata( + dataSourceMetadataId: string, + workspaceId: string, + ) { + const createdObjectMetadata = await this.objectMetadataService.createMany( + Object.values(standardObjectsMetadata).map((objectMetadata) => ({ + ...objectMetadata, + dataSourceId: dataSourceMetadataId, + fields: [], + workspaceId, + isCustom: false, + isActive: true, + })), + ); + + await this.fieldMetadataService.createMany( + createdObjectMetadata.flatMap((objectMetadata: ObjectMetadata) => + standardObjectsMetadata[objectMetadata.nameSingular].fields.map( + (field: FieldMetadata) => ({ + ...field, + objectId: objectMetadata.id, + dataSourceId: dataSourceMetadataId, + workspaceId, + isCustom: false, + isActive: true, + }), + ), + ), + ); + } +} diff --git a/server/src/metadata/tenant-migration/migrations/1697618009-addCompanyTable.ts b/server/src/metadata/tenant-migration/migrations/1697618009-addCompanyTable.ts new file mode 100644 index 0000000000..a132299863 --- /dev/null +++ b/server/src/metadata/tenant-migration/migrations/1697618009-addCompanyTable.ts @@ -0,0 +1,34 @@ +import { TenantMigrationTableAction } from 'src/metadata/tenant-migration/tenant-migration.entity'; + +export const addCompanyTable: TenantMigrationTableAction[] = [ + { + name: 'company', + action: 'create', + }, + { + name: 'company', + action: 'alter', + columns: [ + { + name: 'name', + type: 'varchar', + action: 'create', + }, + { + name: 'domainName', + type: 'varchar', + action: 'create', + }, + { + name: 'address', + type: 'varchar', + action: 'create', + }, + { + name: 'employees', + type: 'integer', + action: 'create', + }, + ], + }, +]; diff --git a/server/src/metadata/tenant-migration/standard-migrations.ts b/server/src/metadata/tenant-migration/standard-migrations.ts new file mode 100644 index 0000000000..47b9dca597 --- /dev/null +++ b/server/src/metadata/tenant-migration/standard-migrations.ts @@ -0,0 +1,6 @@ +import { addCompanyTable } from './migrations/1697618009-addCompanyTable'; + +// TODO: read the folder and return all migrations +export const standardMigrations = { + '1697618009-addCompanyTable': addCompanyTable, +}; diff --git a/server/src/metadata/tenant-migration/tenant-migration.entity.ts b/server/src/metadata/tenant-migration/tenant-migration.entity.ts index ca287e6b90..39e586d07d 100644 --- a/server/src/metadata/tenant-migration/tenant-migration.entity.ts +++ b/server/src/metadata/tenant-migration/tenant-migration.entity.ts @@ -5,29 +5,37 @@ import { PrimaryGeneratedColumn, } from 'typeorm'; -export type TenantMigrationColumnChange = { +export type TenantMigrationColumnAction = { name: string; type: string; - change: 'create' | 'alter'; + action: 'create'; }; -export type TenantMigrationTableChange = { +export type TenantMigrationTableAction = { name: string; - change: 'create' | 'alter'; - columns?: TenantMigrationColumnChange[]; + action: 'create' | 'alter'; + columns?: TenantMigrationColumnAction[]; }; - @Entity('tenant_migrations') export class TenantMigration { @PrimaryGeneratedColumn('uuid') id: string; @Column({ nullable: true, type: 'jsonb' }) - migrations: TenantMigrationTableChange[]; + migrations: TenantMigrationTableAction[]; - @Column({ nullable: true, name: 'applied_at' }) - appliedAt: Date; + @Column({ nullable: true }) + name: string; - @CreateDateColumn({ name: 'created_at' }) + @Column({ default: false }) + isCustom: boolean; + + @Column({ nullable: true }) + appliedAt?: Date; + + @Column() + workspaceId: string; + + @CreateDateColumn() createdAt: Date; } diff --git a/server/src/metadata/tenant-migration/tenant-migration.module.ts b/server/src/metadata/tenant-migration/tenant-migration.module.ts index 1496d535ca..4bd73b39ad 100644 --- a/server/src/metadata/tenant-migration/tenant-migration.module.ts +++ b/server/src/metadata/tenant-migration/tenant-migration.module.ts @@ -1,11 +1,11 @@ import { Module } from '@nestjs/common'; - -import { DataSourceModule } from 'src/metadata/data-source/data-source.module'; +import { TypeOrmModule } from '@nestjs/typeorm'; import { TenantMigrationService } from './tenant-migration.service'; +import { TenantMigration } from './tenant-migration.entity'; @Module({ - imports: [DataSourceModule], + imports: [TypeOrmModule.forFeature([TenantMigration], 'metadata')], exports: [TenantMigrationService], providers: [TenantMigrationService], }) diff --git a/server/src/metadata/tenant-migration/tenant-migration.service.spec.ts b/server/src/metadata/tenant-migration/tenant-migration.service.spec.ts index e8aeccf038..371fbdbadc 100644 --- a/server/src/metadata/tenant-migration/tenant-migration.service.spec.ts +++ b/server/src/metadata/tenant-migration/tenant-migration.service.spec.ts @@ -1,8 +1,8 @@ import { Test, TestingModule } from '@nestjs/testing'; - -import { DataSourceService } from 'src/metadata/data-source/data-source.service'; +import { getRepositoryToken } from '@nestjs/typeorm'; import { TenantMigrationService } from './tenant-migration.service'; +import { TenantMigration } from './tenant-migration.entity'; describe('TenantMigrationService', () => { let service: TenantMigrationService; @@ -12,7 +12,7 @@ describe('TenantMigrationService', () => { providers: [ TenantMigrationService, { - provide: DataSourceService, + provide: getRepositoryToken(TenantMigration, 'metadata'), useValue: {}, }, ], diff --git a/server/src/metadata/tenant-migration/tenant-migration.service.ts b/server/src/metadata/tenant-migration/tenant-migration.service.ts index 1806898dca..36074d3977 100644 --- a/server/src/metadata/tenant-migration/tenant-migration.service.ts +++ b/server/src/metadata/tenant-migration/tenant-migration.service.ts @@ -1,17 +1,55 @@ import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; -import { IsNull } from 'typeorm'; - -import { DataSourceService } from 'src/metadata/data-source/data-source.service'; +import { IsNull, Repository } from 'typeorm'; import { TenantMigration, - TenantMigrationTableChange, + TenantMigrationTableAction, } from './tenant-migration.entity'; +import { standardMigrations } from './standard-migrations'; @Injectable() export class TenantMigrationService { - constructor(private readonly dataSourceService: DataSourceService) {} + constructor( + @InjectRepository(TenantMigration, 'metadata') + private readonly tenantMigrationRepository: Repository, + ) {} + + /** + * Insert all standard migrations that have not been inserted yet + * + * @param workspaceId + */ + public async insertStandardMigrations(workspaceId: string) { + // TODO: we actually don't need to fetch all of them, to improve later so it scales well. + const insertedStandardMigrations = + await this.tenantMigrationRepository.find({ + where: { workspaceId, isCustom: false }, + }); + + const insertedStandardMigrationsMapByName = + insertedStandardMigrations.reduce((acc, migration) => { + acc[migration.name] = migration; + return acc; + }, {}); + + const standardMigrationsList = standardMigrations; + + const standardMigrationsListThatNeedToBeInserted = Object.entries( + standardMigrationsList, + ) + .filter(([name]) => !insertedStandardMigrationsMapByName[name]) + .map(([name, migrations]) => ({ name, migrations })); + + await this.tenantMigrationRepository.save( + standardMigrationsListThatNeedToBeInserted.map((migration) => ({ + ...migration, + workspaceId, + isCustom: false, + })), + ); + } /** * Get all pending migrations for a given workspaceId @@ -22,19 +60,12 @@ export class TenantMigrationService { public async getPendingMigrations( workspaceId: string, ): Promise { - const workspaceDataSource = - await this.dataSourceService.connectToWorkspaceDataSource(workspaceId); - - if (!workspaceDataSource) { - throw new Error('Workspace data source not found'); - } - - const tenantMigrationRepository = - workspaceDataSource.getRepository(TenantMigration); - - return tenantMigrationRepository.find({ + return this.tenantMigrationRepository.find({ order: { createdAt: 'ASC' }, - where: { appliedAt: IsNull() }, + where: { + appliedAt: IsNull(), + workspaceId, + }, }); } @@ -49,17 +80,7 @@ export class TenantMigrationService { workspaceId: string, migration: TenantMigration, ) { - const workspaceDataSource = - await this.dataSourceService.connectToWorkspaceDataSource(workspaceId); - - if (!workspaceDataSource) { - throw new Error('Workspace data source not found'); - } - - const tenantMigrationRepository = - workspaceDataSource.getRepository(TenantMigration); - - await tenantMigrationRepository.save({ + await this.tenantMigrationRepository.save({ id: migration.id, appliedAt: new Date(), }); @@ -71,22 +92,14 @@ export class TenantMigrationService { * @param workspaceId * @param migrations */ - public async createMigration( + public async createCustomMigration( workspaceId: string, - migrations: TenantMigrationTableChange[], + migrations: TenantMigrationTableAction[], ) { - const workspaceDataSource = - await this.dataSourceService.connectToWorkspaceDataSource(workspaceId); - - if (!workspaceDataSource) { - throw new Error('Workspace data source not found'); - } - - const tenantMigrationRepository = - workspaceDataSource.getRepository(TenantMigration); - - await tenantMigrationRepository.save({ + await this.tenantMigrationRepository.save({ migrations, + workspaceId, + isCustom: true, }); } }