Migrate to a monorepo structure (#2909)
This commit is contained in:
@ -0,0 +1,56 @@
|
||||
import { Field, HideField, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { BeforeCreateOne } from '@ptc-org/nestjs-query-graphql';
|
||||
import { IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator';
|
||||
|
||||
import { BeforeCreateOneObject } from 'src/metadata/object-metadata/hooks/before-create-one-object.hook';
|
||||
|
||||
@InputType()
|
||||
@BeforeCreateOne(BeforeCreateOneObject)
|
||||
export class CreateObjectInput {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
nameSingular: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
namePlural: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
labelSingular: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
labelPlural: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
description?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
icon?: string;
|
||||
|
||||
@HideField()
|
||||
dataSourceId: string;
|
||||
|
||||
@HideField()
|
||||
workspaceId: string;
|
||||
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
labelIdentifierFieldMetadataId?: string;
|
||||
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
imageIdentifierFieldMetadataId?: string;
|
||||
}
|
||||
@ -0,0 +1,76 @@
|
||||
import { ObjectType, ID, Field, HideField } from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
Authorize,
|
||||
BeforeDeleteOne,
|
||||
CursorConnection,
|
||||
FilterableField,
|
||||
IDField,
|
||||
QueryOptions,
|
||||
} from '@ptc-org/nestjs-query-graphql';
|
||||
|
||||
import { FieldMetadataDTO } from 'src/metadata/field-metadata/dtos/field-metadata.dto';
|
||||
import { BeforeDeleteOneObject } from 'src/metadata/object-metadata/hooks/before-delete-one-object.hook';
|
||||
|
||||
@ObjectType('object')
|
||||
@Authorize({
|
||||
authorize: (context: any) => ({
|
||||
workspaceId: { eq: context?.req?.user?.workspace?.id },
|
||||
}),
|
||||
})
|
||||
@QueryOptions({
|
||||
defaultResultSize: 10,
|
||||
disableSort: true,
|
||||
maxResultsSize: 1000,
|
||||
})
|
||||
@BeforeDeleteOne(BeforeDeleteOneObject)
|
||||
@CursorConnection('fields', () => FieldMetadataDTO)
|
||||
export class ObjectMetadataDTO {
|
||||
@IDField(() => ID)
|
||||
id: string;
|
||||
|
||||
@Field()
|
||||
dataSourceId: string;
|
||||
|
||||
@Field()
|
||||
nameSingular: string;
|
||||
|
||||
@Field()
|
||||
namePlural: string;
|
||||
|
||||
@Field()
|
||||
labelSingular: string;
|
||||
|
||||
@Field()
|
||||
labelPlural: string;
|
||||
|
||||
@Field({ nullable: true })
|
||||
description: string;
|
||||
|
||||
@Field({ nullable: true })
|
||||
icon: string;
|
||||
|
||||
@FilterableField()
|
||||
isCustom: boolean;
|
||||
|
||||
@FilterableField()
|
||||
isActive: boolean;
|
||||
|
||||
@FilterableField()
|
||||
isSystem: boolean;
|
||||
|
||||
@HideField()
|
||||
workspaceId: string;
|
||||
|
||||
@Field()
|
||||
createdAt: Date;
|
||||
|
||||
@Field()
|
||||
updatedAt: Date;
|
||||
|
||||
@Field({ nullable: true })
|
||||
labelIdentifierFieldMetadataId: string;
|
||||
|
||||
@Field({ nullable: true })
|
||||
imageIdentifierFieldMetadataId: string;
|
||||
}
|
||||
@ -0,0 +1,55 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { BeforeUpdateOne } from '@ptc-org/nestjs-query-graphql';
|
||||
import { IsBoolean, IsOptional, IsString, IsUUID } from 'class-validator';
|
||||
|
||||
import { BeforeUpdateOneObject } from 'src/metadata/object-metadata/hooks/before-update-one-object.hook';
|
||||
|
||||
@InputType()
|
||||
@BeforeUpdateOne(BeforeUpdateOneObject)
|
||||
export class UpdateObjectInput {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
labelSingular?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
labelPlural?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
nameSingular?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
namePlural?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
description?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
icon?: string;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
isActive?: boolean;
|
||||
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
labelIdentifierFieldMetadataId?: string;
|
||||
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
imageIdentifierFieldMetadataId?: string;
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
import {
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import {
|
||||
BeforeCreateOneHook,
|
||||
CreateOneInputType,
|
||||
} from '@ptc-org/nestjs-query-graphql';
|
||||
|
||||
import { CreateObjectInput } from 'src/metadata/object-metadata/dtos/create-object.input';
|
||||
|
||||
const coreObjectNames = ['featureFlag', 'refreshToken', 'workspace', 'user'];
|
||||
|
||||
@Injectable()
|
||||
export class BeforeCreateOneObject<T extends CreateObjectInput>
|
||||
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();
|
||||
}
|
||||
|
||||
if (
|
||||
coreObjectNames.includes(instance.input.nameSingular) ||
|
||||
coreObjectNames.includes(instance.input.namePlural)
|
||||
) {
|
||||
throw new ForbiddenException(
|
||||
'You cannot create an object with this name.',
|
||||
);
|
||||
}
|
||||
instance.input.workspaceId = workspaceId;
|
||||
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import {
|
||||
BeforeDeleteOneHook,
|
||||
DeleteOneInputType,
|
||||
} from '@ptc-org/nestjs-query-graphql';
|
||||
|
||||
import { ObjectMetadataService } from 'src/metadata/object-metadata/object-metadata.service';
|
||||
|
||||
@Injectable()
|
||||
export class BeforeDeleteOneObject implements BeforeDeleteOneHook<any> {
|
||||
constructor(readonly objectMetadataService: ObjectMetadataService) {}
|
||||
|
||||
async run(
|
||||
instance: DeleteOneInputType,
|
||||
context: any,
|
||||
): Promise<DeleteOneInputType> {
|
||||
const workspaceId = context?.req?.user?.workspace?.id;
|
||||
|
||||
if (!workspaceId) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
const objectMetadata =
|
||||
await this.objectMetadataService.findOneWithinWorkspace(workspaceId, {
|
||||
where: {
|
||||
id: instance.id.toString(),
|
||||
},
|
||||
});
|
||||
|
||||
if (!objectMetadata) {
|
||||
throw new BadRequestException('Object does not exist');
|
||||
}
|
||||
|
||||
if (!objectMetadata.isCustom) {
|
||||
throw new BadRequestException("Standard Objects can't be deleted");
|
||||
}
|
||||
|
||||
if (objectMetadata.isActive) {
|
||||
throw new BadRequestException("Active objects can't be deleted");
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,146 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import {
|
||||
BeforeUpdateOneHook,
|
||||
UpdateOneInputType,
|
||||
} from '@ptc-org/nestjs-query-graphql';
|
||||
import { Equal, In, Repository } from 'typeorm';
|
||||
|
||||
import { FieldMetadataEntity } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import { UpdateObjectInput } from 'src/metadata/object-metadata/dtos/update-object.input';
|
||||
import { ObjectMetadataService } from 'src/metadata/object-metadata/object-metadata.service';
|
||||
import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';
|
||||
|
||||
@Injectable()
|
||||
export class BeforeUpdateOneObject<T extends UpdateObjectInput>
|
||||
implements BeforeUpdateOneHook<T, any>
|
||||
{
|
||||
constructor(
|
||||
readonly objectMetadataService: ObjectMetadataService,
|
||||
// TODO: Should not use the repository here
|
||||
@InjectRepository(FieldMetadataEntity, 'metadata')
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
) {}
|
||||
|
||||
// TODO: this logic could be moved to a policy guard
|
||||
async run(
|
||||
instance: UpdateOneInputType<T>,
|
||||
context: any,
|
||||
): Promise<UpdateOneInputType<T>> {
|
||||
const workspaceId = context?.req?.user?.workspace?.id;
|
||||
|
||||
if (!workspaceId) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
const objectMetadata =
|
||||
await this.objectMetadataService.findOneWithinWorkspace(workspaceId, {
|
||||
where: {
|
||||
id: instance.id.toString(),
|
||||
},
|
||||
});
|
||||
|
||||
if (!objectMetadata) {
|
||||
throw new BadRequestException('Object does not exist');
|
||||
}
|
||||
|
||||
if (!objectMetadata.isCustom) {
|
||||
if (
|
||||
Object.keys(instance.update).length === 1 &&
|
||||
instance.update.hasOwnProperty('isActive') &&
|
||||
instance.update.isActive !== undefined
|
||||
) {
|
||||
return {
|
||||
id: instance.id,
|
||||
update: {
|
||||
isActive: instance.update.isActive,
|
||||
} as T,
|
||||
};
|
||||
}
|
||||
|
||||
throw new BadRequestException(
|
||||
'Only isActive field can be updated for standard objects',
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
instance.update.labelIdentifierFieldMetadataId ||
|
||||
instance.update.imageIdentifierFieldMetadataId
|
||||
) {
|
||||
const fields = await this.fieldMetadataRepository.findBy({
|
||||
workspaceId: Equal(workspaceId),
|
||||
objectMetadataId: Equal(instance.id.toString()),
|
||||
id: In(
|
||||
[
|
||||
instance.update.labelIdentifierFieldMetadataId,
|
||||
instance.update.imageIdentifierFieldMetadataId,
|
||||
].filter((id) => id !== null),
|
||||
),
|
||||
});
|
||||
|
||||
const fieldIds = fields.map((field) => field.id);
|
||||
|
||||
if (
|
||||
instance.update.labelIdentifierFieldMetadataId &&
|
||||
!fieldIds.includes(instance.update.labelIdentifierFieldMetadataId)
|
||||
) {
|
||||
throw new BadRequestException('This label identifier does not exist');
|
||||
}
|
||||
|
||||
if (
|
||||
instance.update.imageIdentifierFieldMetadataId &&
|
||||
!fieldIds.includes(instance.update.imageIdentifierFieldMetadataId)
|
||||
) {
|
||||
throw new BadRequestException('This image identifier does not exist');
|
||||
}
|
||||
}
|
||||
|
||||
this.checkIfFieldIsEditable(instance.update, objectMetadata);
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
// This is temporary until we properly use the MigrationRunner to update column names
|
||||
private checkIfFieldIsEditable(
|
||||
update: UpdateObjectInput,
|
||||
objectMetadata: ObjectMetadataEntity,
|
||||
) {
|
||||
if (
|
||||
update.nameSingular &&
|
||||
update.nameSingular !== objectMetadata.nameSingular
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
"Object's nameSingular can't be updated. Please create a new object instead",
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
update.labelSingular &&
|
||||
update.labelSingular !== objectMetadata.labelSingular
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
"Object's labelSingular can't be updated. Please create a new object instead",
|
||||
);
|
||||
}
|
||||
|
||||
if (update.namePlural && update.namePlural !== objectMetadata.namePlural) {
|
||||
throw new BadRequestException(
|
||||
"Object's namePlural can't be updated. Please create a new object instead",
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
update.labelPlural &&
|
||||
update.labelPlural !== objectMetadata.labelPlural
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
"Object's labelPlural can't be updated. Please create a new object instead",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,103 @@
|
||||
import {
|
||||
Entity,
|
||||
Unique,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
OneToMany,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
ManyToOne,
|
||||
} from 'typeorm';
|
||||
|
||||
import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/object-metadata.interface';
|
||||
|
||||
import { FieldMetadataEntity } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import { RelationMetadataEntity } from 'src/metadata/relation-metadata/relation-metadata.entity';
|
||||
import { DataSourceEntity } from 'src/metadata/data-source/data-source.entity';
|
||||
|
||||
@Entity('objectMetadata')
|
||||
@Unique('IndexOnNameSingularAndWorkspaceIdUnique', [
|
||||
'nameSingular',
|
||||
'workspaceId',
|
||||
])
|
||||
@Unique('IndexOnNamePluralAndWorkspaceIdUnique', ['namePlural', 'workspaceId'])
|
||||
export class ObjectMetadataEntity implements ObjectMetadataInterface {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ nullable: false, type: 'uuid' })
|
||||
dataSourceId: string;
|
||||
|
||||
@Column({ nullable: false })
|
||||
nameSingular: string;
|
||||
|
||||
@Column({ nullable: false })
|
||||
namePlural: string;
|
||||
|
||||
@Column({ nullable: false })
|
||||
labelSingular: string;
|
||||
|
||||
@Column({ nullable: false })
|
||||
labelPlural: string;
|
||||
|
||||
@Column({ nullable: true, type: 'text' })
|
||||
description: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
icon: string;
|
||||
|
||||
@Column({ nullable: false })
|
||||
targetTableName: string;
|
||||
|
||||
@Column({ default: false })
|
||||
isCustom: boolean;
|
||||
|
||||
@Column({ default: false })
|
||||
isActive: boolean;
|
||||
|
||||
@Column({ default: false })
|
||||
isSystem: boolean;
|
||||
|
||||
@Column({ nullable: true })
|
||||
labelIdentifierFieldMetadataId?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
imageIdentifierFieldMetadataId?: string;
|
||||
|
||||
@Column({ nullable: false })
|
||||
workspaceId: string;
|
||||
|
||||
@OneToMany(() => FieldMetadataEntity, (field) => field.object, {
|
||||
cascade: true,
|
||||
})
|
||||
fields: FieldMetadataEntity[];
|
||||
|
||||
@OneToMany(
|
||||
() => RelationMetadataEntity,
|
||||
(relation: RelationMetadataEntity) => relation.fromObjectMetadata,
|
||||
{
|
||||
cascade: true,
|
||||
},
|
||||
)
|
||||
fromRelations: RelationMetadataEntity[];
|
||||
|
||||
@OneToMany(
|
||||
() => RelationMetadataEntity,
|
||||
(relation: RelationMetadataEntity) => relation.toObjectMetadata,
|
||||
{
|
||||
cascade: true,
|
||||
},
|
||||
)
|
||||
toRelations: RelationMetadataEntity[];
|
||||
|
||||
@ManyToOne(() => DataSourceEntity, (dataSource) => dataSource.objects, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
dataSource: DataSourceEntity;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt: Date;
|
||||
}
|
||||
@ -0,0 +1,66 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
NestjsQueryGraphQLModule,
|
||||
PagingStrategies,
|
||||
} from '@ptc-org/nestjs-query-graphql';
|
||||
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
|
||||
import { SortDirection } from '@ptc-org/nestjs-query-core';
|
||||
|
||||
import { DataSourceModule } from 'src/metadata/data-source/data-source.module';
|
||||
import { WorkspaceMigrationRunnerModule } from 'src/workspace/workspace-migration-runner/workspace-migration-runner.module';
|
||||
import { WorkspaceMigrationModule } from 'src/metadata/workspace-migration/workspace-migration.module';
|
||||
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
|
||||
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
||||
import { FieldMetadataEntity } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import { RelationMetadataEntity } from 'src/metadata/relation-metadata/relation-metadata.entity';
|
||||
|
||||
import { ObjectMetadataService } from './object-metadata.service';
|
||||
import { ObjectMetadataEntity } from './object-metadata.entity';
|
||||
|
||||
import { CreateObjectInput } from './dtos/create-object.input';
|
||||
import { UpdateObjectInput } from './dtos/update-object.input';
|
||||
import { ObjectMetadataDTO } from './dtos/object-metadata.dto';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
NestjsQueryGraphQLModule.forFeature({
|
||||
imports: [
|
||||
TypeORMModule,
|
||||
NestjsQueryTypeOrmModule.forFeature(
|
||||
[ObjectMetadataEntity, FieldMetadataEntity, RelationMetadataEntity],
|
||||
'metadata',
|
||||
),
|
||||
DataSourceModule,
|
||||
WorkspaceMigrationModule,
|
||||
WorkspaceMigrationRunnerModule,
|
||||
],
|
||||
services: [ObjectMetadataService],
|
||||
resolvers: [
|
||||
{
|
||||
EntityClass: ObjectMetadataEntity,
|
||||
DTOClass: ObjectMetadataDTO,
|
||||
CreateDTOClass: CreateObjectInput,
|
||||
UpdateDTOClass: UpdateObjectInput,
|
||||
ServiceClass: ObjectMetadataService,
|
||||
enableTotalCount: true,
|
||||
pagingStrategy: PagingStrategies.CURSOR,
|
||||
read: {
|
||||
defaultSort: [{ field: 'id', direction: SortDirection.DESC }],
|
||||
},
|
||||
create: {
|
||||
many: { disabled: true },
|
||||
},
|
||||
update: {
|
||||
many: { disabled: true },
|
||||
},
|
||||
delete: { many: { disabled: true } },
|
||||
guards: [JwtAuthGuard],
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
providers: [ObjectMetadataService],
|
||||
exports: [ObjectMetadataService],
|
||||
})
|
||||
export class ObjectMetadataModule {}
|
||||
@ -0,0 +1,346 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { FindManyOptions, FindOneOptions, Repository } from 'typeorm';
|
||||
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
||||
|
||||
import { WorkspaceMigrationService } from 'src/metadata/workspace-migration/workspace-migration.service';
|
||||
import { WorkspaceMigrationRunnerService } from 'src/workspace/workspace-migration-runner/workspace-migration-runner.service';
|
||||
import {
|
||||
WorkspaceMigrationColumnActionType,
|
||||
WorkspaceMigrationColumnCreate,
|
||||
WorkspaceMigrationTableAction,
|
||||
} from 'src/metadata/workspace-migration/workspace-migration.entity';
|
||||
import {
|
||||
FieldMetadataEntity,
|
||||
FieldMetadataType,
|
||||
} from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
|
||||
import { DataSourceService } from 'src/metadata/data-source/data-source.service';
|
||||
import {
|
||||
RelationMetadataEntity,
|
||||
RelationMetadataType,
|
||||
} from 'src/metadata/relation-metadata/relation-metadata.entity';
|
||||
import { createCustomColumnName } from 'src/metadata/utils/create-custom-column-name.util';
|
||||
|
||||
import { ObjectMetadataEntity } from './object-metadata.entity';
|
||||
|
||||
import { CreateObjectInput } from './dtos/create-object.input';
|
||||
|
||||
@Injectable()
|
||||
export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEntity> {
|
||||
constructor(
|
||||
@InjectRepository(ObjectMetadataEntity, 'metadata')
|
||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||
|
||||
@InjectRepository(FieldMetadataEntity, 'metadata')
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
|
||||
@InjectRepository(RelationMetadataEntity, 'metadata')
|
||||
private readonly relationMetadataRepository: Repository<RelationMetadataEntity>,
|
||||
|
||||
private readonly dataSourceService: DataSourceService,
|
||||
private readonly typeORMService: TypeORMService,
|
||||
private readonly workspaceMigrationService: WorkspaceMigrationService,
|
||||
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
|
||||
) {
|
||||
super(objectMetadataRepository);
|
||||
}
|
||||
|
||||
override async createOne(
|
||||
record: CreateObjectInput,
|
||||
): Promise<ObjectMetadataEntity> {
|
||||
const lastDataSourceMetadata =
|
||||
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
|
||||
record.workspaceId,
|
||||
);
|
||||
|
||||
if (record.nameSingular.toLowerCase() === record.namePlural.toLowerCase()) {
|
||||
throw new Error(
|
||||
'The singular and plural name cannot be the same for an object',
|
||||
);
|
||||
}
|
||||
|
||||
const createdObjectMetadata = await super.createOne({
|
||||
...record,
|
||||
dataSourceId: lastDataSourceMetadata.id,
|
||||
targetTableName: createCustomColumnName(record.nameSingular),
|
||||
isActive: true,
|
||||
isCustom: true,
|
||||
isSystem: false,
|
||||
fields:
|
||||
// Creating default fields.
|
||||
// No need to create a custom migration for this though as the default columns are already
|
||||
// created with default values which is not supported yet by workspace migrations.
|
||||
[
|
||||
{
|
||||
type: FieldMetadataType.UUID,
|
||||
name: 'id',
|
||||
label: 'Id',
|
||||
targetColumnMap: {
|
||||
value: 'id',
|
||||
},
|
||||
icon: 'Icon123',
|
||||
description: 'Id',
|
||||
isNullable: true,
|
||||
isActive: true,
|
||||
isCustom: false,
|
||||
isSystem: true,
|
||||
workspaceId: record.workspaceId,
|
||||
defaultValue: { type: 'uuid' },
|
||||
},
|
||||
{
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
targetColumnMap: {
|
||||
value: 'name',
|
||||
},
|
||||
icon: 'IconAbc',
|
||||
description: 'Name',
|
||||
isNullable: true,
|
||||
isActive: true,
|
||||
isCustom: false,
|
||||
workspaceId: record.workspaceId,
|
||||
defaultValue: { value: 'Untitled' },
|
||||
},
|
||||
{
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
name: 'createdAt',
|
||||
label: 'Creation date',
|
||||
targetColumnMap: {
|
||||
value: 'createdAt',
|
||||
},
|
||||
icon: 'IconCalendar',
|
||||
description: 'Creation date',
|
||||
isNullable: true,
|
||||
isActive: true,
|
||||
isCustom: false,
|
||||
workspaceId: record.workspaceId,
|
||||
defaultValue: { type: 'now' },
|
||||
},
|
||||
{
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
name: 'updatedAt',
|
||||
label: 'Update date',
|
||||
targetColumnMap: {
|
||||
value: 'updatedAt',
|
||||
},
|
||||
icon: 'IconCalendar',
|
||||
description: 'Update date',
|
||||
isNullable: true,
|
||||
isActive: true,
|
||||
isCustom: false,
|
||||
isSystem: true,
|
||||
workspaceId: record.workspaceId,
|
||||
defaultValue: { type: 'now' },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const activityTargetObjectMetadata =
|
||||
await this.objectMetadataRepository.findOneByOrFail({
|
||||
nameSingular: 'activityTarget',
|
||||
workspaceId: record.workspaceId,
|
||||
});
|
||||
|
||||
const activityTargetRelationFieldMetadata =
|
||||
await this.fieldMetadataRepository.save([
|
||||
// FROM
|
||||
{
|
||||
objectMetadataId: createdObjectMetadata.id,
|
||||
workspaceId: record.workspaceId,
|
||||
isCustom: true,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'activityTargets',
|
||||
label: 'Activities',
|
||||
targetColumnMap: {},
|
||||
description: `Activities tied to the ${record.labelSingular}`,
|
||||
icon: 'IconCheckbox',
|
||||
isNullable: true,
|
||||
},
|
||||
// TO
|
||||
{
|
||||
objectMetadataId: activityTargetObjectMetadata.id,
|
||||
workspaceId: record.workspaceId,
|
||||
isCustom: true,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: record.nameSingular,
|
||||
label: record.labelSingular,
|
||||
targetColumnMap: {
|
||||
value: `${createdObjectMetadata.targetTableName}Id`,
|
||||
},
|
||||
description: `ActivityTarget ${record.labelSingular}`,
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
isNullable: true,
|
||||
},
|
||||
// Foreign key
|
||||
{
|
||||
objectMetadataId: activityTargetObjectMetadata.id,
|
||||
workspaceId: record.workspaceId,
|
||||
isCustom: true,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.UUID,
|
||||
name: `${createdObjectMetadata.targetTableName}Id`,
|
||||
label: `${record.labelSingular} ID (foreign key)`,
|
||||
targetColumnMap: {},
|
||||
description: `ActivityTarget ${record.labelSingular} id foreign key`,
|
||||
icon: undefined,
|
||||
isNullable: true,
|
||||
isSystem: true,
|
||||
defaultValue: undefined,
|
||||
},
|
||||
]);
|
||||
|
||||
const activityTargetRelationFieldMetadataMap =
|
||||
activityTargetRelationFieldMetadata.reduce(
|
||||
(acc, fieldMetadata: FieldMetadataEntity) => {
|
||||
if (fieldMetadata.type === FieldMetadataType.RELATION) {
|
||||
acc[fieldMetadata.objectMetadataId] = fieldMetadata;
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
await this.relationMetadataRepository.save([
|
||||
{
|
||||
workspaceId: record.workspaceId,
|
||||
relationType: RelationMetadataType.ONE_TO_MANY,
|
||||
fromObjectMetadataId: createdObjectMetadata.id,
|
||||
toObjectMetadataId: activityTargetObjectMetadata.id,
|
||||
fromFieldMetadataId:
|
||||
activityTargetRelationFieldMetadataMap[createdObjectMetadata.id].id,
|
||||
toFieldMetadataId:
|
||||
activityTargetRelationFieldMetadataMap[
|
||||
activityTargetObjectMetadata.id
|
||||
].id,
|
||||
},
|
||||
]);
|
||||
|
||||
await this.workspaceMigrationService.createCustomMigration(
|
||||
createdObjectMetadata.workspaceId,
|
||||
[
|
||||
{
|
||||
name: createdObjectMetadata.targetTableName,
|
||||
action: 'create',
|
||||
} satisfies WorkspaceMigrationTableAction,
|
||||
// Add activity target relation
|
||||
{
|
||||
name: activityTargetObjectMetadata.targetTableName,
|
||||
action: 'alter',
|
||||
columns: [
|
||||
{
|
||||
action: WorkspaceMigrationColumnActionType.CREATE,
|
||||
columnName: `${createdObjectMetadata.targetTableName}Id`,
|
||||
columnType: 'uuid',
|
||||
} satisfies WorkspaceMigrationColumnCreate,
|
||||
],
|
||||
},
|
||||
{
|
||||
name: activityTargetObjectMetadata.targetTableName,
|
||||
action: 'alter',
|
||||
columns: [
|
||||
{
|
||||
action: WorkspaceMigrationColumnActionType.RELATION,
|
||||
columnName: `${createdObjectMetadata.targetTableName}Id`,
|
||||
referencedTableName: createdObjectMetadata.targetTableName,
|
||||
referencedTableColumnName: 'id',
|
||||
},
|
||||
],
|
||||
},
|
||||
// This is temporary until we implement mainIdentifier
|
||||
{
|
||||
name: createdObjectMetadata.targetTableName,
|
||||
action: 'alter',
|
||||
columns: [
|
||||
{
|
||||
action: WorkspaceMigrationColumnActionType.CREATE,
|
||||
columnName: 'name',
|
||||
columnType: 'varchar',
|
||||
defaultValue: "'Untitled'",
|
||||
} satisfies WorkspaceMigrationColumnCreate,
|
||||
],
|
||||
} satisfies WorkspaceMigrationTableAction,
|
||||
],
|
||||
);
|
||||
|
||||
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
|
||||
createdObjectMetadata.workspaceId,
|
||||
);
|
||||
|
||||
const dataSourceMetadata =
|
||||
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
|
||||
createdObjectMetadata.workspaceId,
|
||||
);
|
||||
|
||||
const workspaceDataSource = await this.typeORMService.connectToDataSource(
|
||||
dataSourceMetadata,
|
||||
);
|
||||
|
||||
const view = await workspaceDataSource?.query(
|
||||
`INSERT INTO ${dataSourceMetadata.schema}."view"
|
||||
("objectMetadataId", "type", "name")
|
||||
VALUES ('${createdObjectMetadata.id}', 'table', 'All ${createdObjectMetadata.namePlural}') RETURNING *`,
|
||||
);
|
||||
|
||||
createdObjectMetadata.fields.map(async (field, index) => {
|
||||
if (field.name === 'id') {
|
||||
return;
|
||||
}
|
||||
|
||||
await workspaceDataSource?.query(
|
||||
`INSERT INTO ${dataSourceMetadata.schema}."viewField"
|
||||
("fieldMetadataId", "position", "isVisible", "size", "viewId")
|
||||
VALUES ('${field.id}', '${index - 1}', true, 180, '${
|
||||
view[0].id
|
||||
}') RETURNING *`,
|
||||
);
|
||||
});
|
||||
|
||||
return createdObjectMetadata;
|
||||
}
|
||||
|
||||
public async findOneWithinWorkspace(
|
||||
workspaceId: string,
|
||||
options: FindOneOptions<ObjectMetadataEntity>,
|
||||
): Promise<ObjectMetadataEntity | null> {
|
||||
return this.objectMetadataRepository.findOne({
|
||||
...options,
|
||||
where: {
|
||||
...options.where,
|
||||
workspaceId,
|
||||
},
|
||||
relations: [
|
||||
'fields',
|
||||
'fields.fromRelationMetadata',
|
||||
'fields.toRelationMetadata',
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
public async findManyWithinWorkspace(
|
||||
workspaceId: string,
|
||||
options?: FindManyOptions<ObjectMetadataEntity>,
|
||||
) {
|
||||
return this.objectMetadataRepository.find({
|
||||
...options,
|
||||
where: {
|
||||
...options?.where,
|
||||
workspaceId,
|
||||
},
|
||||
relations: [
|
||||
'fields',
|
||||
'fields.fromRelationMetadata',
|
||||
'fields.toRelationMetadata',
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
public async deleteObjectsMetadata(workspaceId: string) {
|
||||
await this.objectMetadataRepository.delete({ workspaceId });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user