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:
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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 {}
|
||||
|
||||
@ -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',
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"`);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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],
|
||||
},
|
||||
];
|
||||
@ -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;
|
||||
}
|
||||
@ -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 {}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@ -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';
|
||||
|
||||
Reference in New Issue
Block a user