1/ When the user inputs wrong connection informations, we do not inform him. He will only see that no tables are available. We will display a connection failed status if an error is raised testing the connection 2/ If the connection fails, it should still be possible to delete the server. Today, since we try first to delete the tables, the connection failure throws an error that will prevent server deletion. Using the foreign tables instead of calling the distant DB. 3/ Redirect to connection show page instead of connection list after creation 4/ Today, foreign tables are fetched without the server name. This is a mistake because we need to know which foreign table is linked with which server. Updating the associated query. <img width="632" alt="Capture d’écran 2024-04-12 à 10 52 49" src="https://github.com/twentyhq/twenty/assets/22936103/9e8406b8-75d0-494c-ac1f-5e9fa7100f5c"> --------- Co-authored-by: Thomas Trompette <thomast@twenty.com>
906 lines
30 KiB
TypeScript
906 lines
30 KiB
TypeScript
import {
|
|
BadRequestException,
|
|
Injectable,
|
|
NotFoundException,
|
|
} from '@nestjs/common';
|
|
import { InjectRepository } from '@nestjs/typeorm';
|
|
|
|
import console from 'console';
|
|
|
|
import {
|
|
DataSource,
|
|
FindManyOptions,
|
|
FindOneOptions,
|
|
Repository,
|
|
} from 'typeorm';
|
|
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
|
import { Query, QueryOptions } from '@ptc-org/nestjs-query-core';
|
|
|
|
import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service';
|
|
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service';
|
|
import {
|
|
WorkspaceMigrationColumnActionType,
|
|
WorkspaceMigrationColumnDrop,
|
|
WorkspaceMigrationTableActionType,
|
|
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
|
|
import {
|
|
FieldMetadataEntity,
|
|
FieldMetadataType,
|
|
} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
|
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
|
|
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
|
import {
|
|
RelationMetadataEntity,
|
|
RelationMetadataType,
|
|
RelationOnDeleteAction,
|
|
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
|
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
|
|
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
|
import { DeleteOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/delete-object.input';
|
|
import { RelationToDelete } from 'src/engine/metadata-modules/relation-metadata/types/relation-to-delete';
|
|
import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
|
|
import {
|
|
activityTargetStandardFieldIds,
|
|
attachmentStandardFieldIds,
|
|
baseObjectStandardFieldIds,
|
|
customObjectStandardFieldIds,
|
|
eventStandardFieldIds,
|
|
favoriteStandardFieldIds,
|
|
} from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
|
import {
|
|
createForeignKeyDeterministicUuid,
|
|
createRelationDeterministicUuid,
|
|
} from 'src/engine/workspace-manager/workspace-sync-metadata/utils/create-deterministic-uuid.util';
|
|
import { createWorkspaceMigrationsForCustomObject } from 'src/engine/metadata-modules/object-metadata/utils/create-workspace-migrations-for-custom-object.util';
|
|
import { createWorkspaceMigrationsForRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/create-workspace-migrations-for-remote-object.util';
|
|
import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
|
|
import {
|
|
FeatureFlagEntity,
|
|
FeatureFlagKeys,
|
|
} from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
|
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
|
|
|
|
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,
|
|
@InjectRepository(FeatureFlagEntity, 'core')
|
|
private readonly featureFlagRepository: Repository<FeatureFlagEntity>,
|
|
) {
|
|
super(objectMetadataRepository);
|
|
}
|
|
|
|
override async query(
|
|
query: Query<ObjectMetadataEntity>,
|
|
opts?: QueryOptions<ObjectMetadataEntity> | undefined,
|
|
): Promise<ObjectMetadataEntity[]> {
|
|
const start = performance.now();
|
|
|
|
const result = super.query(query, opts);
|
|
|
|
const end = performance.now();
|
|
|
|
console.log(`metadata query time: ${end - start} ms`);
|
|
|
|
return result;
|
|
}
|
|
|
|
public async deleteOneObject(
|
|
input: DeleteOneObjectInput,
|
|
workspaceId: string,
|
|
): Promise<ObjectMetadataEntity> {
|
|
const objectMetadata = await this.objectMetadataRepository.findOne({
|
|
relations: [
|
|
'fromRelations.fromFieldMetadata',
|
|
'fromRelations.toFieldMetadata',
|
|
'toRelations.fromFieldMetadata',
|
|
'toRelations.toFieldMetadata',
|
|
'fromRelations.fromObjectMetadata',
|
|
'fromRelations.toObjectMetadata',
|
|
'toRelations.fromObjectMetadata',
|
|
'toRelations.toObjectMetadata',
|
|
],
|
|
where: {
|
|
id: input.id,
|
|
workspaceId,
|
|
},
|
|
});
|
|
|
|
if (!objectMetadata) {
|
|
throw new NotFoundException('Object does not exist');
|
|
}
|
|
|
|
const relationsToDelete: RelationToDelete[] = [];
|
|
|
|
// TODO: Most of this logic should be moved to relation-metadata.service.ts
|
|
for (const relation of [
|
|
...objectMetadata.fromRelations,
|
|
...objectMetadata.toRelations,
|
|
]) {
|
|
relationsToDelete.push({
|
|
id: relation.id,
|
|
fromFieldMetadataId: relation.fromFieldMetadata.id,
|
|
toFieldMetadataId: relation.toFieldMetadata.id,
|
|
fromFieldMetadataName: relation.fromFieldMetadata.name,
|
|
toFieldMetadataName: relation.toFieldMetadata.name,
|
|
fromObjectMetadataId: relation.fromObjectMetadata.id,
|
|
toObjectMetadataId: relation.toObjectMetadata.id,
|
|
fromObjectName: relation.fromObjectMetadata.nameSingular,
|
|
toObjectName: relation.toObjectMetadata.nameSingular,
|
|
toFieldMetadataIsCustom: relation.toFieldMetadata.isCustom,
|
|
toObjectMetadataIsCustom: relation.toObjectMetadata.isCustom,
|
|
direction:
|
|
relation.fromObjectMetadata.nameSingular ===
|
|
objectMetadata.nameSingular
|
|
? 'from'
|
|
: 'to',
|
|
});
|
|
}
|
|
|
|
if (relationsToDelete.length > 0) {
|
|
await this.relationMetadataRepository.delete(
|
|
relationsToDelete.map((relation) => relation.id),
|
|
);
|
|
}
|
|
|
|
for (const relationToDelete of relationsToDelete) {
|
|
const foreignKeyFieldsToDelete = await this.fieldMetadataRepository.find({
|
|
where: {
|
|
name: `${relationToDelete.toFieldMetadataName}Id`,
|
|
objectMetadataId: relationToDelete.toObjectMetadataId,
|
|
workspaceId,
|
|
},
|
|
});
|
|
|
|
const foreignKeyFieldsToDeleteIds = foreignKeyFieldsToDelete.map(
|
|
(field) => field.id,
|
|
);
|
|
|
|
await this.fieldMetadataRepository.delete([
|
|
...foreignKeyFieldsToDeleteIds,
|
|
relationToDelete.fromFieldMetadataId,
|
|
relationToDelete.toFieldMetadataId,
|
|
]);
|
|
|
|
if (relationToDelete.direction === 'from') {
|
|
await this.workspaceMigrationService.createCustomMigration(
|
|
generateMigrationName(
|
|
`delete-${relationToDelete.fromObjectName}-${relationToDelete.toObjectName}`,
|
|
),
|
|
workspaceId,
|
|
[
|
|
{
|
|
name: computeTableName(
|
|
relationToDelete.toObjectName,
|
|
relationToDelete.toObjectMetadataIsCustom,
|
|
),
|
|
action: WorkspaceMigrationTableActionType.ALTER,
|
|
columns: [
|
|
{
|
|
action: WorkspaceMigrationColumnActionType.DROP,
|
|
columnName: computeColumnName(
|
|
relationToDelete.toFieldMetadataName,
|
|
{ isForeignKey: true },
|
|
),
|
|
} satisfies WorkspaceMigrationColumnDrop,
|
|
],
|
|
},
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
await this.objectMetadataRepository.delete(objectMetadata.id);
|
|
|
|
if (!objectMetadata.isRemote) {
|
|
// DROP TABLE
|
|
await this.workspaceMigrationService.createCustomMigration(
|
|
generateMigrationName(`delete-${objectMetadata.nameSingular}`),
|
|
workspaceId,
|
|
[
|
|
{
|
|
name: computeObjectTargetTable(objectMetadata),
|
|
action: WorkspaceMigrationTableActionType.DROP,
|
|
},
|
|
],
|
|
);
|
|
}
|
|
|
|
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
|
|
workspaceId,
|
|
);
|
|
|
|
return objectMetadata;
|
|
}
|
|
|
|
override async createOne(
|
|
objectMetadataInput: CreateObjectInput,
|
|
): Promise<ObjectMetadataEntity> {
|
|
const lastDataSourceMetadata =
|
|
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
|
|
objectMetadataInput.workspaceId,
|
|
);
|
|
|
|
if (
|
|
objectMetadataInput.nameSingular.toLowerCase() ===
|
|
objectMetadataInput.namePlural.toLowerCase()
|
|
) {
|
|
throw new BadRequestException(
|
|
'The singular and plural name cannot be the same for an object',
|
|
);
|
|
}
|
|
|
|
const isCustom = !objectMetadataInput.isRemote;
|
|
|
|
const createdObjectMetadata = await super.createOne({
|
|
...objectMetadataInput,
|
|
dataSourceId: lastDataSourceMetadata.id,
|
|
targetTableName: 'DEPRECATED',
|
|
isActive: true,
|
|
isCustom: isCustom,
|
|
isSystem: false,
|
|
isRemote: objectMetadataInput.isRemote,
|
|
fields: isCustom
|
|
? // 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.
|
|
[
|
|
{
|
|
standardId: baseObjectStandardFieldIds.id,
|
|
type: FieldMetadataType.UUID,
|
|
name: 'id',
|
|
label: 'Id',
|
|
icon: 'Icon123',
|
|
description: 'Id',
|
|
isNullable: false,
|
|
isActive: true,
|
|
isCustom: false,
|
|
isSystem: true,
|
|
workspaceId: objectMetadataInput.workspaceId,
|
|
defaultValue: 'uuid',
|
|
},
|
|
{
|
|
standardId: customObjectStandardFieldIds.name,
|
|
type: FieldMetadataType.TEXT,
|
|
name: 'name',
|
|
label: 'Name',
|
|
icon: 'IconAbc',
|
|
description: 'Name',
|
|
isNullable: false,
|
|
isActive: true,
|
|
isCustom: false,
|
|
workspaceId: objectMetadataInput.workspaceId,
|
|
defaultValue: "'Untitled'",
|
|
},
|
|
{
|
|
standardId: baseObjectStandardFieldIds.createdAt,
|
|
type: FieldMetadataType.DATE_TIME,
|
|
name: 'createdAt',
|
|
label: 'Creation date',
|
|
icon: 'IconCalendar',
|
|
description: 'Creation date',
|
|
isNullable: false,
|
|
isActive: true,
|
|
isCustom: false,
|
|
workspaceId: objectMetadataInput.workspaceId,
|
|
defaultValue: 'now',
|
|
},
|
|
{
|
|
standardId: baseObjectStandardFieldIds.updatedAt,
|
|
type: FieldMetadataType.DATE_TIME,
|
|
name: 'updatedAt',
|
|
label: 'Update date',
|
|
icon: 'IconCalendar',
|
|
description: 'Update date',
|
|
isNullable: false,
|
|
isActive: true,
|
|
isCustom: false,
|
|
isSystem: true,
|
|
workspaceId: objectMetadataInput.workspaceId,
|
|
defaultValue: 'now',
|
|
},
|
|
{
|
|
standardId: customObjectStandardFieldIds.position,
|
|
type: FieldMetadataType.POSITION,
|
|
name: 'position',
|
|
label: 'Position',
|
|
icon: 'IconHierarchy2',
|
|
description: 'Position',
|
|
isNullable: true,
|
|
isActive: true,
|
|
isCustom: false,
|
|
isSystem: true,
|
|
workspaceId: objectMetadataInput.workspaceId,
|
|
defaultValue: null,
|
|
},
|
|
]
|
|
: // No fields for remote objects.
|
|
[],
|
|
});
|
|
|
|
const dataSourceMetadata =
|
|
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
|
|
createdObjectMetadata.workspaceId,
|
|
);
|
|
|
|
const workspaceDataSource =
|
|
await this.typeORMService.connectToDataSource(dataSourceMetadata);
|
|
|
|
await this.createObjectRelationsMetadataAndMigrations(
|
|
objectMetadataInput,
|
|
createdObjectMetadata,
|
|
lastDataSourceMetadata,
|
|
workspaceDataSource,
|
|
objectMetadataInput.isRemote,
|
|
);
|
|
|
|
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
|
|
createdObjectMetadata.workspaceId,
|
|
);
|
|
|
|
const view = await workspaceDataSource?.query(
|
|
`INSERT INTO ${dataSourceMetadata.schema}."view"
|
|
("objectMetadataId", "type", "name", "key", "icon")
|
|
VALUES ($1, $2, $3, $4, $5) RETURNING *`,
|
|
[
|
|
createdObjectMetadata.id,
|
|
'table',
|
|
`All ${createdObjectMetadata.labelPlural}`,
|
|
'INDEX',
|
|
createdObjectMetadata.icon,
|
|
],
|
|
);
|
|
|
|
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({
|
|
relations: [
|
|
'fields',
|
|
'fields.fromRelationMetadata',
|
|
'fields.toRelationMetadata',
|
|
],
|
|
...options,
|
|
where: {
|
|
...options.where,
|
|
workspaceId,
|
|
},
|
|
});
|
|
}
|
|
|
|
public async findOneOrFailWithinWorkspace(
|
|
workspaceId: string,
|
|
options: FindOneOptions<ObjectMetadataEntity>,
|
|
): Promise<ObjectMetadataEntity> {
|
|
return this.objectMetadataRepository.findOneOrFail({
|
|
relations: [
|
|
'fields',
|
|
'fields.fromRelationMetadata',
|
|
'fields.toRelationMetadata',
|
|
],
|
|
...options,
|
|
where: {
|
|
...options.where,
|
|
workspaceId,
|
|
},
|
|
});
|
|
}
|
|
|
|
public async findManyWithinWorkspace(
|
|
workspaceId: string,
|
|
options?: FindManyOptions<ObjectMetadataEntity>,
|
|
) {
|
|
return this.objectMetadataRepository.find({
|
|
relations: [
|
|
'fields.object',
|
|
'fields',
|
|
'fields.fromRelationMetadata',
|
|
'fields.toRelationMetadata',
|
|
'fields.fromRelationMetadata.toObjectMetadata',
|
|
],
|
|
...options,
|
|
where: {
|
|
...options?.where,
|
|
workspaceId,
|
|
},
|
|
});
|
|
}
|
|
|
|
public async findMany(options?: FindManyOptions<ObjectMetadataEntity>) {
|
|
return this.objectMetadataRepository.find({
|
|
relations: [
|
|
'fields',
|
|
'fields.fromRelationMetadata',
|
|
'fields.toRelationMetadata',
|
|
'fields.fromRelationMetadata.toObjectMetadata',
|
|
],
|
|
...options,
|
|
where: {
|
|
...options?.where,
|
|
},
|
|
});
|
|
}
|
|
|
|
public async deleteObjectsMetadata(workspaceId: string) {
|
|
await this.objectMetadataRepository.delete({ workspaceId });
|
|
}
|
|
|
|
private async createObjectRelationsMetadataAndMigrations(
|
|
objectMetadataInput: CreateObjectInput,
|
|
createdObjectMetadata: ObjectMetadataEntity,
|
|
lastDataSourceMetadata: DataSourceEntity,
|
|
workspaceDataSource: DataSource | undefined,
|
|
isRemoteObject: boolean = false,
|
|
) {
|
|
const isRelationEnabledForRemoteObjects =
|
|
await this.isRelationEnabledForRemoteObjects(
|
|
objectMetadataInput.workspaceId,
|
|
);
|
|
|
|
if (isRemoteObject && !isRelationEnabledForRemoteObjects) {
|
|
return;
|
|
}
|
|
|
|
const { eventObjectMetadata } = await this.createEventRelation(
|
|
objectMetadataInput.workspaceId,
|
|
createdObjectMetadata,
|
|
);
|
|
|
|
const { activityTargetObjectMetadata } =
|
|
await this.createActivityTargetRelation(
|
|
objectMetadataInput.workspaceId,
|
|
createdObjectMetadata,
|
|
);
|
|
|
|
const { favoriteObjectMetadata } = await this.createFavoriteRelation(
|
|
objectMetadataInput.workspaceId,
|
|
createdObjectMetadata,
|
|
);
|
|
|
|
const { attachmentObjectMetadata } = await this.createAttachmentRelation(
|
|
objectMetadataInput.workspaceId,
|
|
createdObjectMetadata,
|
|
);
|
|
|
|
return this.workspaceMigrationService.createCustomMigration(
|
|
generateMigrationName(`create-${createdObjectMetadata.nameSingular}`),
|
|
createdObjectMetadata.workspaceId,
|
|
isRemoteObject
|
|
? await createWorkspaceMigrationsForRemoteObject(
|
|
createdObjectMetadata,
|
|
activityTargetObjectMetadata,
|
|
attachmentObjectMetadata,
|
|
eventObjectMetadata,
|
|
favoriteObjectMetadata,
|
|
lastDataSourceMetadata.schema,
|
|
objectMetadataInput.remoteTablePrimaryKeyColumnType ?? 'uuid',
|
|
workspaceDataSource,
|
|
)
|
|
: createWorkspaceMigrationsForCustomObject(
|
|
createdObjectMetadata,
|
|
activityTargetObjectMetadata,
|
|
attachmentObjectMetadata,
|
|
eventObjectMetadata,
|
|
favoriteObjectMetadata,
|
|
),
|
|
);
|
|
}
|
|
|
|
private async createActivityTargetRelation(
|
|
workspaceId: string,
|
|
createdObjectMetadata: ObjectMetadataEntity,
|
|
) {
|
|
const activityTargetObjectMetadata =
|
|
await this.objectMetadataRepository.findOneByOrFail({
|
|
nameSingular: 'activityTarget',
|
|
workspaceId: workspaceId,
|
|
});
|
|
|
|
const activityTargetRelationFieldMetadata =
|
|
await this.fieldMetadataRepository.save([
|
|
// FROM
|
|
{
|
|
standardId: customObjectStandardFieldIds.activityTargets,
|
|
objectMetadataId: createdObjectMetadata.id,
|
|
workspaceId: workspaceId,
|
|
isCustom: false,
|
|
isActive: true,
|
|
type: FieldMetadataType.RELATION,
|
|
name: 'activityTargets',
|
|
label: 'Activities',
|
|
description: `Activities tied to the ${createdObjectMetadata.labelSingular}`,
|
|
icon: 'IconCheckbox',
|
|
isNullable: true,
|
|
},
|
|
// TO
|
|
{
|
|
standardId: createRelationDeterministicUuid({
|
|
objectId: createdObjectMetadata.id,
|
|
standardId: activityTargetStandardFieldIds.custom,
|
|
}),
|
|
objectMetadataId: activityTargetObjectMetadata.id,
|
|
workspaceId: workspaceId,
|
|
isCustom: false,
|
|
isActive: true,
|
|
type: FieldMetadataType.RELATION,
|
|
name: createdObjectMetadata.nameSingular,
|
|
label: createdObjectMetadata.labelSingular,
|
|
description: `ActivityTarget ${createdObjectMetadata.labelSingular}`,
|
|
icon: 'IconBuildingSkyscraper',
|
|
isNullable: true,
|
|
},
|
|
// Foreign key
|
|
{
|
|
standardId: createForeignKeyDeterministicUuid({
|
|
objectId: createdObjectMetadata.id,
|
|
standardId: activityTargetStandardFieldIds.custom,
|
|
}),
|
|
objectMetadataId: activityTargetObjectMetadata.id,
|
|
workspaceId: workspaceId,
|
|
isCustom: false,
|
|
isActive: true,
|
|
type: FieldMetadataType.UUID,
|
|
name: `${createdObjectMetadata.nameSingular}Id`,
|
|
label: `${createdObjectMetadata.labelSingular} ID (foreign key)`,
|
|
description: `ActivityTarget ${createdObjectMetadata.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: workspaceId,
|
|
relationType: RelationMetadataType.ONE_TO_MANY,
|
|
fromObjectMetadataId: createdObjectMetadata.id,
|
|
toObjectMetadataId: activityTargetObjectMetadata.id,
|
|
fromFieldMetadataId:
|
|
activityTargetRelationFieldMetadataMap[createdObjectMetadata.id].id,
|
|
toFieldMetadataId:
|
|
activityTargetRelationFieldMetadataMap[
|
|
activityTargetObjectMetadata.id
|
|
].id,
|
|
onDeleteAction: RelationOnDeleteAction.CASCADE,
|
|
},
|
|
]);
|
|
|
|
return { activityTargetObjectMetadata };
|
|
}
|
|
|
|
private async createAttachmentRelation(
|
|
workspaceId: string,
|
|
createdObjectMetadata: ObjectMetadataEntity,
|
|
) {
|
|
const attachmentObjectMetadata =
|
|
await this.objectMetadataRepository.findOneByOrFail({
|
|
nameSingular: 'attachment',
|
|
workspaceId: workspaceId,
|
|
});
|
|
|
|
const attachmentRelationFieldMetadata =
|
|
await this.fieldMetadataRepository.save([
|
|
// FROM
|
|
{
|
|
standardId: customObjectStandardFieldIds.attachments,
|
|
objectMetadataId: createdObjectMetadata.id,
|
|
workspaceId: workspaceId,
|
|
isCustom: false,
|
|
isActive: true,
|
|
type: FieldMetadataType.RELATION,
|
|
name: 'attachments',
|
|
label: 'Attachments',
|
|
description: `Attachments tied to the ${createdObjectMetadata.labelSingular}`,
|
|
icon: 'IconFileImport',
|
|
isNullable: true,
|
|
},
|
|
// TO
|
|
{
|
|
standardId: createRelationDeterministicUuid({
|
|
objectId: createdObjectMetadata.id,
|
|
standardId: attachmentStandardFieldIds.custom,
|
|
}),
|
|
objectMetadataId: attachmentObjectMetadata.id,
|
|
workspaceId: workspaceId,
|
|
isCustom: false,
|
|
isActive: true,
|
|
type: FieldMetadataType.RELATION,
|
|
name: createdObjectMetadata.nameSingular,
|
|
label: createdObjectMetadata.labelSingular,
|
|
description: `Attachment ${createdObjectMetadata.labelSingular}`,
|
|
icon: 'IconBuildingSkyscraper',
|
|
isNullable: true,
|
|
},
|
|
// Foreign key
|
|
{
|
|
standardId: createForeignKeyDeterministicUuid({
|
|
objectId: createdObjectMetadata.id,
|
|
standardId: attachmentStandardFieldIds.custom,
|
|
}),
|
|
objectMetadataId: attachmentObjectMetadata.id,
|
|
workspaceId: workspaceId,
|
|
isCustom: false,
|
|
isActive: true,
|
|
type: FieldMetadataType.UUID,
|
|
name: `${createdObjectMetadata.nameSingular}Id`,
|
|
label: `${createdObjectMetadata.labelSingular} ID (foreign key)`,
|
|
description: `Attachment ${createdObjectMetadata.labelSingular} id foreign key`,
|
|
icon: undefined,
|
|
isNullable: true,
|
|
isSystem: true,
|
|
defaultValue: undefined,
|
|
},
|
|
]);
|
|
|
|
const attachmentRelationFieldMetadataMap =
|
|
attachmentRelationFieldMetadata.reduce(
|
|
(acc, fieldMetadata: FieldMetadataEntity) => {
|
|
if (fieldMetadata.type === FieldMetadataType.RELATION) {
|
|
acc[fieldMetadata.objectMetadataId] = fieldMetadata;
|
|
}
|
|
|
|
return acc;
|
|
},
|
|
{},
|
|
);
|
|
|
|
await this.relationMetadataRepository.save([
|
|
{
|
|
workspaceId: workspaceId,
|
|
relationType: RelationMetadataType.ONE_TO_MANY,
|
|
fromObjectMetadataId: createdObjectMetadata.id,
|
|
toObjectMetadataId: attachmentObjectMetadata.id,
|
|
fromFieldMetadataId:
|
|
attachmentRelationFieldMetadataMap[createdObjectMetadata.id].id,
|
|
toFieldMetadataId:
|
|
attachmentRelationFieldMetadataMap[attachmentObjectMetadata.id].id,
|
|
onDeleteAction: RelationOnDeleteAction.CASCADE,
|
|
},
|
|
]);
|
|
|
|
return { attachmentObjectMetadata };
|
|
}
|
|
|
|
private async createEventRelation(
|
|
workspaceId: string,
|
|
createdObjectMetadata: ObjectMetadataEntity,
|
|
) {
|
|
const eventObjectMetadata =
|
|
await this.objectMetadataRepository.findOneByOrFail({
|
|
nameSingular: 'event',
|
|
workspaceId: workspaceId,
|
|
});
|
|
|
|
const eventRelationFieldMetadata = await this.fieldMetadataRepository.save([
|
|
// FROM
|
|
{
|
|
standardId: customObjectStandardFieldIds.events,
|
|
objectMetadataId: createdObjectMetadata.id,
|
|
workspaceId: workspaceId,
|
|
isCustom: false,
|
|
isActive: true,
|
|
type: FieldMetadataType.RELATION,
|
|
name: 'events',
|
|
label: 'Events',
|
|
description: `Events tied to the ${createdObjectMetadata.labelSingular}`,
|
|
icon: 'IconFileImport',
|
|
isNullable: true,
|
|
},
|
|
// TO
|
|
{
|
|
standardId: createRelationDeterministicUuid({
|
|
objectId: createdObjectMetadata.id,
|
|
standardId: eventStandardFieldIds.custom,
|
|
}),
|
|
objectMetadataId: eventObjectMetadata.id,
|
|
workspaceId: workspaceId,
|
|
isCustom: false,
|
|
isActive: true,
|
|
type: FieldMetadataType.RELATION,
|
|
name: createdObjectMetadata.nameSingular,
|
|
label: createdObjectMetadata.labelSingular,
|
|
description: `Event ${createdObjectMetadata.labelSingular}`,
|
|
icon: 'IconBuildingSkyscraper',
|
|
isNullable: true,
|
|
},
|
|
// Foreign key
|
|
{
|
|
standardId: createForeignKeyDeterministicUuid({
|
|
objectId: createdObjectMetadata.id,
|
|
standardId: eventStandardFieldIds.custom,
|
|
}),
|
|
objectMetadataId: eventObjectMetadata.id,
|
|
workspaceId: workspaceId,
|
|
isCustom: false,
|
|
isActive: true,
|
|
type: FieldMetadataType.UUID,
|
|
name: `${createdObjectMetadata.nameSingular}Id`,
|
|
label: `${createdObjectMetadata.labelSingular} ID (foreign key)`,
|
|
description: `Event ${createdObjectMetadata.labelSingular} id foreign key`,
|
|
icon: undefined,
|
|
isNullable: true,
|
|
isSystem: true,
|
|
defaultValue: undefined,
|
|
},
|
|
]);
|
|
|
|
const eventRelationFieldMetadataMap = eventRelationFieldMetadata.reduce(
|
|
(acc, fieldMetadata: FieldMetadataEntity) => {
|
|
if (fieldMetadata.type === FieldMetadataType.RELATION) {
|
|
acc[fieldMetadata.objectMetadataId] = fieldMetadata;
|
|
}
|
|
|
|
return acc;
|
|
},
|
|
{},
|
|
);
|
|
|
|
await this.relationMetadataRepository.save([
|
|
{
|
|
workspaceId: workspaceId,
|
|
relationType: RelationMetadataType.ONE_TO_MANY,
|
|
fromObjectMetadataId: createdObjectMetadata.id,
|
|
toObjectMetadataId: eventObjectMetadata.id,
|
|
fromFieldMetadataId:
|
|
eventRelationFieldMetadataMap[createdObjectMetadata.id].id,
|
|
toFieldMetadataId:
|
|
eventRelationFieldMetadataMap[eventObjectMetadata.id].id,
|
|
onDeleteAction: RelationOnDeleteAction.CASCADE,
|
|
},
|
|
]);
|
|
|
|
return { eventObjectMetadata };
|
|
}
|
|
|
|
private async createFavoriteRelation(
|
|
workspaceId: string,
|
|
createdObjectMetadata: ObjectMetadataEntity,
|
|
) {
|
|
const favoriteObjectMetadata =
|
|
await this.objectMetadataRepository.findOneByOrFail({
|
|
nameSingular: 'favorite',
|
|
workspaceId: workspaceId,
|
|
});
|
|
|
|
const favoriteRelationFieldMetadata =
|
|
await this.fieldMetadataRepository.save([
|
|
// FROM
|
|
{
|
|
standardId: customObjectStandardFieldIds.favorites,
|
|
objectMetadataId: createdObjectMetadata.id,
|
|
workspaceId: workspaceId,
|
|
isCustom: false,
|
|
isActive: true,
|
|
isSystem: true,
|
|
type: FieldMetadataType.RELATION,
|
|
name: 'favorites',
|
|
label: 'Favorites',
|
|
description: `Favorites tied to the ${createdObjectMetadata.labelSingular}`,
|
|
icon: 'IconHeart',
|
|
isNullable: true,
|
|
},
|
|
// TO
|
|
{
|
|
standardId: createRelationDeterministicUuid({
|
|
objectId: createdObjectMetadata.id,
|
|
standardId: favoriteStandardFieldIds.custom,
|
|
}),
|
|
objectMetadataId: favoriteObjectMetadata.id,
|
|
workspaceId: workspaceId,
|
|
isCustom: false,
|
|
isActive: true,
|
|
type: FieldMetadataType.RELATION,
|
|
name: createdObjectMetadata.nameSingular,
|
|
label: createdObjectMetadata.labelSingular,
|
|
description: `Favorite ${createdObjectMetadata.labelSingular}`,
|
|
icon: 'IconBuildingSkyscraper',
|
|
isNullable: true,
|
|
},
|
|
// Foreign key
|
|
{
|
|
standardId: createForeignKeyDeterministicUuid({
|
|
objectId: createdObjectMetadata.id,
|
|
standardId: favoriteStandardFieldIds.custom,
|
|
}),
|
|
objectMetadataId: favoriteObjectMetadata.id,
|
|
workspaceId: workspaceId,
|
|
isCustom: false,
|
|
isActive: true,
|
|
type: FieldMetadataType.UUID,
|
|
name: `${createdObjectMetadata.nameSingular}Id`,
|
|
label: `${createdObjectMetadata.labelSingular} ID (foreign key)`,
|
|
description: `Favorite ${createdObjectMetadata.labelSingular} id foreign key`,
|
|
icon: undefined,
|
|
isNullable: true,
|
|
isSystem: true,
|
|
defaultValue: undefined,
|
|
},
|
|
]);
|
|
|
|
const favoriteRelationFieldMetadataMap =
|
|
favoriteRelationFieldMetadata.reduce(
|
|
(acc, fieldMetadata: FieldMetadataEntity) => {
|
|
if (fieldMetadata.type === FieldMetadataType.RELATION) {
|
|
acc[fieldMetadata.objectMetadataId] = fieldMetadata;
|
|
}
|
|
|
|
return acc;
|
|
},
|
|
{},
|
|
);
|
|
|
|
await this.relationMetadataRepository.save([
|
|
{
|
|
workspaceId: workspaceId,
|
|
relationType: RelationMetadataType.ONE_TO_MANY,
|
|
fromObjectMetadataId: createdObjectMetadata.id,
|
|
toObjectMetadataId: favoriteObjectMetadata.id,
|
|
fromFieldMetadataId:
|
|
favoriteRelationFieldMetadataMap[createdObjectMetadata.id].id,
|
|
toFieldMetadataId:
|
|
favoriteRelationFieldMetadataMap[favoriteObjectMetadata.id].id,
|
|
onDeleteAction: RelationOnDeleteAction.CASCADE,
|
|
},
|
|
]);
|
|
|
|
return { favoriteObjectMetadata };
|
|
}
|
|
|
|
private async isRelationEnabledForRemoteObjects(workspaceId: string) {
|
|
const featureFlag = await this.featureFlagRepository.findOneBy({
|
|
workspaceId,
|
|
key: FeatureFlagKeys.IsRelationForRemoteObjectsEnabled,
|
|
value: true,
|
|
});
|
|
|
|
return featureFlag && featureFlag.value;
|
|
}
|
|
}
|