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:
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user