Add Tenant initialisation service (#2100)

* Add Tenant initialisation service

* few fixes

* fix constraint

* fix tests

* update metadata json with employees and address

* add V2

* remove metadata.gql
This commit is contained in:
Weiko
2023-10-18 18:01:52 +02:00
committed by GitHub
parent 1cd91e60fa
commit 7fbef6d60d
37 changed files with 513 additions and 177 deletions

View File

@ -1,4 +1,4 @@
import { Global, Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { AbilityFactory } from 'src/ability/ability.factory'; import { AbilityFactory } from 'src/ability/ability.factory';
import { PrismaService } from 'src/database/prisma.service'; import { PrismaService } from 'src/database/prisma.service';
@ -129,7 +129,6 @@ import {
ReadApiKeyAbilityHandler, ReadApiKeyAbilityHandler,
} from './handlers/api-key.ability-handler'; } from './handlers/api-key.ability-handler';
@Global()
@Module({ @Module({
providers: [ providers: [
AbilityFactory, AbilityFactory,

View File

@ -1,10 +1,14 @@
import { Module } from '@nestjs/common'; 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 { ActivityResolver } from './resolvers/activity.resolver';
import { ActivityService } from './services/activity.service'; import { ActivityService } from './services/activity.service';
import { ActivityTargetService } from './services/activity-target.service'; import { ActivityTargetService } from './services/activity-target.service';
@Module({ @Module({
imports: [AbilityModule, PrismaModule],
providers: [ActivityResolver, ActivityService, ActivityTargetService], providers: [ActivityResolver, ActivityService, ActivityTargetService],
exports: [ActivityService, ActivityTargetService], exports: [ActivityService, ActivityTargetService],
}) })

View File

@ -2,11 +2,14 @@ import { Module } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt'; import { JwtService } from '@nestjs/jwt';
import { TokenService } from 'src/core/auth/services/token.service'; 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 { ApiKeyResolver } from './api-key.resolver';
import { ApiKeyService } from './api-key.service'; import { ApiKeyService } from './api-key.service';
@Module({ @Module({
imports: [AbilityModule, PrismaModule],
providers: [ApiKeyResolver, ApiKeyService, TokenService, JwtService], providers: [ApiKeyResolver, ApiKeyService, TokenService, JwtService],
}) })
export class ApiKeyModule {} export class ApiKeyModule {}

View File

@ -1,11 +1,14 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { FileUploadService } from 'src/core/file/services/file-upload.service'; 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 { AttachmentResolver } from './resolvers/attachment.resolver';
import { AttachmentService } from './services/attachment.service'; import { AttachmentService } from './services/attachment.service';
@Module({ @Module({
imports: [AbilityModule, PrismaModule],
providers: [AttachmentService, AttachmentResolver, FileUploadService], providers: [AttachmentService, AttachmentResolver, FileUploadService],
exports: [AttachmentService], exports: [AttachmentService],
}) })

View File

@ -1,9 +1,13 @@
import { Module } from '@nestjs/common'; 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 { CommentService } from './comment.service';
import { CommentResolver } from './comment.resolver'; import { CommentResolver } from './comment.resolver';
@Module({ @Module({
imports: [AbilityModule, PrismaModule],
providers: [CommentService, CommentResolver], providers: [CommentService, CommentResolver],
exports: [CommentService], exports: [CommentService],
}) })

View File

@ -2,13 +2,15 @@ import { Module } from '@nestjs/common';
import { CommentModule } from 'src/core/comment/comment.module'; import { CommentModule } from 'src/core/comment/comment.module';
import { ActivityModule } from 'src/core/activity/activity.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 { CompanyService } from './company.service';
import { CompanyResolver } from './company.resolver'; import { CompanyResolver } from './company.resolver';
import { CompanyRelationsResolver } from './company-relations.resolver'; import { CompanyRelationsResolver } from './company-relations.resolver';
@Module({ @Module({
imports: [CommentModule, ActivityModule], imports: [CommentModule, ActivityModule, AbilityModule, PrismaModule],
providers: [CompanyService, CompanyResolver, CompanyRelationsResolver], providers: [CompanyService, CompanyResolver, CompanyRelationsResolver],
exports: [CompanyService], exports: [CompanyService],
}) })

View File

@ -1,9 +1,13 @@
import { Module } from '@nestjs/common'; 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 { FavoriteResolver } from './resolvers/favorite.resolver';
import { FavoriteService } from './services/favorite.service'; import { FavoriteService } from './services/favorite.service';
@Module({ @Module({
imports: [AbilityModule, PrismaModule],
providers: [FavoriteService, FavoriteResolver], providers: [FavoriteService, FavoriteResolver],
exports: [FavoriteService], exports: [FavoriteService],
}) })

View File

@ -3,13 +3,21 @@ import { Module } from '@nestjs/common';
import { CommentModule } from 'src/core/comment/comment.module'; import { CommentModule } from 'src/core/comment/comment.module';
import { ActivityModule } from 'src/core/activity/activity.module'; import { ActivityModule } from 'src/core/activity/activity.module';
import { FileModule } from 'src/core/file/file.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 { PersonService } from './person.service';
import { PersonResolver } from './person.resolver'; import { PersonResolver } from './person.resolver';
import { PersonRelationsResolver } from './person-relations.resolver'; import { PersonRelationsResolver } from './person-relations.resolver';
@Module({ @Module({
imports: [CommentModule, ActivityModule, FileModule], imports: [
CommentModule,
ActivityModule,
FileModule,
AbilityModule,
PrismaModule,
],
providers: [PersonService, PersonResolver, PersonRelationsResolver], providers: [PersonService, PersonResolver, PersonRelationsResolver],
exports: [PersonService], exports: [PersonService],
}) })

View File

@ -1,5 +1,8 @@
import { Module } from '@nestjs/common'; 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 { PipelineService } from './services/pipeline.service';
import { PipelineResolver } from './resolvers/pipeline.resolver'; import { PipelineResolver } from './resolvers/pipeline.resolver';
import { PipelineStageResolver } from './resolvers/pipeline-stage.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'; import { PipelineProgressService } from './services/pipeline-progress.service';
@Module({ @Module({
imports: [], imports: [AbilityModule, PrismaModule],
providers: [ providers: [
PipelineService, PipelineService,
PipelineStageService, PipelineStageService,

View File

@ -3,12 +3,20 @@ import { Module } from '@nestjs/common';
import { FileModule } from 'src/core/file/file.module'; import { FileModule } from 'src/core/file/file.module';
import { WorkspaceModule } from 'src/core/workspace/workspace.module'; import { WorkspaceModule } from 'src/core/workspace/workspace.module';
import { EnvironmentModule } from 'src/integrations/environment/environment.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 { UserService } from './user.service';
import { UserResolver } from './user.resolver'; import { UserResolver } from './user.resolver';
@Module({ @Module({
imports: [FileModule, WorkspaceModule, EnvironmentModule], imports: [
FileModule,
WorkspaceModule,
EnvironmentModule,
AbilityModule,
PrismaModule,
],
providers: [UserService, UserResolver], providers: [UserService, UserResolver],
exports: [UserService], exports: [UserService],
}) })

View File

@ -1,5 +1,8 @@
import { Module } from '@nestjs/common'; 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 { ViewFieldService } from './services/view-field.service';
import { ViewFieldResolver } from './resolvers/view-field.resolver'; import { ViewFieldResolver } from './resolvers/view-field.resolver';
import { ViewSortService } from './services/view-sort.service'; 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'; import { ViewFilterResolver } from './resolvers/view-filter.resolver';
@Module({ @Module({
imports: [AbilityModule, PrismaModule],
providers: [ providers: [
ViewService, ViewService,
ViewFieldService, ViewFieldService,

View File

@ -8,7 +8,7 @@ import { PersonService } from 'src/core/person/person.service';
import { CompanyService } from 'src/core/company/company.service'; import { CompanyService } from 'src/core/company/company.service';
import { PipelineProgressService } from 'src/core/pipeline/services/pipeline-progress.service'; import { PipelineProgressService } from 'src/core/pipeline/services/pipeline-progress.service';
import { ViewService } from 'src/core/view/services/view.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'; import { WorkspaceService } from './workspace.service';
@ -48,7 +48,7 @@ describe('WorkspaceService', () => {
useValue: {}, useValue: {},
}, },
{ {
provide: DataSourceService, provide: TenantInitialisationService,
useValue: {}, useValue: {},
}, },
], ],

View File

@ -11,7 +11,7 @@ import { PipelineService } from 'src/core/pipeline/services/pipeline.service';
import { ViewService } from 'src/core/view/services/view.service'; import { ViewService } from 'src/core/view/services/view.service';
import { PrismaService } from 'src/database/prisma.service'; import { PrismaService } from 'src/database/prisma.service';
import { assert } from 'src/utils/assert'; 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() @Injectable()
export class WorkspaceService { export class WorkspaceService {
@ -23,7 +23,7 @@ export class WorkspaceService {
private readonly pipelineStageService: PipelineStageService, private readonly pipelineStageService: PipelineStageService,
private readonly pipelineProgressService: PipelineProgressService, private readonly pipelineProgressService: PipelineProgressService,
private readonly viewService: ViewService, private readonly viewService: ViewService,
private readonly dataSourceService: DataSourceService, private readonly tenantInitialisationService: TenantInitialisationService,
) {} ) {}
// Find // Find
@ -66,7 +66,7 @@ export class WorkspaceService {
}); });
// Create workspace schema // Create workspace schema
await this.dataSourceService.createWorkspaceSchema(workspace.id); await this.tenantInitialisationService.init(workspace.id);
// Create default companies // Create default companies
const companies = await this.companyService.createDefaultCompanies({ const companies = await this.companyService.createDefaultCompanies({

View File

@ -5,7 +5,9 @@ import { PipelineModule } from 'src/core/pipeline/pipeline.module';
import { CompanyModule } from 'src/core/company/company.module'; import { CompanyModule } from 'src/core/company/company.module';
import { PersonModule } from 'src/core/person/person.module'; import { PersonModule } from 'src/core/person/person.module';
import { ViewModule } from 'src/core/view/view.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 { WorkspaceService } from './services/workspace.service';
import { WorkspaceMemberService } from './services/workspace-member.service'; import { WorkspaceMemberService } from './services/workspace-member.service';
@ -14,11 +16,13 @@ import { WorkspaceResolver } from './resolvers/workspace.resolver';
@Module({ @Module({
imports: [ imports: [
AbilityModule,
PipelineModule, PipelineModule,
CompanyModule, CompanyModule,
PersonModule, PersonModule,
ViewModule, ViewModule,
DataSourceModule, TenantInitialisationModule,
PrismaModule,
], ],
providers: [ providers: [
WorkspaceService, WorkspaceService,

View File

@ -1,8 +1,7 @@
import { Global, Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { PrismaService } from './prisma.service'; import { PrismaService } from './prisma.service';
@Global()
@Module({ @Module({
providers: [PrismaService], providers: [PrismaService],
exports: [PrismaService], exports: [PrismaService],

View File

@ -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' '80f5e1e3-574a-4bf9-b5bc-98aedd2b76e6', 'workspace_twenty', 'postgres', 'twenty-dev-7ed9d212-1c25-4d02-bf25-6aeccf7ea420'
) ON CONFLICT DO NOTHING`, ) 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( await prisma.$queryRawUnsafe(
'CREATE SCHEMA IF NOT EXISTS workspace_twenty_7icsva0r6s00mpcp6cwg4w4rd', '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' 'b37b2163-7f63-47a9-b1b3-6c7290ca9fb1', 'workspace_twenty_7icsva0r6s00mpcp6cwg4w4rd', 'postgres', 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419'
) ON CONFLICT DO NOTHING`, ) 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()
);
`);
}; };

View File

@ -1,11 +1,12 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { TerminusModule } from '@nestjs/terminus'; import { TerminusModule } from '@nestjs/terminus';
import { PrismaModule } from 'src/database/prisma.module';
import { HealthController } from 'src/health/health.controller'; import { HealthController } from 'src/health/health.controller';
import { PrismaHealthIndicator } from 'src/health/indicators/prisma-health-indicator'; import { PrismaHealthIndicator } from 'src/health/indicators/prisma-health-indicator';
@Module({ @Module({
imports: [TerminusModule], imports: [TerminusModule, PrismaModule],
controllers: [HealthController], controllers: [HealthController],
providers: [PrismaHealthIndicator], providers: [PrismaHealthIndicator],
}) })

View File

@ -1,6 +1,6 @@
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common'; 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 { EnvironmentService } from 'src/integrations/environment/environment.service';
import { DataSourceMetadataService } from 'src/metadata/data-source-metadata/data-source-metadata.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); const schemaAlreadyExists = await queryRunner.hasSchema(schemaName);
if (schemaAlreadyExists) { if (schemaAlreadyExists) {
return schemaName; throw new Error(`Schema ${schemaName} already exists`);
} }
await queryRunner.createSchema(schemaName, true); await queryRunner.createSchema(schemaName, true);
await this.createMigrationTable(queryRunner, schemaName);
await queryRunner.release();
await this.dataSourceMetadataService.createDataSourceMetadata(
workspaceId,
schemaName,
);
return 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. * Connects to a workspace data source using the workspace metadata. Returns a cached connection if it exists.
* @param workspaceId * @param workspaceId

View File

@ -8,6 +8,7 @@ import {
JoinColumn, JoinColumn,
ManyToOne, ManyToOne,
PrimaryGeneratedColumn, PrimaryGeneratedColumn,
Unique,
UpdateDateColumn, UpdateDateColumn,
} from 'typeorm'; } from 'typeorm';
import { import {
@ -38,6 +39,7 @@ export type FieldMetadataTargetColumnMap = {
disableFilter: true, disableFilter: true,
disableSort: true, disableSort: true,
}) })
@Unique('IndexOnNameAndWorkspaceIdUnique', ['name', 'objectId', 'workspaceId'])
export class FieldMetadata { export class FieldMetadata {
@IDField(() => ID) @IDField(() => ID)
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')

View File

@ -10,13 +10,13 @@ import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity'; import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity';
import { import {
convertFieldMetadataToColumnChanges, convertFieldMetadataToColumnActions,
generateTargetColumnMap, generateTargetColumnMap,
} from 'src/metadata/field-metadata/utils/field-metadata.util'; } from 'src/metadata/field-metadata/utils/field-metadata.util';
import { MigrationRunnerService } from 'src/metadata/migration-runner/migration-runner.service'; import { MigrationRunnerService } from 'src/metadata/migration-runner/migration-runner.service';
import { TenantMigrationService } from 'src/metadata/tenant-migration/tenant-migration.service'; import { TenantMigrationService } from 'src/metadata/tenant-migration/tenant-migration.service';
import { ObjectMetadataService } from 'src/metadata/object-metadata/services/object-metadata.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() @Injectable()
export class FieldMetadataService extends TypeOrmQueryService<FieldMetadata> { export class FieldMetadataService extends TypeOrmQueryService<FieldMetadata> {
@ -59,13 +59,16 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadata> {
targetColumnMap: generateTargetColumnMap(record.type), targetColumnMap: generateTargetColumnMap(record.type),
}); });
await this.tenantMigrationService.createMigration(record.workspaceId, [ await this.tenantMigrationService.createCustomMigration(
{ record.workspaceId,
name: objectMetadata.targetTableName, [
change: 'alter', {
columns: convertFieldMetadataToColumnChanges(createdFieldMetadata), name: objectMetadata.targetTableName,
} satisfies TenantMigrationTableChange, action: 'alter',
]); columns: convertFieldMetadataToColumnActions(createdFieldMetadata),
} satisfies TenantMigrationTableAction,
],
);
await this.migrationRunnerService.executeMigrationFromPendingMigrations( await this.migrationRunnerService.executeMigrationFromPendingMigrations(
record.workspaceId, record.workspaceId,

View File

@ -5,7 +5,7 @@ import {
FieldMetadata, FieldMetadata,
FieldMetadataTargetColumnMap, FieldMetadataTargetColumnMap,
} from 'src/metadata/field-metadata/field-metadata.entity'; } 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. * 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, fieldMetadata: FieldMetadata,
): TenantMigrationColumnChange[] { ): TenantMigrationColumnAction[] {
switch (fieldMetadata.type) { switch (fieldMetadata.type) {
case 'text': case 'text':
return [ return [
{ {
name: fieldMetadata.targetColumnMap.value, name: fieldMetadata.targetColumnMap.value,
change: 'create', action: 'create',
type: 'text', type: 'text',
}, },
]; ];
@ -69,7 +69,7 @@ export function convertFieldMetadataToColumnChanges(
return [ return [
{ {
name: fieldMetadata.targetColumnMap.value, name: fieldMetadata.targetColumnMap.value,
change: 'create', action: 'create',
type: 'varchar', type: 'varchar',
}, },
]; ];
@ -77,7 +77,7 @@ export function convertFieldMetadataToColumnChanges(
return [ return [
{ {
name: fieldMetadata.targetColumnMap.value, name: fieldMetadata.targetColumnMap.value,
change: 'create', action: 'create',
type: 'integer', type: 'integer',
}, },
]; ];
@ -85,7 +85,7 @@ export function convertFieldMetadataToColumnChanges(
return [ return [
{ {
name: fieldMetadata.targetColumnMap.value, name: fieldMetadata.targetColumnMap.value,
change: 'create', action: 'create',
type: 'boolean', type: 'boolean',
}, },
]; ];
@ -93,7 +93,7 @@ export function convertFieldMetadataToColumnChanges(
return [ return [
{ {
name: fieldMetadata.targetColumnMap.value, name: fieldMetadata.targetColumnMap.value,
change: 'create', action: 'create',
type: 'timestamp', type: 'timestamp',
}, },
]; ];
@ -101,12 +101,12 @@ export function convertFieldMetadataToColumnChanges(
return [ return [
{ {
name: fieldMetadata.targetColumnMap.text, name: fieldMetadata.targetColumnMap.text,
change: 'create', action: 'create',
type: 'varchar', type: 'varchar',
}, },
{ {
name: fieldMetadata.targetColumnMap.link, name: fieldMetadata.targetColumnMap.link,
change: 'create', action: 'create',
type: 'varchar', type: 'varchar',
}, },
]; ];
@ -114,12 +114,12 @@ export function convertFieldMetadataToColumnChanges(
return [ return [
{ {
name: fieldMetadata.targetColumnMap.amount, name: fieldMetadata.targetColumnMap.amount,
change: 'create', action: 'create',
type: 'integer', type: 'integer',
}, },
{ {
name: fieldMetadata.targetColumnMap.currency, name: fieldMetadata.targetColumnMap.currency,
change: 'create', action: 'create',
type: 'varchar', type: 'varchar',
}, },
]; ];

View File

@ -11,6 +11,8 @@ import { MetadataNameLabelRefactoring1697126636202 } from './migrations/16971266
import { RemoveFieldMetadataPlaceholder1697471445015 } from './migrations/1697471445015-removeFieldMetadataPlaceholder'; import { RemoveFieldMetadataPlaceholder1697471445015 } from './migrations/1697471445015-removeFieldMetadataPlaceholder';
import { AddSoftDelete1697474804403 } from './migrations/1697474804403-addSoftDelete'; import { AddSoftDelete1697474804403 } from './migrations/1697474804403-addSoftDelete';
import { RemoveSingularPluralFromFieldLabelAndName1697534910933 } from './migrations/1697534910933-removeSingularPluralFromFieldLabelAndName'; import { RemoveSingularPluralFromFieldLabelAndName1697534910933 } from './migrations/1697534910933-removeSingularPluralFromFieldLabelAndName';
import { AddNameAndIsCustomToTenantMigration1697622715467 } from './migrations/1697622715467-addNameAndIsCustomToTenantMigration';
import { AddUniqueConstraintsOnFieldObjectMetadata1697630766924 } from './migrations/1697630766924-addUniqueConstraintsOnFieldObjectMetadata';
config(); config();
@ -33,6 +35,8 @@ export const typeORMMetadataModuleOptions: TypeOrmModuleOptions = {
RemoveFieldMetadataPlaceholder1697471445015, RemoveFieldMetadataPlaceholder1697471445015,
AddSoftDelete1697474804403, AddSoftDelete1697474804403,
RemoveSingularPluralFromFieldLabelAndName1697534910933, RemoveSingularPluralFromFieldLabelAndName1697534910933,
AddNameAndIsCustomToTenantMigration1697622715467,
AddUniqueConstraintsOnFieldObjectMetadata1697630766924,
], ],
}; };

View File

@ -4,8 +4,8 @@ import { QueryRunner, Table, TableColumn } from 'typeorm';
import { DataSourceService } from 'src/metadata/data-source/data-source.service'; import { DataSourceService } from 'src/metadata/data-source/data-source.service';
import { import {
TenantMigrationTableChange, TenantMigrationTableAction,
TenantMigrationColumnChange, TenantMigrationColumnAction,
} from 'src/metadata/tenant-migration/tenant-migration.entity'; } from 'src/metadata/tenant-migration/tenant-migration.entity';
import { TenantMigrationService } from 'src/metadata/tenant-migration/tenant-migration.service'; import { TenantMigrationService } from 'src/metadata/tenant-migration/tenant-migration.service';
@ -20,11 +20,11 @@ export class MigrationRunnerService {
* Executes pending migrations for a given workspace * Executes pending migrations for a given workspace
* *
* @param workspaceId string * @param workspaceId string
* @returns Promise<TenantMigrationTableChange[]> * @returns Promise<TenantMigrationTableAction[]>
*/ */
public async executeMigrationFromPendingMigrations( public async executeMigrationFromPendingMigrations(
workspaceId: string, workspaceId: string,
): Promise<TenantMigrationTableChange[]> { ): Promise<TenantMigrationTableAction[]> {
const workspaceDataSource = const workspaceDataSource =
await this.dataSourceService.connectToWorkspaceDataSource(workspaceId); await this.dataSourceService.connectToWorkspaceDataSource(workspaceId);
@ -35,7 +35,7 @@ export class MigrationRunnerService {
const pendingMigrations = const pendingMigrations =
await this.tenantMigrationService.getPendingMigrations(workspaceId); await this.tenantMigrationService.getPendingMigrations(workspaceId);
const flattenedPendingMigrations: TenantMigrationTableChange[] = const flattenedPendingMigrations: TenantMigrationTableAction[] =
pendingMigrations.reduce((acc, pendingMigration) => { pendingMigrations.reduce((acc, pendingMigration) => {
return [...acc, ...pendingMigration.migrations]; return [...acc, ...pendingMigration.migrations];
}, []); }, []);
@ -71,9 +71,9 @@ export class MigrationRunnerService {
private async handleTableChanges( private async handleTableChanges(
queryRunner: QueryRunner, queryRunner: QueryRunner,
schemaName: string, schemaName: string,
tableMigration: TenantMigrationTableChange, tableMigration: TenantMigrationTableAction,
) { ) {
switch (tableMigration.change) { switch (tableMigration.action) {
case 'create': case 'create':
await this.createTable(queryRunner, schemaName, tableMigration.name); await this.createTable(queryRunner, schemaName, tableMigration.name);
break; break;
@ -87,7 +87,7 @@ export class MigrationRunnerService {
break; break;
default: default:
throw new Error( 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 queryRunner QueryRunner
* @param schemaName string * @param schemaName string
* @param tableName string * @param tableName string
* @param columnMigrations TenantMigrationColumnChange[] * @param columnMigrations TenantMigrationColumnAction[]
* @returns * @returns
*/ */
private async handleColumnChanges( private async handleColumnChanges(
queryRunner: QueryRunner, queryRunner: QueryRunner,
schemaName: string, schemaName: string,
tableName: string, tableName: string,
columnMigrations?: TenantMigrationColumnChange[], columnMigrations?: TenantMigrationColumnAction[],
) { ) {
if (!columnMigrations || columnMigrations.length === 0) { if (!columnMigrations || columnMigrations.length === 0) {
return; return;
} }
for (const columnMigration of columnMigrations) { for (const columnMigration of columnMigrations) {
switch (columnMigration.change) { switch (columnMigration.action) {
case 'create': case 'create':
await this.createColumn( await this.createColumn(
queryRunner, queryRunner,
@ -165,13 +165,9 @@ export class MigrationRunnerService {
columnMigration, columnMigration,
); );
break; break;
case 'alter':
throw new Error(
`Migration column change ${columnMigration.change} not supported`,
);
default: default:
throw new Error( 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 queryRunner QueryRunner
* @param schemaName string * @param schemaName string
* @param tableName string * @param tableName string
* @param migrationColumn TenantMigrationColumnChange * @param migrationColumn TenantMigrationColumnAction
*/ */
private async createColumn( private async createColumn(
queryRunner: QueryRunner, queryRunner: QueryRunner,
schemaName: string, schemaName: string,
tableName: string, tableName: string,
migrationColumn: TenantMigrationColumnChange, migrationColumn: TenantMigrationColumnAction,
) { ) {
await queryRunner.addColumn( await queryRunner.addColumn(
`${schemaName}.${tableName}`, `${schemaName}.${tableName}`,

View File

@ -0,0 +1,55 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddNameAndIsCustomToTenantMigration1697622715467
implements MigrationInterface
{
name = 'AddNameAndIsCustomToTenantMigration1697622715467';
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
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`,
);
}
}

View File

@ -0,0 +1,43 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddUniqueConstraintsOnFieldObjectMetadata1697630766924
implements MigrationInterface
{
name = 'AddUniqueConstraintsOnFieldObjectMetadata1697630766924';
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
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")`,
);
}
}

View File

@ -7,6 +7,7 @@ import {
Entity, Entity,
OneToMany, OneToMany,
PrimaryGeneratedColumn, PrimaryGeneratedColumn,
Unique,
UpdateDateColumn, UpdateDateColumn,
} from 'typeorm'; } from 'typeorm';
import { import {
@ -36,6 +37,11 @@ import { BeforeCreateOneObject } from './hooks/before-create-one-object.hook';
disableSort: true, disableSort: true,
}) })
@CursorConnection('fields', () => FieldMetadata) @CursorConnection('fields', () => FieldMetadata)
@Unique('IndexOnNameSingularAndWorkspaceIdUnique', [
'nameSingular',
'workspaceId',
])
@Unique('IndexOnNamePluralAndWorkspaceIdUnique', ['namePlural', 'workspaceId'])
export class ObjectMetadata { export class ObjectMetadata {
@IDField(() => ID) @IDField(() => ID)
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
@ -46,11 +52,11 @@ export class ObjectMetadata {
dataSourceId: string; dataSourceId: string;
@Field() @Field()
@Column({ nullable: false, name: 'name_singular', unique: true }) @Column({ nullable: false, name: 'name_singular' })
nameSingular: string; nameSingular: string;
@Field() @Field()
@Column({ nullable: false, name: 'name_plural', unique: true }) @Column({ nullable: false, name: 'name_plural' })
namePlural: string; namePlural: string;
@Field() @Field()

View File

@ -5,7 +5,7 @@ import { Repository } from 'typeorm';
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
import { TenantMigrationService } from 'src/metadata/tenant-migration/tenant-migration.service'; 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 { MigrationRunnerService } from 'src/metadata/migration-runner/migration-runner.service';
import { ObjectMetadata } from 'src/metadata/object-metadata/object-metadata.entity'; import { ObjectMetadata } from 'src/metadata/object-metadata/object-metadata.entity';
@ -24,13 +24,13 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadata> {
override async createOne(record: ObjectMetadata): Promise<ObjectMetadata> { override async createOne(record: ObjectMetadata): Promise<ObjectMetadata> {
const createdObjectMetadata = await super.createOne(record); const createdObjectMetadata = await super.createOne(record);
await this.tenantMigrationService.createMigration( await this.tenantMigrationService.createCustomMigration(
createdObjectMetadata.workspaceId, createdObjectMetadata.workspaceId,
[ [
{ {
name: createdObjectMetadata.targetTableName, name: createdObjectMetadata.targetTableName,
change: 'create', action: 'create',
} satisfies TenantMigrationTableChange, } satisfies TenantMigrationTableAction,
], ],
); );

View File

@ -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
}
]
}

View File

@ -0,0 +1,5 @@
import companyObject from './companies.metadata.json';
export const standardObjectsMetadata = {
companyV2: companyObject,
};

View File

@ -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 {}

View File

@ -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<void>
*/
public async init(workspaceId: string): Promise<void> {
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,
}),
),
),
);
}
}

View File

@ -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',
},
],
},
];

View File

@ -0,0 +1,6 @@
import { addCompanyTable } from './migrations/1697618009-addCompanyTable';
// TODO: read the folder and return all migrations
export const standardMigrations = {
'1697618009-addCompanyTable': addCompanyTable,
};

View File

@ -5,29 +5,37 @@ import {
PrimaryGeneratedColumn, PrimaryGeneratedColumn,
} from 'typeorm'; } from 'typeorm';
export type TenantMigrationColumnChange = { export type TenantMigrationColumnAction = {
name: string; name: string;
type: string; type: string;
change: 'create' | 'alter'; action: 'create';
}; };
export type TenantMigrationTableChange = { export type TenantMigrationTableAction = {
name: string; name: string;
change: 'create' | 'alter'; action: 'create' | 'alter';
columns?: TenantMigrationColumnChange[]; columns?: TenantMigrationColumnAction[];
}; };
@Entity('tenant_migrations') @Entity('tenant_migrations')
export class TenantMigration { export class TenantMigration {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
id: string; id: string;
@Column({ nullable: true, type: 'jsonb' }) @Column({ nullable: true, type: 'jsonb' })
migrations: TenantMigrationTableChange[]; migrations: TenantMigrationTableAction[];
@Column({ nullable: true, name: 'applied_at' }) @Column({ nullable: true })
appliedAt: Date; name: string;
@CreateDateColumn({ name: 'created_at' }) @Column({ default: false })
isCustom: boolean;
@Column({ nullable: true })
appliedAt?: Date;
@Column()
workspaceId: string;
@CreateDateColumn()
createdAt: Date; createdAt: Date;
} }

View File

@ -1,11 +1,11 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { DataSourceModule } from 'src/metadata/data-source/data-source.module';
import { TenantMigrationService } from './tenant-migration.service'; import { TenantMigrationService } from './tenant-migration.service';
import { TenantMigration } from './tenant-migration.entity';
@Module({ @Module({
imports: [DataSourceModule], imports: [TypeOrmModule.forFeature([TenantMigration], 'metadata')],
exports: [TenantMigrationService], exports: [TenantMigrationService],
providers: [TenantMigrationService], providers: [TenantMigrationService],
}) })

View File

@ -1,8 +1,8 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { DataSourceService } from 'src/metadata/data-source/data-source.service';
import { TenantMigrationService } from './tenant-migration.service'; import { TenantMigrationService } from './tenant-migration.service';
import { TenantMigration } from './tenant-migration.entity';
describe('TenantMigrationService', () => { describe('TenantMigrationService', () => {
let service: TenantMigrationService; let service: TenantMigrationService;
@ -12,7 +12,7 @@ describe('TenantMigrationService', () => {
providers: [ providers: [
TenantMigrationService, TenantMigrationService,
{ {
provide: DataSourceService, provide: getRepositoryToken(TenantMigration, 'metadata'),
useValue: {}, useValue: {},
}, },
], ],

View File

@ -1,17 +1,55 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { IsNull } from 'typeorm'; import { IsNull, Repository } from 'typeorm';
import { DataSourceService } from 'src/metadata/data-source/data-source.service';
import { import {
TenantMigration, TenantMigration,
TenantMigrationTableChange, TenantMigrationTableAction,
} from './tenant-migration.entity'; } from './tenant-migration.entity';
import { standardMigrations } from './standard-migrations';
@Injectable() @Injectable()
export class TenantMigrationService { export class TenantMigrationService {
constructor(private readonly dataSourceService: DataSourceService) {} constructor(
@InjectRepository(TenantMigration, 'metadata')
private readonly tenantMigrationRepository: Repository<TenantMigration>,
) {}
/**
* 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 * Get all pending migrations for a given workspaceId
@ -22,19 +60,12 @@ export class TenantMigrationService {
public async getPendingMigrations( public async getPendingMigrations(
workspaceId: string, workspaceId: string,
): Promise<TenantMigration[]> { ): Promise<TenantMigration[]> {
const workspaceDataSource = return this.tenantMigrationRepository.find({
await this.dataSourceService.connectToWorkspaceDataSource(workspaceId);
if (!workspaceDataSource) {
throw new Error('Workspace data source not found');
}
const tenantMigrationRepository =
workspaceDataSource.getRepository(TenantMigration);
return tenantMigrationRepository.find({
order: { createdAt: 'ASC' }, order: { createdAt: 'ASC' },
where: { appliedAt: IsNull() }, where: {
appliedAt: IsNull(),
workspaceId,
},
}); });
} }
@ -49,17 +80,7 @@ export class TenantMigrationService {
workspaceId: string, workspaceId: string,
migration: TenantMigration, migration: TenantMigration,
) { ) {
const workspaceDataSource = await this.tenantMigrationRepository.save({
await this.dataSourceService.connectToWorkspaceDataSource(workspaceId);
if (!workspaceDataSource) {
throw new Error('Workspace data source not found');
}
const tenantMigrationRepository =
workspaceDataSource.getRepository(TenantMigration);
await tenantMigrationRepository.save({
id: migration.id, id: migration.id,
appliedAt: new Date(), appliedAt: new Date(),
}); });
@ -71,22 +92,14 @@ export class TenantMigrationService {
* @param workspaceId * @param workspaceId
* @param migrations * @param migrations
*/ */
public async createMigration( public async createCustomMigration(
workspaceId: string, workspaceId: string,
migrations: TenantMigrationTableChange[], migrations: TenantMigrationTableAction[],
) { ) {
const workspaceDataSource = await this.tenantMigrationRepository.save({
await this.dataSourceService.connectToWorkspaceDataSource(workspaceId);
if (!workspaceDataSource) {
throw new Error('Workspace data source not found');
}
const tenantMigrationRepository =
workspaceDataSource.getRepository(TenantMigration);
await tenantMigrationRepository.save({
migrations, migrations,
workspaceId,
isCustom: true,
}); });
} }
} }