Add Relation Metadata (#2388)

* Add Relation Metadata

* remove logs

* fix migrations

* add one-to-many relation inside entities

* fix relation

* use enum for tenant migration column action type
This commit is contained in:
Weiko
2023-11-08 09:39:44 +01:00
committed by GitHub
parent 4ca4f17897
commit cafffd973f
21 changed files with 631 additions and 130 deletions

View File

@ -6,6 +6,7 @@ import {
Entity,
JoinColumn,
ManyToOne,
OneToOne,
PrimaryGeneratedColumn,
Unique,
UpdateDateColumn,
@ -15,11 +16,13 @@ import {
BeforeCreateOne,
IDField,
QueryOptions,
Relation,
} from '@ptc-org/nestjs-query-graphql';
import { FieldMetadataInterface } from 'src/tenant/schema-builder/interfaces/field-metadata.interface';
import { ObjectMetadata } from 'src/metadata/object-metadata/object-metadata.entity';
import { RelationMetadata } from 'src/metadata/relation-metadata/relation-metadata.entity';
import { BeforeCreateOneField } from './hooks/before-create-one-field.hook';
import { FieldMetadataTargetColumnMap } from './interfaces/field-metadata-target-column-map.interface';
@ -35,6 +38,7 @@ export enum FieldMetadataType {
ENUM = 'ENUM',
URL = 'URL',
MONEY = 'MONEY',
RELATION = 'RELATION',
}
registerEnumType(FieldMetadataType, {
@ -61,6 +65,8 @@ registerEnumType(FieldMetadataType, {
'objectId',
'workspaceId',
])
@Relation('toRelationMetadata', () => RelationMetadata, { nullable: true })
@Relation('fromRelationMetadata', () => RelationMetadata, { nullable: true })
export class FieldMetadata implements FieldMetadataInterface {
@IDField(() => ID)
@PrimaryGeneratedColumn('uuid')
@ -119,6 +125,12 @@ export class FieldMetadata implements FieldMetadataInterface {
@JoinColumn({ name: 'object_id' })
object: ObjectMetadata;
@OneToOne(() => RelationMetadata, (relation) => relation.fromFieldMetadata)
fromRelationMetadata: RelationMetadata;
@OneToOne(() => RelationMetadata, (relation) => relation.toFieldMetadata)
toRelationMetadata: RelationMetadata;
@Field()
@CreateDateColumn({ name: 'created_at' })
createdAt: Date;

View File

@ -53,6 +53,8 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadata> {
throw new BadRequestException("Active fields can't be deleted");
}
// TODO: delete associated relation-metadata and field-metadata from the relation
return super.deleteOne(id, opts);
}

View File

@ -7,7 +7,10 @@ import {
FieldMetadata,
FieldMetadataType,
} from 'src/metadata/field-metadata/field-metadata.entity';
import { TenantMigrationColumnAction } from 'src/metadata/tenant-migration/tenant-migration.entity';
import {
TenantMigrationColumnAction,
TenantMigrationColumnActionType,
} from 'src/metadata/tenant-migration/tenant-migration.entity';
/**
* Generate a column name from a field name removing unsupported characters.
@ -61,68 +64,68 @@ export function convertFieldMetadataToColumnActions(
case FieldMetadataType.TEXT:
return [
{
name: fieldMetadata.targetColumnMap.value,
action: 'create',
type: 'text',
action: TenantMigrationColumnActionType.CREATE,
columnName: fieldMetadata.targetColumnMap.value,
columnType: 'text',
},
];
case FieldMetadataType.PHONE:
case FieldMetadataType.EMAIL:
return [
{
name: fieldMetadata.targetColumnMap.value,
action: 'create',
type: 'varchar',
action: TenantMigrationColumnActionType.CREATE,
columnName: fieldMetadata.targetColumnMap.value,
columnType: 'varchar',
},
];
case FieldMetadataType.NUMBER:
return [
{
name: fieldMetadata.targetColumnMap.value,
action: 'create',
type: 'integer',
action: TenantMigrationColumnActionType.CREATE,
columnName: fieldMetadata.targetColumnMap.value,
columnType: 'integer',
},
];
case FieldMetadataType.BOOLEAN:
return [
{
name: fieldMetadata.targetColumnMap.value,
action: 'create',
type: 'boolean',
action: TenantMigrationColumnActionType.CREATE,
columnName: fieldMetadata.targetColumnMap.value,
columnType: 'boolean',
},
];
case FieldMetadataType.DATE:
return [
{
name: fieldMetadata.targetColumnMap.value,
action: 'create',
type: 'timestamp',
action: TenantMigrationColumnActionType.CREATE,
columnName: fieldMetadata.targetColumnMap.value,
columnType: 'timestamp',
},
];
case FieldMetadataType.URL:
return [
{
name: fieldMetadata.targetColumnMap.text,
action: 'create',
type: 'varchar',
action: TenantMigrationColumnActionType.CREATE,
columnName: fieldMetadata.targetColumnMap.text,
columnType: 'varchar',
},
{
name: fieldMetadata.targetColumnMap.link,
action: 'create',
type: 'varchar',
action: TenantMigrationColumnActionType.CREATE,
columnName: fieldMetadata.targetColumnMap.link,
columnType: 'varchar',
},
];
case FieldMetadataType.MONEY:
return [
{
name: fieldMetadata.targetColumnMap.amount,
action: 'create',
type: 'integer',
action: TenantMigrationColumnActionType.CREATE,
columnName: fieldMetadata.targetColumnMap.amount,
columnType: 'integer',
},
{
name: fieldMetadata.targetColumnMap.currency,
action: 'create',
type: 'varchar',
action: TenantMigrationColumnActionType.CREATE,
columnName: fieldMetadata.targetColumnMap.currency,
columnType: 'varchar',
},
];
default:

View File

@ -4,17 +4,6 @@ import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import { DataSource, DataSourceOptions } from 'typeorm';
import { config } from 'dotenv';
import { InitMetadataTables1695214465080 } from './migrations/1695214465080-InitMetadataTables';
import { AlterFieldMetadataTable1695717691800 } from './migrations/1695717691800-alter-field-metadata-table';
import { AddTargetColumnMap1696409050890 } from './migrations/1696409050890-add-target-column-map';
import { MetadataNameLabelRefactoring1697126636202 } from './migrations/1697126636202-MetadataNameLabelRefactoring';
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';
import { RemoveMetadataSoftDelete1698328717102 } from './migrations/1698328717102-removeMetadataSoftDelete';
config();
const configService = new ConfigService();
@ -28,18 +17,7 @@ export const typeORMMetadataModuleOptions: TypeOrmModuleOptions = {
synchronize: false,
migrationsRun: true,
migrationsTableName: '_typeorm_migrations',
migrations: [
InitMetadataTables1695214465080,
AlterFieldMetadataTable1695717691800,
AddTargetColumnMap1696409050890,
MetadataNameLabelRefactoring1697126636202,
RemoveFieldMetadataPlaceholder1697471445015,
AddSoftDelete1697474804403,
RemoveSingularPluralFromFieldLabelAndName1697534910933,
AddNameAndIsCustomToTenantMigration1697622715467,
AddUniqueConstraintsOnFieldObjectMetadata1697630766924,
RemoveMetadataSoftDelete1698328717102,
],
migrations: [__dirname + '/migrations/*{.ts,.js}'],
};
export const connectionSource = new DataSource(

View File

@ -14,6 +14,7 @@ import { DataSourceModule } from './data-source/data-source.module';
import { DataSourceMetadataModule } from './data-source-metadata/data-source-metadata.module';
import { FieldMetadataModule } from './field-metadata/field-metadata.module';
import { ObjectMetadataModule } from './object-metadata/object-metadata.module';
import { RelationMetadataModule } from './relation-metadata/relation-metadata.module';
const typeORMFactory = async (): Promise<TypeOrmModuleOptions> => ({
...typeORMMetadataModuleOptions,
@ -41,6 +42,7 @@ const typeORMFactory = async (): Promise<TypeOrmModuleOptions> => ({
ObjectMetadataModule,
MigrationRunnerModule,
TenantMigrationModule,
RelationMetadataModule,
],
})
export class MetadataModule {}

View File

@ -1,11 +1,14 @@
import { Injectable } from '@nestjs/common';
import { QueryRunner, Table, TableColumn } from 'typeorm';
import { QueryRunner, Table, TableColumn, TableForeignKey } from 'typeorm';
import { DataSourceService } from 'src/metadata/data-source/data-source.service';
import {
TenantMigrationTableAction,
TenantMigrationColumnAction,
TenantMigrationColumnCreate,
TenantMigrationColumnRelation,
TenantMigrationColumnActionType,
} from 'src/metadata/tenant-migration/tenant-migration.entity';
import { TenantMigrationService } from 'src/metadata/tenant-migration/tenant-migration.service';
@ -143,7 +146,7 @@ export class MigrationRunnerService {
for (const columnMigration of columnMigrations) {
switch (columnMigration.action) {
case 'create':
case TenantMigrationColumnActionType.CREATE:
await this.createColumn(
queryRunner,
schemaName,
@ -151,10 +154,16 @@ export class MigrationRunnerService {
columnMigration,
);
break;
default:
throw new Error(
`Migration column action ${columnMigration.action} not supported`,
case TenantMigrationColumnActionType.RELATION:
await this.createForeignKey(
queryRunner,
schemaName,
tableName,
columnMigration,
);
break;
default:
throw new Error(`Migration column action not supported`);
}
}
}
@ -171,22 +180,40 @@ export class MigrationRunnerService {
queryRunner: QueryRunner,
schemaName: string,
tableName: string,
migrationColumn: TenantMigrationColumnAction,
migrationColumn: TenantMigrationColumnCreate,
) {
const hasColumn = await queryRunner.hasColumn(
`${schemaName}.${tableName}`,
migrationColumn.name,
migrationColumn.columnName,
);
if (hasColumn) {
return;
}
await queryRunner.addColumn(
`${schemaName}.${tableName}`,
new TableColumn({
name: migrationColumn.name,
type: migrationColumn.type,
name: migrationColumn.columnName,
type: migrationColumn.columnType,
isNullable: true,
}),
);
}
private async createForeignKey(
queryRunner: QueryRunner,
schemaName: string,
tableName: string,
migrationColumn: TenantMigrationColumnRelation,
) {
await queryRunner.createForeignKey(
`${schemaName}.${tableName}`,
new TableForeignKey({
columnNames: [migrationColumn.columnName],
referencedColumnNames: [migrationColumn.referencedTableColumnName],
referencedTableName: migrationColumn.referencedTableName,
onDelete: 'CASCADE',
}),
);
}
}

View File

@ -0,0 +1,39 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddRelationMetadata1699289664146 implements MigrationInterface {
name = 'AddRelationMetadata1699289664146';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "metadata"."relationMetadata" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "relationType" character varying NOT NULL, "fromObjectMetadataId" uuid NOT NULL, "toObjectMetadataId" uuid NOT NULL, "fromFieldMetadataId" uuid NOT NULL, "toFieldMetadataId" uuid NOT NULL, "workspaceId" character varying NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "REL_3deb257254145a3bdde9575e7d" UNIQUE ("fromFieldMetadataId"), CONSTRAINT "REL_9dea8f90d04edbbf9c541a95c3" UNIQUE ("toFieldMetadataId"), CONSTRAINT "PK_2724f60cb4f17a89481a7e8d7d3" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`ALTER TABLE "metadata"."relationMetadata" ADD CONSTRAINT "FK_f2a0acd3a548ee446a1a35df44d" FOREIGN KEY ("fromObjectMetadataId") REFERENCES "metadata"."object_metadata"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "metadata"."relationMetadata" ADD CONSTRAINT "FK_0f781f589e5a527b8f3d3a4b824" FOREIGN KEY ("toObjectMetadataId") REFERENCES "metadata"."object_metadata"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "metadata"."relationMetadata" ADD CONSTRAINT "FK_3deb257254145a3bdde9575e7d6" FOREIGN KEY ("fromFieldMetadataId") REFERENCES "metadata"."field_metadata"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "metadata"."relationMetadata" ADD CONSTRAINT "FK_9dea8f90d04edbbf9c541a95c3b" FOREIGN KEY ("toFieldMetadataId") REFERENCES "metadata"."field_metadata"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "metadata"."relationMetadata" DROP CONSTRAINT "FK_9dea8f90d04edbbf9c541a95c3b"`,
);
await queryRunner.query(
`ALTER TABLE "metadata"."relationMetadata" DROP CONSTRAINT "FK_3deb257254145a3bdde9575e7d6"`,
);
await queryRunner.query(
`ALTER TABLE "metadata"."relationMetadata" DROP CONSTRAINT "FK_0f781f589e5a527b8f3d3a4b824"`,
);
await queryRunner.query(
`ALTER TABLE "metadata"."relationMetadata" DROP CONSTRAINT "FK_f2a0acd3a548ee446a1a35df44d"`,
);
await queryRunner.query(`DROP TABLE "metadata"."relationMetadata"`);
}
}

View File

@ -20,6 +20,7 @@ import {
import { ObjectMetadataInterface } from 'src/tenant/schema-builder/interfaces/object-metadata.interface';
import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity';
import { RelationMetadata } from 'src/metadata/relation-metadata/relation-metadata.entity';
import { BeforeCreateOneObject } from './hooks/before-create-one-object.hook';
@ -95,6 +96,12 @@ export class ObjectMetadata implements ObjectMetadataInterface {
})
fields: FieldMetadata[];
@OneToMany(() => RelationMetadata, (relation) => relation.fromObjectMetadata)
fromRelations: RelationMetadata[];
@OneToMany(() => RelationMetadata, (relation) => relation.toObjectMetadata)
toRelations: RelationMetadata[];
@Field()
@CreateDateColumn({ name: 'created_at' })
createdAt: Date;

View File

@ -5,7 +5,7 @@ import {
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Equal, In, Repository } from 'typeorm';
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
import { DeleteOneOptions } from '@ptc-org/nestjs-query-core';
@ -93,6 +93,16 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadata> {
});
}
public async findManyWithinWorkspace(
objectMetadataIds: string[],
workspaceId: string,
) {
return this.objectMetadataRepository.findBy({
id: In(objectMetadataIds),
workspaceId: Equal(workspaceId),
});
}
/**
*
* Create all standard objects and fields metadata for a given workspace

View File

@ -0,0 +1,54 @@
import { Field, InputType } from '@nestjs/graphql';
import { BeforeCreateOne } from '@ptc-org/nestjs-query-graphql';
import {
IsEnum,
IsNotEmpty,
IsOptional,
IsString,
IsUUID,
} from 'class-validator';
import { RelationType } from 'src/metadata/relation-metadata/relation-metadata.entity';
import { BeforeCreateOneRelation } from 'src/metadata/relation-metadata/hooks/before-create-one-relation.hook';
@InputType()
@BeforeCreateOne(BeforeCreateOneRelation)
export class CreateRelationInput {
@IsEnum(RelationType)
@IsNotEmpty()
@Field()
relationType: RelationType;
@IsUUID()
@IsNotEmpty()
@Field()
fromObjectMetadataId: string;
@IsUUID()
@IsNotEmpty()
@Field()
toObjectMetadataId: string;
@IsString()
@IsNotEmpty()
@Field()
name: string;
@IsString()
@IsNotEmpty()
@Field()
label: string;
@IsString()
@IsOptional()
@Field({ nullable: true })
description?: string;
@IsString()
@IsOptional()
@Field({ nullable: true })
icon?: string;
workspaceId: string;
}

View File

@ -0,0 +1,27 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import {
BeforeCreateOneHook,
CreateOneInputType,
} from '@ptc-org/nestjs-query-graphql';
import { CreateRelationInput } from 'src/metadata/relation-metadata/dtos/create-relation.input';
@Injectable()
export class BeforeCreateOneRelation<T extends CreateRelationInput>
implements BeforeCreateOneHook<T, any>
{
async run(
instance: CreateOneInputType<T>,
context: any,
): Promise<CreateOneInputType<T>> {
const workspaceId = context?.req?.user?.workspace?.id;
if (!workspaceId) {
throw new UnauthorizedException();
}
instance.input.workspaceId = workspaceId;
return instance;
}
}

View File

@ -0,0 +1,35 @@
import {
AutoResolverOpts,
PagingStrategies,
ReadResolverOpts,
} from '@ptc-org/nestjs-query-graphql';
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
import { RelationMetadata } from './relation-metadata.entity';
import { RelationMetadataService } from './services/relation-metadata.service';
import { CreateRelationInput } from './dtos/create-relation.input';
export const relationMetadataAutoResolverOpts: AutoResolverOpts<
any,
any,
unknown,
unknown,
ReadResolverOpts<any>,
PagingStrategies
>[] = [
{
EntityClass: RelationMetadata,
DTOClass: RelationMetadata,
ServiceClass: RelationMetadataService,
CreateDTOClass: CreateRelationInput,
enableTotalCount: true,
pagingStrategy: PagingStrategies.CURSOR,
read: { many: { disabled: true } },
create: { many: { disabled: true } },
update: { disabled: true },
delete: { disabled: true },
guards: [JwtAuthGuard],
},
];

View File

@ -0,0 +1,93 @@
import { ObjectType, ID, Field } from '@nestjs/graphql';
import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
ManyToOne,
OneToOne,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import {
Authorize,
IDField,
QueryOptions,
Relation,
} from '@ptc-org/nestjs-query-graphql';
import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity';
import { ObjectMetadata } from 'src/metadata/object-metadata/object-metadata.entity';
export enum RelationType {
ONE_TO_ONE = 'ONE_TO_ONE',
ONE_TO_MANY = 'ONE_TO_MANY',
MANY_TO_MANY = 'MANY_TO_MANY',
}
@Entity('relationMetadata')
@ObjectType('relation')
@Authorize({
authorize: (context: any) => ({
workspaceId: { eq: context?.req?.user?.workspace?.id },
}),
})
@QueryOptions({
defaultResultSize: 10,
disableFilter: true,
disableSort: true,
maxResultsSize: 1000,
})
@Relation('fromObjectMetadata', () => ObjectMetadata)
@Relation('toObjectMetadata', () => ObjectMetadata)
export class RelationMetadata {
@IDField(() => ID)
@PrimaryGeneratedColumn('uuid')
id: string;
@Field()
@Column({ nullable: false })
relationType: RelationType;
@Field()
@Column({ nullable: false, type: 'uuid' })
fromObjectMetadataId: string;
@Field()
@Column({ nullable: false, type: 'uuid' })
toObjectMetadataId: string;
@Field()
@Column({ nullable: false, type: 'uuid' })
fromFieldMetadataId: string;
@Field()
@Column({ nullable: false, type: 'uuid' })
toFieldMetadataId: string;
@Column({ nullable: false })
workspaceId: string;
@ManyToOne(() => ObjectMetadata, (object) => object.fromRelations)
fromObjectMetadata: ObjectMetadata;
@ManyToOne(() => ObjectMetadata, (object) => object.toRelations)
toObjectMetadata: ObjectMetadata;
@OneToOne(() => FieldMetadata, (field) => field.fromRelationMetadata)
@JoinColumn()
fromFieldMetadata: FieldMetadata;
@OneToOne(() => FieldMetadata, (field) => field.toRelationMetadata)
@JoinColumn()
toFieldMetadata: FieldMetadata;
@Field()
@CreateDateColumn()
createdAt: Date;
@Field()
@UpdateDateColumn()
updatedAt: Date;
}

View File

@ -0,0 +1,33 @@
import { Module } from '@nestjs/common';
import { NestjsQueryGraphQLModule } from '@ptc-org/nestjs-query-graphql';
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { FieldMetadataModule } from 'src/metadata/field-metadata/field-metadata.module';
import { ObjectMetadataModule } from 'src/metadata/object-metadata/object-metadata.module';
import { MigrationRunnerModule } from 'src/metadata/migration-runner/migration-runner.module';
import { TenantMigrationModule } from 'src/metadata/tenant-migration/tenant-migration.module';
import { RelationMetadata } from './relation-metadata.entity';
import { relationMetadataAutoResolverOpts } from './relation-metadata.auto-resolver-opts';
import { RelationMetadataService } from './services/relation-metadata.service';
@Module({
imports: [
NestjsQueryGraphQLModule.forFeature({
imports: [
NestjsQueryTypeOrmModule.forFeature([RelationMetadata], 'metadata'),
ObjectMetadataModule,
FieldMetadataModule,
MigrationRunnerModule,
TenantMigrationModule,
],
services: [RelationMetadataService],
resolvers: relationMetadataAutoResolverOpts,
}),
],
providers: [RelationMetadataService],
exports: [RelationMetadataService],
})
export class RelationMetadataModule {}

View File

@ -0,0 +1,148 @@
import {
BadRequestException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
import { Repository } from 'typeorm';
import {
RelationMetadata,
RelationType,
} from 'src/metadata/relation-metadata/relation-metadata.entity';
import { ObjectMetadataService } from 'src/metadata/object-metadata/services/object-metadata.service';
import { FieldMetadataService } from 'src/metadata/field-metadata/services/field-metadata.service';
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
import { CreateRelationInput } from 'src/metadata/relation-metadata/dtos/create-relation.input';
import { MigrationRunnerService } from 'src/metadata/migration-runner/migration-runner.service';
import { TenantMigrationService } from 'src/metadata/tenant-migration/tenant-migration.service';
import { TenantMigrationColumnActionType } from 'src/metadata/tenant-migration/tenant-migration.entity';
@Injectable()
export class RelationMetadataService extends TypeOrmQueryService<RelationMetadata> {
constructor(
@InjectRepository(RelationMetadata, 'metadata')
private readonly relationMetadataRepository: Repository<RelationMetadata>,
private readonly objectMetadataService: ObjectMetadataService,
private readonly fieldMetadataService: FieldMetadataService,
private readonly tenantMigrationService: TenantMigrationService,
private readonly migrationRunnerService: MigrationRunnerService,
) {
super(relationMetadataRepository);
}
override async createOne(
record: CreateRelationInput,
): Promise<RelationMetadata> {
if (record.relationType === RelationType.MANY_TO_MANY) {
throw new BadRequestException(
'Many to many relations are not supported yet',
);
}
const objectMetadataEntries =
await this.objectMetadataService.findManyWithinWorkspace(
[record.fromObjectMetadataId, record.toObjectMetadataId],
record.workspaceId,
);
const objectMetadataMap = objectMetadataEntries.reduce((acc, curr) => {
acc[curr.id] = curr;
return acc;
}, {});
if (
objectMetadataMap[record.fromObjectMetadataId] === undefined ||
objectMetadataMap[record.toObjectMetadataId] === undefined
) {
throw new NotFoundException(
'Can\t find an existing object matching fromObjectMetadataId or toObjectMetadataId',
);
}
const createdFields = await this.fieldMetadataService.createMany([
{
name: record.name,
label: record.label,
description: record.description,
icon: record.icon,
isCustom: true,
targetColumnMap: {},
isActive: true,
type: FieldMetadataType.RELATION,
objectId: record.fromObjectMetadataId,
workspaceId: record.workspaceId,
},
// NOTE: Since we have to create the field-metadata for the user, we need to use the toObjectMetadata info.
// This is not ideal because we might see some conflicts with existing names.
// NOTE2: Once MANY_TO_MANY is supported, we need to use namePlural/labelPlural instead.
{
name: objectMetadataMap[record.fromObjectMetadataId].nameSingular,
label: objectMetadataMap[record.fromObjectMetadataId].labelSingular,
description: undefined,
icon: objectMetadataMap[record.fromObjectMetadataId].icon,
isCustom: true,
targetColumnMap: {},
isActive: true,
type: FieldMetadataType.RELATION,
objectId: record.toObjectMetadataId,
workspaceId: record.workspaceId,
},
]);
const createdFieldMap = createdFields.reduce((acc, curr) => {
acc[curr.objectId] = curr;
return acc;
}, {});
const createdRelationMetadata = await super.createOne({
...record,
fromFieldMetadataId: createdFieldMap[record.fromObjectMetadataId].id,
toFieldMetadataId: createdFieldMap[record.toObjectMetadataId].id,
});
const foreignKeyColumnName = `${
objectMetadataMap[record.fromObjectMetadataId].targetTableName
}Id`;
await this.tenantMigrationService.createCustomMigration(
record.workspaceId,
[
// Create the column
{
name: objectMetadataMap[record.toObjectMetadataId].targetTableName,
action: 'alter',
columns: [
{
action: TenantMigrationColumnActionType.CREATE,
columnName: foreignKeyColumnName,
columnType: 'uuid',
},
],
},
// Create the foreignKey
{
name: objectMetadataMap[record.toObjectMetadataId].targetTableName,
action: 'alter',
columns: [
{
action: TenantMigrationColumnActionType.RELATION,
columnName: foreignKeyColumnName,
referencedTableName:
objectMetadataMap[record.fromObjectMetadataId].targetTableName,
referencedTableColumnName: 'id',
},
],
},
],
);
await this.migrationRunnerService.executeMigrationFromPendingMigrations(
record.workspaceId,
);
return createdRelationMetadata;
}
}

View File

@ -1,4 +1,7 @@
import { TenantMigrationTableAction } from 'src/metadata/tenant-migration/tenant-migration.entity';
import {
TenantMigrationColumnActionType,
TenantMigrationTableAction,
} from 'src/metadata/tenant-migration/tenant-migration.entity';
export const addCompanyTable: TenantMigrationTableAction[] = [
{
@ -10,24 +13,24 @@ export const addCompanyTable: TenantMigrationTableAction[] = [
action: 'alter',
columns: [
{
name: 'name',
type: 'varchar',
action: 'create',
columnName: 'name',
columnType: 'varchar',
action: TenantMigrationColumnActionType.CREATE,
},
{
name: 'domainName',
type: 'varchar',
action: 'create',
columnName: 'domainName',
columnType: 'varchar',
action: TenantMigrationColumnActionType.CREATE,
},
{
name: 'address',
type: 'varchar',
action: 'create',
columnName: 'address',
columnType: 'varchar',
action: TenantMigrationColumnActionType.CREATE,
},
{
name: 'employees',
type: 'integer',
action: 'create',
columnName: 'employees',
columnType: 'integer',
action: TenantMigrationColumnActionType.CREATE,
},
],
},

View File

@ -1,4 +1,7 @@
import { TenantMigrationTableAction } from 'src/metadata/tenant-migration/tenant-migration.entity';
import {
TenantMigrationColumnActionType,
TenantMigrationTableAction,
} from 'src/metadata/tenant-migration/tenant-migration.entity';
export const addViewTable: TenantMigrationTableAction[] = [
{
@ -10,19 +13,19 @@ export const addViewTable: TenantMigrationTableAction[] = [
action: 'alter',
columns: [
{
name: 'name',
type: 'varchar',
action: 'create',
columnName: 'name',
columnType: 'varchar',
action: TenantMigrationColumnActionType.CREATE,
},
{
name: 'objectId',
type: 'varchar',
action: 'create',
columnName: 'objectId',
columnType: 'varchar',
action: TenantMigrationColumnActionType.CREATE,
},
{
name: 'type',
type: 'varchar',
action: 'create',
columnName: 'type',
columnType: 'varchar',
action: TenantMigrationColumnActionType.CREATE,
},
],
},

View File

@ -1,4 +1,7 @@
import { TenantMigrationTableAction } from 'src/metadata/tenant-migration/tenant-migration.entity';
import {
TenantMigrationColumnActionType,
TenantMigrationTableAction,
} from 'src/metadata/tenant-migration/tenant-migration.entity';
export const addViewFieldTable: TenantMigrationTableAction[] = [
{
@ -10,29 +13,29 @@ export const addViewFieldTable: TenantMigrationTableAction[] = [
action: 'alter',
columns: [
{
name: 'fieldId',
type: 'varchar',
action: 'create',
columnName: 'fieldId',
columnType: 'varchar',
action: TenantMigrationColumnActionType.CREATE,
},
{
name: 'viewId',
type: 'varchar',
action: 'create',
columnName: 'viewId',
columnType: 'varchar',
action: TenantMigrationColumnActionType.CREATE,
},
{
name: 'position',
type: 'integer',
action: 'create',
columnName: 'position',
columnType: 'integer',
action: TenantMigrationColumnActionType.CREATE,
},
{
name: 'isVisible',
type: 'boolean',
action: 'create',
columnName: 'isVisible',
columnType: 'boolean',
action: TenantMigrationColumnActionType.CREATE,
},
{
name: 'size',
type: 'integer',
action: 'create',
columnName: 'size',
columnType: 'integer',
action: TenantMigrationColumnActionType.CREATE,
},
],
},

View File

@ -1,4 +1,7 @@
import { TenantMigrationTableAction } from 'src/metadata/tenant-migration/tenant-migration.entity';
import {
TenantMigrationColumnActionType,
TenantMigrationTableAction,
} from 'src/metadata/tenant-migration/tenant-migration.entity';
export const addViewFilterTable: TenantMigrationTableAction[] = [
{
@ -10,29 +13,29 @@ export const addViewFilterTable: TenantMigrationTableAction[] = [
action: 'alter',
columns: [
{
name: 'fieldId',
type: 'varchar',
action: 'create',
columnName: 'fieldId',
columnType: 'varchar',
action: TenantMigrationColumnActionType.CREATE,
},
{
name: 'viewId',
type: 'varchar',
action: 'create',
columnName: 'viewId',
columnType: 'varchar',
action: TenantMigrationColumnActionType.CREATE,
},
{
name: 'operand',
type: 'varchar',
action: 'create',
columnName: 'operand',
columnType: 'varchar',
action: TenantMigrationColumnActionType.CREATE,
},
{
name: 'value',
type: 'varchar',
action: 'create',
columnName: 'value',
columnType: 'varchar',
action: TenantMigrationColumnActionType.CREATE,
},
{
name: 'displayValue',
type: 'varchar',
action: 'create',
columnName: 'displayValue',
columnType: 'varchar',
action: TenantMigrationColumnActionType.CREATE,
},
],
},

View File

@ -1,4 +1,7 @@
import { TenantMigrationTableAction } from 'src/metadata/tenant-migration/tenant-migration.entity';
import {
TenantMigrationColumnActionType,
TenantMigrationTableAction,
} from 'src/metadata/tenant-migration/tenant-migration.entity';
export const addViewSortTable: TenantMigrationTableAction[] = [
{
@ -10,19 +13,19 @@ export const addViewSortTable: TenantMigrationTableAction[] = [
action: 'alter',
columns: [
{
name: 'fieldId',
type: 'varchar',
action: 'create',
columnName: 'fieldId',
columnType: 'varchar',
action: TenantMigrationColumnActionType.CREATE,
},
{
name: 'viewId',
type: 'varchar',
action: 'create',
columnName: 'viewId',
columnType: 'varchar',
action: TenantMigrationColumnActionType.CREATE,
},
{
name: 'direction',
type: 'varchar',
action: 'create',
columnName: 'direction',
columnType: 'varchar',
action: TenantMigrationColumnActionType.CREATE,
},
],
},

View File

@ -5,12 +5,28 @@ import {
PrimaryGeneratedColumn,
} from 'typeorm';
export type TenantMigrationColumnAction = {
name: string;
type: string;
action: 'create';
export enum TenantMigrationColumnActionType {
CREATE = 'CREATE',
RELATION = 'RELATION',
}
export type TenantMigrationColumnCreate = {
action: TenantMigrationColumnActionType.CREATE;
columnName: string;
columnType: string;
};
export type TenantMigrationColumnRelation = {
action: TenantMigrationColumnActionType.RELATION;
columnName: string;
referencedTableName: string;
referencedTableColumnName: string;
};
export type TenantMigrationColumnAction = {
action: TenantMigrationColumnActionType;
} & (TenantMigrationColumnCreate | TenantMigrationColumnRelation);
export type TenantMigrationTableAction = {
name: string;
action: 'create' | 'alter';