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