414f2ac498
This PR was created by [GitStart](https://gitstart.com/) to address the requirements from this ticket: [TWNTY-5491](https://clients.gitstart.com/twenty/5449/tickets/TWNTY-5491). This ticket was imported from: [TWNTY-5491](https://github.com/twentyhq/twenty/issues/5491) --- ### Description **How To Test:**\ 1. Reset db using `npx nx database:reset twenty-server` on this PR 1. Run both backend and frontend 2. Navigate to `settings/data-model/objects/ `page 3. Select a `Custom `object from the list or create a new `Custom `object 4. Navigate to custom object details page and click on edit button 5. Finally edit the object details. **Issues and bugs** The Typecheck is failing but we could not see this error locally There is a bug after updating the label of a custom object. View title is not updated till refreshing the page. We could not find a consistent way to update this, should we reload the page after editing an object? ### Demo <https://www.loom.com/share/64ecb57efad7498d99085cb11480b5dd?sid=28d0868c-e54f-454d-8432-3f789be9e2b7> ### Refs #5491 --------- Co-authored-by: gitstart-twenty <gitstart-twenty@users.noreply.github.com> Co-authored-by: gitstart-twenty <140154534+gitstart-twenty@users.noreply.github.com> Co-authored-by: Marie Stoppa <marie.stoppa@essec.edu> Co-authored-by: Charles Bochet <charles@twenty.com> Co-authored-by: Weiko <corentin@twenty.com>
1645 lines
55 KiB
TypeScript
1645 lines
55 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
|
import { InjectRepository } from '@nestjs/typeorm';
|
|
|
|
import console from 'console';
|
|
|
|
import { Query, QueryOptions } from '@ptc-org/nestjs-query-core';
|
|
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
|
import { isDefined } from 'class-validator';
|
|
import { FindManyOptions, FindOneOptions, In, Not, Repository } from 'typeorm';
|
|
|
|
import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
|
|
|
|
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
|
|
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
|
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
|
import {
|
|
FieldMetadataEntity,
|
|
FieldMetadataType,
|
|
} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
|
import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
|
|
import { DeleteOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/delete-object.input';
|
|
import { UpdateOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input';
|
|
import {
|
|
ObjectMetadataException,
|
|
ObjectMetadataExceptionCode,
|
|
} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
|
|
import { buildMigrationsForCustomObjectRelations } from 'src/engine/metadata-modules/object-metadata/utils/build-migrations-for-custom-object-relations.util';
|
|
import { validateObjectMetadataInputOrThrow } from 'src/engine/metadata-modules/object-metadata/utils/validate-object-metadata-input.util';
|
|
import { validateNameAndLabelAreSyncOrThrow } from 'src/engine/metadata-modules/object-metadata/utils/validate-object-metadata-sync-label-name.util';
|
|
import {
|
|
RelationMetadataEntity,
|
|
RelationMetadataType,
|
|
RelationOnDeleteAction,
|
|
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
|
import { RelationToDelete } from 'src/engine/metadata-modules/relation-metadata/types/relation-to-delete';
|
|
import { RemoteTableRelationsService } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/remote-table-relations.service';
|
|
import { mapUdtNameToFieldType } from 'src/engine/metadata-modules/remote-server/remote-table/utils/udt-name-mapper.util';
|
|
import { SearchService } from 'src/engine/metadata-modules/search/search.service';
|
|
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
|
|
import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util';
|
|
import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
|
|
import {
|
|
WorkspaceMigrationColumnActionType,
|
|
WorkspaceMigrationColumnDrop,
|
|
WorkspaceMigrationTableAction,
|
|
WorkspaceMigrationTableActionType,
|
|
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
|
|
import { WorkspaceMigrationFactory } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.factory';
|
|
import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service';
|
|
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
|
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
|
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
|
|
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service';
|
|
import {
|
|
ACTIVITY_TARGET_STANDARD_FIELD_IDS,
|
|
ATTACHMENT_STANDARD_FIELD_IDS,
|
|
BASE_OBJECT_STANDARD_FIELD_IDS,
|
|
CUSTOM_OBJECT_STANDARD_FIELD_IDS,
|
|
FAVORITE_STANDARD_FIELD_IDS,
|
|
NOTE_TARGET_STANDARD_FIELD_IDS,
|
|
TASK_TARGET_STANDARD_FIELD_IDS,
|
|
TIMELINE_ACTIVITY_STANDARD_FIELD_IDS,
|
|
} 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 { isSearchableFieldType } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/is-searchable-field.util';
|
|
import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity';
|
|
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-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 remoteTableRelationsService: RemoteTableRelationsService,
|
|
|
|
private readonly dataSourceService: DataSourceService,
|
|
private readonly typeORMService: TypeORMService,
|
|
private readonly workspaceMigrationService: WorkspaceMigrationService,
|
|
private readonly featureFlagService: FeatureFlagService,
|
|
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
|
|
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
|
|
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
|
private readonly searchService: SearchService,
|
|
private readonly workspaceMigrationFactory: WorkspaceMigrationFactory,
|
|
) {
|
|
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 ObjectMetadataException(
|
|
'Object does not exist',
|
|
ObjectMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
|
|
);
|
|
}
|
|
|
|
// DELETE RELATIONS
|
|
if (objectMetadata.isRemote) {
|
|
await this.remoteTableRelationsService.deleteForeignKeysMetadataAndCreateMigrations(
|
|
objectMetadata.workspaceId,
|
|
objectMetadata,
|
|
);
|
|
} else {
|
|
await this.deleteAllRelationsAndDropTable(objectMetadata, workspaceId);
|
|
}
|
|
|
|
// DELETE VIEWS
|
|
const viewRepository =
|
|
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewWorkspaceEntity>(
|
|
workspaceId,
|
|
'view',
|
|
);
|
|
|
|
const views = await viewRepository.find({
|
|
where: {
|
|
objectMetadataId: objectMetadata.id,
|
|
},
|
|
});
|
|
|
|
if (views.length > 0) {
|
|
await viewRepository.delete(views.map((view) => view.id));
|
|
}
|
|
|
|
// DELETE OBJECT
|
|
await this.objectMetadataRepository.delete(objectMetadata.id);
|
|
|
|
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
|
|
workspaceId,
|
|
);
|
|
|
|
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
|
workspaceId,
|
|
);
|
|
|
|
return objectMetadata;
|
|
}
|
|
|
|
override async createOne(
|
|
objectMetadataInput: CreateObjectInput,
|
|
): Promise<ObjectMetadataEntity> {
|
|
const lastDataSourceMetadata =
|
|
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
|
|
objectMetadataInput.workspaceId,
|
|
);
|
|
|
|
validateObjectMetadataInputOrThrow(objectMetadataInput);
|
|
|
|
if (
|
|
objectMetadataInput.nameSingular.toLowerCase() ===
|
|
objectMetadataInput.namePlural.toLowerCase()
|
|
) {
|
|
throw new ObjectMetadataException(
|
|
'The singular and plural name cannot be the same for an object',
|
|
ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
|
|
);
|
|
}
|
|
|
|
if (objectMetadataInput.shouldSyncLabelAndName === true) {
|
|
validateNameAndLabelAreSyncOrThrow(
|
|
objectMetadataInput.labelSingular,
|
|
objectMetadataInput.nameSingular,
|
|
);
|
|
validateNameAndLabelAreSyncOrThrow(
|
|
objectMetadataInput.labelPlural,
|
|
objectMetadataInput.namePlural,
|
|
);
|
|
}
|
|
|
|
this.validatesNoOtherObjectWithSameNameExistsOrThrows({
|
|
objectMetadataNamePlural: objectMetadataInput.namePlural,
|
|
objectMetadataNameSingular: objectMetadataInput.nameSingular,
|
|
workspaceId: objectMetadataInput.workspaceId,
|
|
});
|
|
|
|
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: BASE_OBJECT_STANDARD_FIELD_IDS.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: CUSTOM_OBJECT_STANDARD_FIELD_IDS.name,
|
|
type: FieldMetadataType.TEXT,
|
|
name: 'name',
|
|
label: 'Name',
|
|
icon: 'IconAbc',
|
|
description: 'Name',
|
|
isNullable: false,
|
|
isActive: true,
|
|
isCustom: false,
|
|
workspaceId: objectMetadataInput.workspaceId,
|
|
defaultValue: "'Untitled'",
|
|
},
|
|
{
|
|
standardId: BASE_OBJECT_STANDARD_FIELD_IDS.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: BASE_OBJECT_STANDARD_FIELD_IDS.updatedAt,
|
|
type: FieldMetadataType.DATE_TIME,
|
|
name: 'updatedAt',
|
|
label: 'Last update',
|
|
icon: 'IconCalendarClock',
|
|
description: 'Last time the record was changed',
|
|
isNullable: false,
|
|
isActive: true,
|
|
isCustom: false,
|
|
isSystem: false,
|
|
workspaceId: objectMetadataInput.workspaceId,
|
|
defaultValue: 'now',
|
|
},
|
|
{
|
|
standardId: BASE_OBJECT_STANDARD_FIELD_IDS.deletedAt,
|
|
type: FieldMetadataType.DATE_TIME,
|
|
name: 'deletedAt',
|
|
label: 'Deleted at',
|
|
icon: 'IconCalendarClock',
|
|
description: 'Deletion date',
|
|
isNullable: true,
|
|
isActive: true,
|
|
isCustom: false,
|
|
isSystem: false,
|
|
workspaceId: objectMetadataInput.workspaceId,
|
|
defaultValue: null,
|
|
},
|
|
{
|
|
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.createdBy,
|
|
type: FieldMetadataType.ACTOR,
|
|
name: 'createdBy',
|
|
label: 'Created by',
|
|
icon: 'IconCreativeCommonsSa',
|
|
description: 'The creator of the record',
|
|
isNullable: false,
|
|
isActive: true,
|
|
isCustom: false,
|
|
isSystem: false,
|
|
workspaceId: objectMetadataInput.workspaceId,
|
|
defaultValue: { name: "''", source: "'MANUAL'" },
|
|
},
|
|
{
|
|
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.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);
|
|
|
|
if (isCustom) {
|
|
await this.createObjectRelationsMetadataAndMigrations(
|
|
objectMetadataInput,
|
|
createdObjectMetadata,
|
|
);
|
|
|
|
await this.searchService.createSearchVectorFieldForObject(
|
|
objectMetadataInput,
|
|
createdObjectMetadata,
|
|
);
|
|
} else {
|
|
await this.remoteTableRelationsService.createForeignKeysMetadataAndMigrations(
|
|
objectMetadataInput.workspaceId,
|
|
createdObjectMetadata,
|
|
objectMetadataInput.primaryKeyFieldMetadataSettings,
|
|
objectMetadataInput.primaryKeyColumnType,
|
|
);
|
|
}
|
|
|
|
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' || field.name === 'deletedAt') {
|
|
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 *`,
|
|
);
|
|
});
|
|
|
|
await this.createViewWorkspaceFavorite(
|
|
objectMetadataInput.workspaceId,
|
|
view[0].id,
|
|
);
|
|
|
|
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
|
objectMetadataInput.workspaceId,
|
|
);
|
|
|
|
return createdObjectMetadata;
|
|
}
|
|
|
|
public async updateOneObject(
|
|
input: UpdateOneObjectInput,
|
|
workspaceId: string,
|
|
): Promise<ObjectMetadataEntity> {
|
|
validateObjectMetadataInputOrThrow(input.update);
|
|
|
|
const existingObjectMetadata = await this.objectMetadataRepository.findOne({
|
|
where: { id: input.id, workspaceId: workspaceId },
|
|
});
|
|
|
|
if (!existingObjectMetadata) {
|
|
throw new ObjectMetadataException(
|
|
'Object does not exist',
|
|
ObjectMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
|
|
);
|
|
}
|
|
|
|
const fullObjectMetadataAfterUpdate = {
|
|
...existingObjectMetadata,
|
|
...input.update,
|
|
};
|
|
|
|
await this.validatesNoOtherObjectWithSameNameExistsOrThrows({
|
|
objectMetadataNameSingular: fullObjectMetadataAfterUpdate.nameSingular,
|
|
objectMetadataNamePlural: fullObjectMetadataAfterUpdate.namePlural,
|
|
workspaceId: workspaceId,
|
|
existingObjectMetadataId: fullObjectMetadataAfterUpdate.id,
|
|
});
|
|
|
|
if (fullObjectMetadataAfterUpdate.shouldSyncLabelAndName) {
|
|
validateNameAndLabelAreSyncOrThrow(
|
|
fullObjectMetadataAfterUpdate.labelSingular,
|
|
fullObjectMetadataAfterUpdate.nameSingular,
|
|
);
|
|
validateNameAndLabelAreSyncOrThrow(
|
|
fullObjectMetadataAfterUpdate.labelPlural,
|
|
fullObjectMetadataAfterUpdate.namePlural,
|
|
);
|
|
}
|
|
|
|
const updatedObject = await super.updateOne(input.id, input.update);
|
|
|
|
await this.handleObjectNameAndLabelUpdates(
|
|
existingObjectMetadata,
|
|
fullObjectMetadataAfterUpdate,
|
|
input,
|
|
);
|
|
|
|
if (input.update.isActive !== undefined) {
|
|
await this.updateObjectRelationships(input.id, input.update.isActive);
|
|
}
|
|
|
|
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
|
|
workspaceId,
|
|
);
|
|
if (input.update.labelIdentifierFieldMetadataId) {
|
|
const labelIdentifierFieldMetadata =
|
|
await this.fieldMetadataRepository.findOneByOrFail({
|
|
id: input.update.labelIdentifierFieldMetadataId,
|
|
objectMetadataId: input.id,
|
|
workspaceId: workspaceId,
|
|
});
|
|
|
|
if (isSearchableFieldType(labelIdentifierFieldMetadata.type)) {
|
|
await this.searchService.updateSearchVector(
|
|
input.id,
|
|
[
|
|
{
|
|
name: labelIdentifierFieldMetadata.name,
|
|
type: labelIdentifierFieldMetadata.type,
|
|
},
|
|
],
|
|
workspaceId,
|
|
);
|
|
}
|
|
|
|
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
|
|
workspaceId,
|
|
);
|
|
}
|
|
|
|
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
|
workspaceId,
|
|
);
|
|
|
|
return updatedObject;
|
|
}
|
|
|
|
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 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 });
|
|
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
|
workspaceId,
|
|
);
|
|
}
|
|
|
|
private async createObjectRelationsMetadataAndMigrations(
|
|
objectMetadataInput: CreateObjectInput,
|
|
createdObjectMetadata: ObjectMetadataEntity,
|
|
) {
|
|
const { timelineActivityObjectMetadata } =
|
|
await this.createTimelineActivityRelation(
|
|
objectMetadataInput.workspaceId,
|
|
createdObjectMetadata,
|
|
mapUdtNameToFieldType(
|
|
objectMetadataInput.primaryKeyColumnType ?? 'uuid',
|
|
),
|
|
objectMetadataInput.primaryKeyFieldMetadataSettings,
|
|
);
|
|
|
|
const { activityTargetObjectMetadata } =
|
|
await this.createActivityTargetRelation(
|
|
objectMetadataInput.workspaceId,
|
|
createdObjectMetadata,
|
|
mapUdtNameToFieldType(
|
|
objectMetadataInput.primaryKeyColumnType ?? 'uuid',
|
|
),
|
|
objectMetadataInput.primaryKeyFieldMetadataSettings,
|
|
);
|
|
|
|
const { favoriteObjectMetadata } = await this.createFavoriteRelation(
|
|
objectMetadataInput.workspaceId,
|
|
createdObjectMetadata,
|
|
mapUdtNameToFieldType(objectMetadataInput.primaryKeyColumnType ?? 'uuid'),
|
|
objectMetadataInput.primaryKeyFieldMetadataSettings,
|
|
);
|
|
|
|
const { attachmentObjectMetadata } = await this.createAttachmentRelation(
|
|
objectMetadataInput.workspaceId,
|
|
createdObjectMetadata,
|
|
mapUdtNameToFieldType(objectMetadataInput.primaryKeyColumnType ?? 'uuid'),
|
|
objectMetadataInput.primaryKeyFieldMetadataSettings,
|
|
);
|
|
|
|
const { noteTargetObjectMetadata } = await this.createNoteTargetRelation(
|
|
objectMetadataInput.workspaceId,
|
|
createdObjectMetadata,
|
|
mapUdtNameToFieldType(objectMetadataInput.primaryKeyColumnType ?? 'uuid'),
|
|
objectMetadataInput.primaryKeyFieldMetadataSettings,
|
|
);
|
|
|
|
const { taskTargetObjectMetadata } = await this.createTaskTargetRelation(
|
|
objectMetadataInput.workspaceId,
|
|
createdObjectMetadata,
|
|
mapUdtNameToFieldType(objectMetadataInput.primaryKeyColumnType ?? 'uuid'),
|
|
objectMetadataInput.primaryKeyFieldMetadataSettings,
|
|
);
|
|
|
|
await this.workspaceMigrationService.createCustomMigration(
|
|
generateMigrationName(`create-${createdObjectMetadata.nameSingular}`),
|
|
createdObjectMetadata.workspaceId,
|
|
[
|
|
{
|
|
name: computeObjectTargetTable(createdObjectMetadata),
|
|
action: WorkspaceMigrationTableActionType.CREATE,
|
|
} satisfies WorkspaceMigrationTableAction,
|
|
],
|
|
);
|
|
|
|
for (const fieldMetadata of createdObjectMetadata.fields) {
|
|
await this.workspaceMigrationService.createCustomMigration(
|
|
generateMigrationName(`create-${fieldMetadata.name}`),
|
|
createdObjectMetadata.workspaceId,
|
|
[
|
|
{
|
|
name: computeObjectTargetTable(createdObjectMetadata),
|
|
action: WorkspaceMigrationTableActionType.ALTER,
|
|
columns: this.workspaceMigrationFactory.createColumnActions(
|
|
WorkspaceMigrationColumnActionType.CREATE,
|
|
fieldMetadata,
|
|
),
|
|
},
|
|
] satisfies WorkspaceMigrationTableAction[],
|
|
);
|
|
}
|
|
|
|
await this.workspaceMigrationService.createCustomMigration(
|
|
generateMigrationName(
|
|
`create-${createdObjectMetadata.nameSingular}-relations`,
|
|
),
|
|
createdObjectMetadata.workspaceId,
|
|
buildMigrationsForCustomObjectRelations(
|
|
createdObjectMetadata,
|
|
activityTargetObjectMetadata,
|
|
attachmentObjectMetadata,
|
|
timelineActivityObjectMetadata,
|
|
favoriteObjectMetadata,
|
|
noteTargetObjectMetadata,
|
|
taskTargetObjectMetadata,
|
|
),
|
|
);
|
|
}
|
|
|
|
private async createActivityTargetRelation(
|
|
workspaceId: string,
|
|
createdObjectMetadata: ObjectMetadataEntity,
|
|
objectPrimaryKeyType: FieldMetadataType,
|
|
objectPrimaryKeyFieldSettings:
|
|
| FieldMetadataSettings<FieldMetadataType | 'default'>
|
|
| undefined,
|
|
) {
|
|
const activityTargetObjectMetadata =
|
|
await this.objectMetadataRepository.findOneByOrFail({
|
|
nameSingular: 'activityTarget',
|
|
workspaceId: workspaceId,
|
|
});
|
|
|
|
await this.fieldMetadataRepository.save(
|
|
// Foreign key
|
|
{
|
|
standardId: createForeignKeyDeterministicUuid({
|
|
objectId: createdObjectMetadata.id,
|
|
standardId: ACTIVITY_TARGET_STANDARD_FIELD_IDS.custom,
|
|
}),
|
|
objectMetadataId: activityTargetObjectMetadata.id,
|
|
workspaceId: workspaceId,
|
|
isCustom: false,
|
|
isActive: true,
|
|
type: objectPrimaryKeyType,
|
|
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,
|
|
settings: { ...objectPrimaryKeyFieldSettings, isForeignKey: true },
|
|
},
|
|
);
|
|
|
|
const activityTargetRelationFieldMetadata =
|
|
await this.fieldMetadataRepository.save([
|
|
// FROM
|
|
{
|
|
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.activityTargets,
|
|
objectMetadataId: createdObjectMetadata.id,
|
|
workspaceId: workspaceId,
|
|
isCustom: false,
|
|
isActive: true,
|
|
isSystem: 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: ACTIVITY_TARGET_STANDARD_FIELD_IDS.custom,
|
|
}),
|
|
objectMetadataId: activityTargetObjectMetadata.id,
|
|
workspaceId: workspaceId,
|
|
isCustom: false,
|
|
isActive: true,
|
|
isSystem: true,
|
|
type: FieldMetadataType.RELATION,
|
|
name: createdObjectMetadata.nameSingular,
|
|
label: createdObjectMetadata.labelSingular,
|
|
description: `ActivityTarget ${createdObjectMetadata.labelSingular}`,
|
|
icon: 'IconBuildingSkyscraper',
|
|
isNullable: true,
|
|
},
|
|
]);
|
|
|
|
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 createNoteTargetRelation(
|
|
workspaceId: string,
|
|
createdObjectMetadata: ObjectMetadataEntity,
|
|
objectPrimaryKeyType: FieldMetadataType,
|
|
objectPrimaryKeyFieldSettings:
|
|
| FieldMetadataSettings<FieldMetadataType | 'default'>
|
|
| undefined,
|
|
) {
|
|
const noteTargetObjectMetadata =
|
|
await this.objectMetadataRepository.findOneByOrFail({
|
|
nameSingular: 'noteTarget',
|
|
workspaceId: workspaceId,
|
|
});
|
|
|
|
await this.fieldMetadataRepository.save(
|
|
// Foreign key
|
|
{
|
|
standardId: createForeignKeyDeterministicUuid({
|
|
objectId: createdObjectMetadata.id,
|
|
standardId: NOTE_TARGET_STANDARD_FIELD_IDS.custom,
|
|
}),
|
|
objectMetadataId: noteTargetObjectMetadata.id,
|
|
workspaceId: workspaceId,
|
|
isCustom: false,
|
|
isActive: true,
|
|
type: objectPrimaryKeyType,
|
|
name: `${createdObjectMetadata.nameSingular}Id`,
|
|
label: `${createdObjectMetadata.labelSingular} ID (foreign key)`,
|
|
description: `NoteTarget ${createdObjectMetadata.labelSingular} id foreign key`,
|
|
icon: undefined,
|
|
isNullable: true,
|
|
isSystem: true,
|
|
defaultValue: undefined,
|
|
settings: { ...objectPrimaryKeyFieldSettings, isForeignKey: true },
|
|
},
|
|
);
|
|
|
|
const noteTargetRelationFieldMetadata =
|
|
await this.fieldMetadataRepository.save([
|
|
// FROM
|
|
{
|
|
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.noteTargets,
|
|
objectMetadataId: createdObjectMetadata.id,
|
|
workspaceId: workspaceId,
|
|
isCustom: false,
|
|
isActive: true,
|
|
type: FieldMetadataType.RELATION,
|
|
name: 'noteTargets',
|
|
label: 'Notes',
|
|
description: `Notes tied to the ${createdObjectMetadata.labelSingular}`,
|
|
icon: 'IconNotes',
|
|
isNullable: true,
|
|
},
|
|
// TO
|
|
{
|
|
standardId: createRelationDeterministicUuid({
|
|
objectId: createdObjectMetadata.id,
|
|
standardId: NOTE_TARGET_STANDARD_FIELD_IDS.custom,
|
|
}),
|
|
objectMetadataId: noteTargetObjectMetadata.id,
|
|
workspaceId: workspaceId,
|
|
isCustom: false,
|
|
isActive: true,
|
|
type: FieldMetadataType.RELATION,
|
|
name: createdObjectMetadata.nameSingular,
|
|
label: createdObjectMetadata.labelSingular,
|
|
description: `NoteTarget ${createdObjectMetadata.labelSingular}`,
|
|
icon: 'IconBuildingSkyscraper',
|
|
isNullable: true,
|
|
},
|
|
]);
|
|
|
|
const noteTargetRelationFieldMetadataMap =
|
|
noteTargetRelationFieldMetadata.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: noteTargetObjectMetadata.id,
|
|
fromFieldMetadataId:
|
|
noteTargetRelationFieldMetadataMap[createdObjectMetadata.id].id,
|
|
toFieldMetadataId:
|
|
noteTargetRelationFieldMetadataMap[noteTargetObjectMetadata.id].id,
|
|
onDeleteAction: RelationOnDeleteAction.CASCADE,
|
|
},
|
|
]);
|
|
|
|
return { noteTargetObjectMetadata };
|
|
}
|
|
|
|
private async createTaskTargetRelation(
|
|
workspaceId: string,
|
|
createdObjectMetadata: ObjectMetadataEntity,
|
|
objectPrimaryKeyType: FieldMetadataType,
|
|
objectPrimaryKeyFieldSettings:
|
|
| FieldMetadataSettings<FieldMetadataType | 'default'>
|
|
| undefined,
|
|
) {
|
|
const taskTargetObjectMetadata =
|
|
await this.objectMetadataRepository.findOneByOrFail({
|
|
nameSingular: 'taskTarget',
|
|
workspaceId: workspaceId,
|
|
});
|
|
|
|
await this.fieldMetadataRepository.save(
|
|
// Foreign key
|
|
{
|
|
standardId: createForeignKeyDeterministicUuid({
|
|
objectId: createdObjectMetadata.id,
|
|
standardId: TASK_TARGET_STANDARD_FIELD_IDS.custom,
|
|
}),
|
|
objectMetadataId: taskTargetObjectMetadata.id,
|
|
workspaceId: workspaceId,
|
|
isCustom: false,
|
|
isActive: true,
|
|
type: objectPrimaryKeyType,
|
|
name: `${createdObjectMetadata.nameSingular}Id`,
|
|
label: `${createdObjectMetadata.labelSingular} ID (foreign key)`,
|
|
description: `TaskTarget ${createdObjectMetadata.labelSingular} id foreign key`,
|
|
icon: undefined,
|
|
isNullable: true,
|
|
isSystem: true,
|
|
defaultValue: undefined,
|
|
settings: { ...objectPrimaryKeyFieldSettings, isForeignKey: true },
|
|
},
|
|
);
|
|
|
|
const taskTargetRelationFieldMetadata =
|
|
await this.fieldMetadataRepository.save([
|
|
// FROM
|
|
{
|
|
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.taskTargets,
|
|
objectMetadataId: createdObjectMetadata.id,
|
|
workspaceId: workspaceId,
|
|
isCustom: false,
|
|
isActive: true,
|
|
type: FieldMetadataType.RELATION,
|
|
name: 'taskTargets',
|
|
label: 'Tasks',
|
|
description: `Tasks tied to the ${createdObjectMetadata.labelSingular}`,
|
|
icon: 'IconCheckbox',
|
|
isNullable: true,
|
|
},
|
|
// TO
|
|
{
|
|
standardId: createRelationDeterministicUuid({
|
|
objectId: createdObjectMetadata.id,
|
|
standardId: TASK_TARGET_STANDARD_FIELD_IDS.custom,
|
|
}),
|
|
objectMetadataId: taskTargetObjectMetadata.id,
|
|
workspaceId: workspaceId,
|
|
isCustom: false,
|
|
isActive: true,
|
|
type: FieldMetadataType.RELATION,
|
|
name: createdObjectMetadata.nameSingular,
|
|
label: createdObjectMetadata.labelSingular,
|
|
description: `TaskTarget ${createdObjectMetadata.labelSingular}`,
|
|
icon: 'IconBuildingSkyscraper',
|
|
isNullable: true,
|
|
},
|
|
]);
|
|
|
|
const taskTargetRelationFieldMetadataMap =
|
|
taskTargetRelationFieldMetadata.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: taskTargetObjectMetadata.id,
|
|
fromFieldMetadataId:
|
|
taskTargetRelationFieldMetadataMap[createdObjectMetadata.id].id,
|
|
toFieldMetadataId:
|
|
taskTargetRelationFieldMetadataMap[taskTargetObjectMetadata.id].id,
|
|
onDeleteAction: RelationOnDeleteAction.CASCADE,
|
|
},
|
|
]);
|
|
|
|
return { taskTargetObjectMetadata };
|
|
}
|
|
|
|
private async createAttachmentRelation(
|
|
workspaceId: string,
|
|
createdObjectMetadata: ObjectMetadataEntity,
|
|
objectPrimaryKeyType: FieldMetadataType,
|
|
objectPrimaryKeyFieldSettings:
|
|
| FieldMetadataSettings<FieldMetadataType | 'default'>
|
|
| undefined,
|
|
) {
|
|
const attachmentObjectMetadata =
|
|
await this.objectMetadataRepository.findOneByOrFail({
|
|
nameSingular: 'attachment',
|
|
workspaceId: workspaceId,
|
|
});
|
|
|
|
await this.fieldMetadataRepository.save(
|
|
// Foreign key
|
|
{
|
|
standardId: createForeignKeyDeterministicUuid({
|
|
objectId: createdObjectMetadata.id,
|
|
standardId: ATTACHMENT_STANDARD_FIELD_IDS.custom,
|
|
}),
|
|
objectMetadataId: attachmentObjectMetadata.id,
|
|
workspaceId: workspaceId,
|
|
isCustom: false,
|
|
isActive: true,
|
|
type: objectPrimaryKeyType,
|
|
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,
|
|
settings: { ...objectPrimaryKeyFieldSettings, isForeignKey: true },
|
|
},
|
|
);
|
|
|
|
const attachmentRelationFieldMetadata =
|
|
await this.fieldMetadataRepository.save([
|
|
// FROM
|
|
{
|
|
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.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: ATTACHMENT_STANDARD_FIELD_IDS.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,
|
|
},
|
|
]);
|
|
|
|
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 createTimelineActivityRelation(
|
|
workspaceId: string,
|
|
createdObjectMetadata: ObjectMetadataEntity,
|
|
objectPrimaryKeyType: FieldMetadataType,
|
|
objectPrimaryKeyFieldSettings:
|
|
| FieldMetadataSettings<FieldMetadataType | 'default'>
|
|
| undefined,
|
|
) {
|
|
const timelineActivityObjectMetadata =
|
|
await this.objectMetadataRepository.findOneByOrFail({
|
|
nameSingular: 'timelineActivity',
|
|
workspaceId: workspaceId,
|
|
});
|
|
|
|
await this.fieldMetadataRepository.save(
|
|
// Foreign key
|
|
{
|
|
standardId: createForeignKeyDeterministicUuid({
|
|
objectId: createdObjectMetadata.id,
|
|
standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.custom,
|
|
}),
|
|
objectMetadataId: timelineActivityObjectMetadata.id,
|
|
workspaceId: workspaceId,
|
|
isCustom: false,
|
|
isActive: true,
|
|
type: objectPrimaryKeyType,
|
|
name: `${createdObjectMetadata.nameSingular}Id`,
|
|
label: `${createdObjectMetadata.labelSingular} ID (foreign key)`,
|
|
description: `Timeline Activity ${createdObjectMetadata.labelSingular} id foreign key`,
|
|
icon: undefined,
|
|
isNullable: true,
|
|
isSystem: true,
|
|
defaultValue: undefined,
|
|
settings: { ...objectPrimaryKeyFieldSettings, isForeignKey: true },
|
|
},
|
|
);
|
|
|
|
const timelineActivityRelationFieldMetadata =
|
|
await this.fieldMetadataRepository.save([
|
|
// FROM
|
|
{
|
|
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.timelineActivities,
|
|
objectMetadataId: createdObjectMetadata.id,
|
|
workspaceId: workspaceId,
|
|
isCustom: false,
|
|
isActive: true,
|
|
type: FieldMetadataType.RELATION,
|
|
name: 'timelineActivities',
|
|
label: 'Timeline Activities',
|
|
description: `Timeline Activities tied to the ${createdObjectMetadata.labelSingular}`,
|
|
icon: 'IconIconTimelineEvent',
|
|
isNullable: true,
|
|
isSystem: true,
|
|
},
|
|
// TO
|
|
{
|
|
standardId: createRelationDeterministicUuid({
|
|
objectId: createdObjectMetadata.id,
|
|
standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.custom,
|
|
}),
|
|
objectMetadataId: timelineActivityObjectMetadata.id,
|
|
workspaceId: workspaceId,
|
|
isCustom: false,
|
|
isActive: true,
|
|
type: FieldMetadataType.RELATION,
|
|
name: createdObjectMetadata.nameSingular,
|
|
label: createdObjectMetadata.labelSingular,
|
|
description: `Timeline Activity ${createdObjectMetadata.labelSingular}`,
|
|
icon: 'IconTimeline',
|
|
isNullable: true,
|
|
},
|
|
]);
|
|
|
|
const timelineActivityRelationFieldMetadataMap =
|
|
timelineActivityRelationFieldMetadata.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: timelineActivityObjectMetadata.id,
|
|
fromFieldMetadataId:
|
|
timelineActivityRelationFieldMetadataMap[createdObjectMetadata.id].id,
|
|
toFieldMetadataId:
|
|
timelineActivityRelationFieldMetadataMap[
|
|
timelineActivityObjectMetadata.id
|
|
].id,
|
|
onDeleteAction: RelationOnDeleteAction.CASCADE,
|
|
},
|
|
]);
|
|
|
|
return { timelineActivityObjectMetadata };
|
|
}
|
|
|
|
private async createFavoriteRelation(
|
|
workspaceId: string,
|
|
createdObjectMetadata: ObjectMetadataEntity,
|
|
objectPrimaryKeyType: FieldMetadataType,
|
|
objectPrimaryKeyFieldSettings:
|
|
| FieldMetadataSettings<FieldMetadataType | 'default'>
|
|
| undefined,
|
|
) {
|
|
const favoriteObjectMetadata =
|
|
await this.objectMetadataRepository.findOneByOrFail({
|
|
nameSingular: 'favorite',
|
|
workspaceId: workspaceId,
|
|
});
|
|
|
|
await this.fieldMetadataRepository.save(
|
|
// Foreign key
|
|
{
|
|
standardId: createForeignKeyDeterministicUuid({
|
|
objectId: createdObjectMetadata.id,
|
|
standardId: FAVORITE_STANDARD_FIELD_IDS.custom,
|
|
}),
|
|
objectMetadataId: favoriteObjectMetadata.id,
|
|
workspaceId: workspaceId,
|
|
isCustom: false,
|
|
isActive: true,
|
|
type: objectPrimaryKeyType,
|
|
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,
|
|
settings: { ...objectPrimaryKeyFieldSettings, isForeignKey: true },
|
|
},
|
|
);
|
|
|
|
const favoriteRelationFieldMetadata =
|
|
await this.fieldMetadataRepository.save([
|
|
// FROM
|
|
{
|
|
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.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: FAVORITE_STANDARD_FIELD_IDS.custom,
|
|
}),
|
|
objectMetadataId: favoriteObjectMetadata.id,
|
|
workspaceId: workspaceId,
|
|
isCustom: false,
|
|
isActive: true,
|
|
type: FieldMetadataType.RELATION,
|
|
name: createdObjectMetadata.nameSingular,
|
|
label: createdObjectMetadata.labelSingular,
|
|
description: `Favorite ${createdObjectMetadata.labelSingular}`,
|
|
icon: 'IconHeart',
|
|
isNullable: true,
|
|
},
|
|
]);
|
|
|
|
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 deleteAllRelationsAndDropTable(
|
|
objectMetadata: ObjectMetadataEntity,
|
|
workspaceId: string,
|
|
) {
|
|
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,
|
|
],
|
|
},
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
// DROP TABLE
|
|
await this.workspaceMigrationService.createCustomMigration(
|
|
generateMigrationName(`delete-${objectMetadata.nameSingular}`),
|
|
workspaceId,
|
|
[
|
|
{
|
|
name: computeObjectTargetTable(objectMetadata),
|
|
action: WorkspaceMigrationTableActionType.DROP,
|
|
},
|
|
],
|
|
);
|
|
}
|
|
|
|
private async updateObjectRelationships(
|
|
objectMetadataId: string,
|
|
isActive: boolean,
|
|
) {
|
|
const affectedRelations = await this.relationMetadataRepository.find({
|
|
where: [
|
|
{ fromObjectMetadataId: objectMetadataId },
|
|
{ toObjectMetadataId: objectMetadataId },
|
|
],
|
|
});
|
|
|
|
const affectedFieldIds = affectedRelations.reduce(
|
|
(acc, { fromFieldMetadataId, toFieldMetadataId }) => {
|
|
acc.push(fromFieldMetadataId, toFieldMetadataId);
|
|
|
|
return acc;
|
|
},
|
|
[] as string[],
|
|
);
|
|
|
|
if (affectedFieldIds.length > 0) {
|
|
await this.fieldMetadataRepository.update(
|
|
{ id: In(affectedFieldIds) },
|
|
{ isActive: isActive },
|
|
);
|
|
}
|
|
}
|
|
|
|
private async createViewWorkspaceFavorite(
|
|
workspaceId: string,
|
|
viewId: string,
|
|
) {
|
|
const favoriteRepository =
|
|
await this.twentyORMGlobalManager.getRepositoryForWorkspace<FavoriteWorkspaceEntity>(
|
|
workspaceId,
|
|
'favorite',
|
|
);
|
|
|
|
const favoriteCount = await favoriteRepository.count();
|
|
|
|
return favoriteRepository.insert(
|
|
favoriteRepository.create({
|
|
viewId,
|
|
position: favoriteCount,
|
|
}),
|
|
);
|
|
}
|
|
|
|
private async handleObjectNameAndLabelUpdates(
|
|
existingObjectMetadata: ObjectMetadataEntity,
|
|
objectMetadataForUpdate: ObjectMetadataEntity,
|
|
input: UpdateOneObjectInput,
|
|
) {
|
|
if (
|
|
isDefined(input.update.nameSingular) ||
|
|
isDefined(input.update.namePlural)
|
|
) {
|
|
if (
|
|
objectMetadataForUpdate.nameSingular ===
|
|
objectMetadataForUpdate.namePlural
|
|
) {
|
|
throw new ObjectMetadataException(
|
|
'The singular and plural name cannot be the same for an object',
|
|
ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
|
|
);
|
|
}
|
|
}
|
|
|
|
const newTargetTableName = computeObjectTargetTable(
|
|
objectMetadataForUpdate,
|
|
);
|
|
const existingTargetTableName = computeObjectTargetTable(
|
|
existingObjectMetadata,
|
|
);
|
|
|
|
if (!(newTargetTableName === existingTargetTableName)) {
|
|
await this.createRenameTableMigration(
|
|
existingObjectMetadata,
|
|
objectMetadataForUpdate,
|
|
);
|
|
|
|
await this.createRelationsUpdatesMigrations(
|
|
existingObjectMetadata,
|
|
objectMetadataForUpdate,
|
|
);
|
|
}
|
|
|
|
if (input.update.labelPlural || input.update.icon) {
|
|
if (
|
|
!(input.update.labelPlural === existingObjectMetadata.labelPlural) ||
|
|
!(input.update.icon === existingObjectMetadata.icon)
|
|
) {
|
|
await this.updateObjectView(
|
|
objectMetadataForUpdate,
|
|
objectMetadataForUpdate.workspaceId,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
private async createRenameTableMigration(
|
|
existingObjectMetadata: ObjectMetadataEntity,
|
|
objectMetadataForUpdate: ObjectMetadataEntity,
|
|
) {
|
|
const newTargetTableName = computeObjectTargetTable(
|
|
objectMetadataForUpdate,
|
|
);
|
|
const existingTargetTableName = computeObjectTargetTable(
|
|
existingObjectMetadata,
|
|
);
|
|
|
|
this.workspaceMigrationService.createCustomMigration(
|
|
generateMigrationName(`rename-${existingObjectMetadata.nameSingular}`),
|
|
objectMetadataForUpdate.workspaceId,
|
|
[
|
|
{
|
|
name: existingTargetTableName,
|
|
newName: newTargetTableName,
|
|
action: WorkspaceMigrationTableActionType.ALTER,
|
|
},
|
|
],
|
|
);
|
|
}
|
|
|
|
private async createRelationsUpdatesMigrations(
|
|
existingObjectMetadata: ObjectMetadataEntity,
|
|
updatedObjectMetadata: ObjectMetadataEntity,
|
|
) {
|
|
const existingTableName = computeObjectTargetTable(existingObjectMetadata);
|
|
const newTableName = computeObjectTargetTable(updatedObjectMetadata);
|
|
|
|
if (existingTableName !== newTableName) {
|
|
const searchCriteria = {
|
|
isCustom: false,
|
|
settings: {
|
|
isForeignKey: true,
|
|
},
|
|
name: `${existingObjectMetadata.nameSingular}Id`,
|
|
};
|
|
|
|
const fieldsWihStandardRelation = await this.fieldMetadataRepository.find(
|
|
{
|
|
where: {
|
|
isCustom: false,
|
|
settings: {
|
|
isForeignKey: true,
|
|
},
|
|
name: `${existingObjectMetadata.nameSingular}Id`,
|
|
},
|
|
},
|
|
);
|
|
|
|
await this.fieldMetadataRepository.update(searchCriteria, {
|
|
name: `${updatedObjectMetadata.nameSingular}Id`,
|
|
});
|
|
|
|
await Promise.all(
|
|
fieldsWihStandardRelation.map(async (fieldWihStandardRelation) => {
|
|
const relatedObject = await this.objectMetadataRepository.findOneBy({
|
|
id: fieldWihStandardRelation.objectMetadataId,
|
|
workspaceId: updatedObjectMetadata.workspaceId,
|
|
});
|
|
|
|
if (relatedObject) {
|
|
await this.fieldMetadataRepository.update(
|
|
{
|
|
name: existingObjectMetadata.nameSingular,
|
|
label: existingObjectMetadata.labelSingular,
|
|
},
|
|
{
|
|
name: updatedObjectMetadata.nameSingular,
|
|
label: updatedObjectMetadata.labelSingular,
|
|
},
|
|
);
|
|
|
|
const relationTableName = computeObjectTargetTable(relatedObject);
|
|
const columnName = `${existingObjectMetadata.nameSingular}Id`;
|
|
const columnType = fieldMetadataTypeToColumnType(
|
|
fieldWihStandardRelation.type,
|
|
);
|
|
|
|
await this.workspaceMigrationService.createCustomMigration(
|
|
generateMigrationName(
|
|
`rename-${existingObjectMetadata.nameSingular}-to-${updatedObjectMetadata.nameSingular}-in-${relatedObject.nameSingular}`,
|
|
),
|
|
updatedObjectMetadata.workspaceId,
|
|
[
|
|
{
|
|
name: relationTableName,
|
|
action: WorkspaceMigrationTableActionType.ALTER,
|
|
columns: [
|
|
{
|
|
action: WorkspaceMigrationColumnActionType.ALTER,
|
|
currentColumnDefinition: {
|
|
columnName,
|
|
columnType,
|
|
isNullable: true,
|
|
defaultValue: null,
|
|
},
|
|
alteredColumnDefinition: {
|
|
columnName: `${updatedObjectMetadata.nameSingular}Id`,
|
|
columnType,
|
|
isNullable: true,
|
|
defaultValue: null,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
],
|
|
);
|
|
}
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|
|
private async updateObjectView(
|
|
updatedObjectMetadata: ObjectMetadataEntity,
|
|
workspaceId: string,
|
|
) {
|
|
const dataSourceMetadata =
|
|
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
|
|
workspaceId,
|
|
);
|
|
|
|
const workspaceDataSource =
|
|
await this.typeORMService.connectToDataSource(dataSourceMetadata);
|
|
|
|
await workspaceDataSource?.query(
|
|
`UPDATE ${dataSourceMetadata.schema}."view"
|
|
SET "name"=$1, "icon"=$2 WHERE "objectMetadataId"=$3 AND "key"=$4`,
|
|
[
|
|
`All ${updatedObjectMetadata.labelPlural}`,
|
|
updatedObjectMetadata.icon,
|
|
updatedObjectMetadata.id,
|
|
'INDEX',
|
|
],
|
|
);
|
|
}
|
|
|
|
private validatesNoOtherObjectWithSameNameExistsOrThrows = async ({
|
|
objectMetadataNameSingular,
|
|
objectMetadataNamePlural,
|
|
workspaceId,
|
|
existingObjectMetadataId,
|
|
}: {
|
|
objectMetadataNameSingular: string;
|
|
objectMetadataNamePlural: string;
|
|
workspaceId: string;
|
|
existingObjectMetadataId?: string;
|
|
}): Promise<void> => {
|
|
const baseWhereConditions = [
|
|
{ nameSingular: objectMetadataNameSingular, workspaceId },
|
|
{ nameSingular: objectMetadataNamePlural, workspaceId },
|
|
{ namePlural: objectMetadataNameSingular, workspaceId },
|
|
{ namePlural: objectMetadataNamePlural, workspaceId },
|
|
];
|
|
|
|
const whereConditions = baseWhereConditions.map((condition) => {
|
|
return {
|
|
...condition,
|
|
...(isDefined(existingObjectMetadataId)
|
|
? { id: Not(In([existingObjectMetadataId])) }
|
|
: {}),
|
|
};
|
|
});
|
|
|
|
const objectAlreadyExists = await this.objectMetadataRepository.findOne({
|
|
where: whereConditions,
|
|
});
|
|
|
|
if (objectAlreadyExists) {
|
|
throw new ObjectMetadataException(
|
|
'Object already exists',
|
|
ObjectMetadataExceptionCode.OBJECT_ALREADY_EXISTS,
|
|
);
|
|
}
|
|
};
|
|
}
|