mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-11 00:33:21 +03:00
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:
parent
1cd91e60fa
commit
7fbef6d60d
@ -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,
|
||||
|
@ -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],
|
||||
})
|
||||
|
@ -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 {}
|
||||
|
@ -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],
|
||||
})
|
||||
|
@ -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],
|
||||
})
|
||||
|
@ -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],
|
||||
})
|
||||
|
@ -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],
|
||||
})
|
||||
|
@ -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],
|
||||
})
|
||||
|
@ -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,
|
||||
|
@ -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],
|
||||
})
|
||||
|
@ -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,
|
||||
|
@ -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: {},
|
||||
},
|
||||
],
|
||||
|
@ -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({
|
||||
|
@ -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,
|
||||
|
@ -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],
|
||||
|
@ -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()
|
||||
);
|
||||
`);
|
||||
};
|
||||
|
@ -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],
|
||||
})
|
||||
|
@ -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
|
||||
|
@ -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')
|
||||
|
@ -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<FieldMetadata> {
|
||||
@ -59,13 +59,16 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadata> {
|
||||
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,
|
||||
|
@ -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',
|
||||
},
|
||||
];
|
||||
|
@ -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,
|
||||
],
|
||||
};
|
||||
|
||||
|
@ -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<TenantMigrationTableChange[]>
|
||||
* @returns Promise<TenantMigrationTableAction[]>
|
||||
*/
|
||||
public async executeMigrationFromPendingMigrations(
|
||||
workspaceId: string,
|
||||
): Promise<TenantMigrationTableChange[]> {
|
||||
): Promise<TenantMigrationTableAction[]> {
|
||||
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}`,
|
||||
|
@ -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`,
|
||||
);
|
||||
}
|
||||
}
|
@ -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")`,
|
||||
);
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
@ -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<ObjectMetadata> {
|
||||
override async createOne(record: ObjectMetadata): Promise<ObjectMetadata> {
|
||||
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,
|
||||
],
|
||||
);
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
import companyObject from './companies.metadata.json';
|
||||
|
||||
export const standardObjectsMetadata = {
|
||||
companyV2: companyObject,
|
||||
};
|
@ -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 {}
|
@ -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,
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
@ -0,0 +1,6 @@
|
||||
import { addCompanyTable } from './migrations/1697618009-addCompanyTable';
|
||||
|
||||
// TODO: read the folder and return all migrations
|
||||
export const standardMigrations = {
|
||||
'1697618009-addCompanyTable': addCompanyTable,
|
||||
};
|
@ -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;
|
||||
}
|
||||
|
@ -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],
|
||||
})
|
||||
|
@ -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: {},
|
||||
},
|
||||
],
|
||||
|
@ -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<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
|
||||
@ -22,19 +60,12 @@ export class TenantMigrationService {
|
||||
public async getPendingMigrations(
|
||||
workspaceId: string,
|
||||
): Promise<TenantMigration[]> {
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user