Migrate to a monorepo structure (#2909)

This commit is contained in:
Charles Bochet
2023-12-10 18:10:54 +01:00
committed by GitHub
parent a70a9281eb
commit 5bdca9de6c
2304 changed files with 37152 additions and 25869 deletions

View File

@ -0,0 +1,80 @@
import { Field, HideField, InputType } from '@nestjs/graphql';
import { BeforeCreateOne } from '@ptc-org/nestjs-query-graphql';
import {
IsEnum,
IsNotEmpty,
IsOptional,
IsString,
IsUUID,
} from 'class-validator';
import { BeforeCreateOneRelation } from 'src/metadata/relation-metadata/hooks/before-create-one-relation.hook';
import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity';
@InputType()
@BeforeCreateOne(BeforeCreateOneRelation)
export class CreateRelationInput {
@IsEnum(RelationMetadataType)
@IsNotEmpty()
@Field(() => RelationMetadataType)
relationType: RelationMetadataType;
@IsUUID()
@IsNotEmpty()
@Field()
fromObjectMetadataId: string;
@IsUUID()
@IsNotEmpty()
@Field()
toObjectMetadataId: string;
@IsString()
@IsNotEmpty()
@Field()
fromName: string;
@IsString()
@IsNotEmpty()
@Field()
toName: string;
@IsString()
@IsNotEmpty()
@Field()
fromLabel: string;
@IsString()
@IsNotEmpty()
@Field()
toLabel: string;
@IsString()
@IsOptional()
@Field({ nullable: true })
fromIcon?: string;
@IsString()
@IsOptional()
@Field({ nullable: true })
toIcon?: string;
@IsString()
@IsOptional()
@Field({ nullable: true, deprecationReason: 'Use fromDescription instead' })
description?: string;
@IsString()
@IsOptional()
@Field({ nullable: true })
fromDescription?: string;
@IsString()
@IsOptional()
@Field({ nullable: true })
toDescription?: string;
@HideField()
workspaceId: string;
}

View File

@ -0,0 +1,71 @@
import {
ObjectType,
ID,
Field,
HideField,
registerEnumType,
} from '@nestjs/graphql';
import { CreateDateColumn, UpdateDateColumn } from 'typeorm';
import {
Authorize,
BeforeDeleteOne,
IDField,
QueryOptions,
Relation,
} from '@ptc-org/nestjs-query-graphql';
import { ObjectMetadataDTO } from 'src/metadata/object-metadata/dtos/object-metadata.dto';
import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity';
import { BeforeDeleteOneRelation } from 'src/metadata/relation-metadata/hooks/before-delete-one-field.hook';
registerEnumType(RelationMetadataType, {
name: 'RelationMetadataType',
description: 'Type of the relation',
});
@ObjectType('relation')
@Authorize({
authorize: (context: any) => ({
workspaceId: { eq: context?.req?.user?.workspace?.id },
}),
})
@QueryOptions({
defaultResultSize: 10,
disableFilter: true,
disableSort: true,
maxResultsSize: 1000,
})
@BeforeDeleteOne(BeforeDeleteOneRelation)
@Relation('fromObjectMetadata', () => ObjectMetadataDTO)
@Relation('toObjectMetadata', () => ObjectMetadataDTO)
export class RelationMetadataDTO {
@IDField(() => ID)
id: string;
@Field(() => RelationMetadataType)
relationType: RelationMetadataType;
@Field()
fromObjectMetadataId: string;
@Field()
toObjectMetadataId: string;
@Field()
fromFieldMetadataId: string;
@Field()
toFieldMetadataId: string;
@HideField()
workspaceId: string;
@Field()
@CreateDateColumn()
createdAt: Date;
@Field()
@UpdateDateColumn()
updatedAt: Date;
}

View File

@ -0,0 +1,28 @@
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,55 @@
import {
BadRequestException,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import {
BeforeDeleteOneHook,
DeleteOneInputType,
} from '@ptc-org/nestjs-query-graphql';
import { RelationMetadataService } from 'src/metadata/relation-metadata/relation-metadata.service';
@Injectable()
export class BeforeDeleteOneRelation implements BeforeDeleteOneHook<any> {
constructor(readonly relationMetadataService: RelationMetadataService) {}
async run(
instance: DeleteOneInputType,
context: any,
): Promise<DeleteOneInputType> {
const workspaceId = context?.req?.user?.workspace?.id;
if (!workspaceId) {
throw new UnauthorizedException();
}
const relationMetadata =
await this.relationMetadataService.findOneWithinWorkspace(workspaceId, {
where: {
id: instance.id.toString(),
},
});
if (!relationMetadata) {
throw new BadRequestException('Relation does not exist');
}
if (
!relationMetadata.toFieldMetadata.isCustom ||
!relationMetadata.fromFieldMetadata.isCustom
) {
throw new BadRequestException("Standard Relations can't be deleted");
}
if (
relationMetadata.toFieldMetadata.isActive ||
relationMetadata.fromFieldMetadata.isActive
) {
throw new BadRequestException("Active relations can't be deleted");
}
return instance;
}
}

View File

@ -0,0 +1,83 @@
import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
ManyToOne,
OneToOne,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { RelationMetadataInterface } from 'src/metadata/field-metadata/interfaces/relation-metadata.interface';
import { FieldMetadataEntity } from 'src/metadata/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';
export enum RelationMetadataType {
ONE_TO_ONE = 'ONE_TO_ONE',
ONE_TO_MANY = 'ONE_TO_MANY',
MANY_TO_MANY = 'MANY_TO_MANY',
}
@Entity('relationMetadata')
export class RelationMetadataEntity implements RelationMetadataInterface {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ nullable: false })
relationType: RelationMetadataType;
@Column({ nullable: false, type: 'uuid' })
fromObjectMetadataId: string;
@Column({ nullable: false, type: 'uuid' })
toObjectMetadataId: string;
@Column({ nullable: false, type: 'uuid' })
fromFieldMetadataId: string;
@Column({ nullable: false, type: 'uuid' })
toFieldMetadataId: string;
@Column({ nullable: false })
workspaceId: string;
@ManyToOne(
() => ObjectMetadataEntity,
(object: ObjectMetadataEntity) => object.fromRelations,
{
onDelete: 'CASCADE',
},
)
fromObjectMetadata: ObjectMetadataEntity;
@ManyToOne(
() => ObjectMetadataEntity,
(object: ObjectMetadataEntity) => object.toRelations,
{
onDelete: 'CASCADE',
},
)
toObjectMetadata: ObjectMetadataEntity;
@OneToOne(
() => FieldMetadataEntity,
(field: FieldMetadataEntity) => field.fromRelationMetadata,
)
@JoinColumn()
fromFieldMetadata: FieldMetadataEntity;
@OneToOne(
() => FieldMetadataEntity,
(field: FieldMetadataEntity) => field.toRelationMetadata,
)
@JoinColumn()
toFieldMetadata: FieldMetadataEntity;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}

View File

@ -0,0 +1,54 @@
import { Module } from '@nestjs/common';
import {
NestjsQueryGraphQLModule,
PagingStrategies,
} from '@ptc-org/nestjs-query-graphql';
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
import { FieldMetadataModule } from 'src/metadata/field-metadata/field-metadata.module';
import { ObjectMetadataModule } from 'src/metadata/object-metadata/object-metadata.module';
import { WorkspaceMigrationModule } from 'src/metadata/workspace-migration/workspace-migration.module';
import { WorkspaceMigrationRunnerModule } from 'src/workspace/workspace-migration-runner/workspace-migration-runner.module';
import { RelationMetadataService } from './relation-metadata.service';
import { RelationMetadataEntity } from './relation-metadata.entity';
import { CreateRelationInput } from './dtos/create-relation.input';
import { RelationMetadataDTO } from './dtos/relation-metadata.dto';
@Module({
imports: [
NestjsQueryGraphQLModule.forFeature({
imports: [
NestjsQueryTypeOrmModule.forFeature(
[RelationMetadataEntity],
'metadata',
),
ObjectMetadataModule,
FieldMetadataModule,
WorkspaceMigrationRunnerModule,
WorkspaceMigrationModule,
],
services: [RelationMetadataService],
resolvers: [
{
EntityClass: RelationMetadataEntity,
DTOClass: RelationMetadataDTO,
ServiceClass: RelationMetadataService,
CreateDTOClass: CreateRelationInput,
enableTotalCount: true,
pagingStrategy: PagingStrategies.CURSOR,
create: { many: { disabled: true } },
update: { disabled: true },
delete: { many: { disabled: true } },
guards: [JwtAuthGuard],
},
],
}),
],
providers: [RelationMetadataService],
exports: [RelationMetadataService],
})
export class RelationMetadataModule {}

View File

@ -0,0 +1,238 @@
import {
BadRequestException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
import { FindOneOptions, In, Repository } from 'typeorm';
import camelCase from 'lodash.camelcase';
import { ObjectMetadataService } from 'src/metadata/object-metadata/object-metadata.service';
import { FieldMetadataService } from 'src/metadata/field-metadata/field-metadata.service';
import { CreateRelationInput } from 'src/metadata/relation-metadata/dtos/create-relation.input';
import { WorkspaceMigrationRunnerService } from 'src/workspace/workspace-migration-runner/workspace-migration-runner.service';
import { WorkspaceMigrationService } from 'src/metadata/workspace-migration/workspace-migration.service';
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
import { WorkspaceMigrationColumnActionType } from 'src/metadata/workspace-migration/workspace-migration.entity';
import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';
import { createCustomColumnName } from 'src/metadata/utils/create-custom-column-name.util';
import {
RelationMetadataEntity,
RelationMetadataType,
} from './relation-metadata.entity';
@Injectable()
export class RelationMetadataService extends TypeOrmQueryService<RelationMetadataEntity> {
constructor(
@InjectRepository(RelationMetadataEntity, 'metadata')
private readonly relationMetadataRepository: Repository<RelationMetadataEntity>,
private readonly objectMetadataService: ObjectMetadataService,
private readonly fieldMetadataService: FieldMetadataService,
private readonly workspaceMigrationService: WorkspaceMigrationService,
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
) {
super(relationMetadataRepository);
}
override async deleteOne(id: string): Promise<RelationMetadataEntity> {
// TODO: This logic is duplicated with the BeforeDeleteOneRelation hook
const relationMetadata = await this.relationMetadataRepository.findOne({
where: { id },
relations: ['fromFieldMetadata', 'toFieldMetadata'],
});
if (!relationMetadata) {
throw new NotFoundException('Relation does not exist');
}
const deletedRelationMetadata = super.deleteOne(id);
// TODO: Move to a cdc scheduler
this.fieldMetadataService.deleteMany({
id: {
in: [
relationMetadata.fromFieldMetadataId,
relationMetadata.toFieldMetadataId,
],
},
});
return deletedRelationMetadata;
}
override async createOne(
record: CreateRelationInput,
): Promise<RelationMetadataEntity> {
if (record.relationType === RelationMetadataType.MANY_TO_MANY) {
throw new BadRequestException(
'Many to many relations are not supported yet',
);
}
/**
* Relation types
*
* MANY TO MANY:
* FROM Ǝ-E TO (NOT YET SUPPORTED)
*
* ONE TO MANY:
* FROM --E TO (host the id in the TO table)
*
* ONE TO ONE:
* FROM --- TO (host the id in the TO table)
*/
const objectMetadataEntries =
await this.objectMetadataService.findManyWithinWorkspace(
record.workspaceId,
{
where: {
id: In([record.fromObjectMetadataId, record.toObjectMetadataId]),
},
},
);
const objectMetadataMap = objectMetadataEntries.reduce((acc, curr) => {
acc[curr.id] = curr;
return acc;
}, {} as { [key: string]: ObjectMetadataEntity });
if (
objectMetadataMap[record.fromObjectMetadataId] === undefined ||
objectMetadataMap[record.toObjectMetadataId] === undefined
) {
throw new NotFoundException(
'Can\t find an existing object matching fromObjectMetadataId or toObjectMetadataId',
);
}
const baseColumnName = `${camelCase(record.toName)}Id`;
const isToCustom = objectMetadataMap[record.toObjectMetadataId].isCustom;
const foreignKeyColumnName = isToCustom
? createCustomColumnName(baseColumnName)
: baseColumnName;
const createdFields = await this.fieldMetadataService.createMany([
// FROM
{
name: record.fromName,
label: record.fromLabel,
description: record.fromDescription,
icon: record.fromIcon,
isCustom: true,
targetColumnMap: {},
isActive: true,
type: FieldMetadataType.RELATION,
objectMetadataId: record.fromObjectMetadataId,
workspaceId: record.workspaceId,
},
// TO
{
name: record.toName,
label: record.toLabel,
description: record.toDescription,
icon: record.toIcon,
isCustom: true,
targetColumnMap: {
value: isToCustom
? createCustomColumnName(record.toName)
: record.toName,
},
isActive: true,
type: FieldMetadataType.RELATION,
objectMetadataId: record.toObjectMetadataId,
workspaceId: record.workspaceId,
},
// FOREIGN KEY
{
name: baseColumnName,
label: `${record.toLabel} Foreign Key`,
description: record.toDescription
? `${record.toDescription} Foreign Key`
: undefined,
icon: undefined,
isCustom: true,
targetColumnMap: {
value: foreignKeyColumnName,
},
isActive: true,
// Should not be visible on the front side
isSystem: true,
type: FieldMetadataType.UUID,
objectMetadataId: record.toObjectMetadataId,
workspaceId: record.workspaceId,
},
]);
const createdFieldMap = createdFields.reduce((acc, fieldMetadata) => {
if (fieldMetadata.type === FieldMetadataType.RELATION) {
acc[fieldMetadata.name] = fieldMetadata;
}
return acc;
}, {});
const createdRelationMetadata = await super.createOne({
...record,
fromFieldMetadataId: createdFieldMap[record.fromName].id,
toFieldMetadataId: createdFieldMap[record.toName].id,
});
await this.workspaceMigrationService.createCustomMigration(
record.workspaceId,
[
// Create the column
{
name: objectMetadataMap[record.toObjectMetadataId].targetTableName,
action: 'alter',
columns: [
{
action: WorkspaceMigrationColumnActionType.CREATE,
columnName: foreignKeyColumnName,
columnType: 'uuid',
},
],
},
// Create the foreignKey
{
name: objectMetadataMap[record.toObjectMetadataId].targetTableName,
action: 'alter',
columns: [
{
action: WorkspaceMigrationColumnActionType.RELATION,
columnName: foreignKeyColumnName,
referencedTableName:
objectMetadataMap[record.fromObjectMetadataId].targetTableName,
referencedTableColumnName: 'id',
isUnique: record.relationType === RelationMetadataType.ONE_TO_ONE,
},
],
},
],
);
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
record.workspaceId,
);
return createdRelationMetadata;
}
public async findOneWithinWorkspace(
workspaceId: string,
options: FindOneOptions<RelationMetadataEntity>,
) {
return this.relationMetadataRepository.findOne({
...options,
where: {
...options.where,
workspaceId,
},
relations: ['fromFieldMetadata', 'toFieldMetadata'],
});
}
}