Fix index renaming (#8771)

Fixes https://github.com/twentyhq/twenty/issues/8760

## Context
Index names are based on table names and column names, which means
renaming an object (or a field) should also trigger a recompute of index
names. As of today it raises a bug where you can't create an object with
a name that was previously used.

I also took the occasion to refactor a bit the part where we create and
update (after renaming) relations. Basically the only relations we want
to affect are standard relations so I've aligned the logic with
sync-metadata which uses standardId as a source of truth to simplify the
code.

Note: We don't create index for custom relations
Next step should be to do that and update that code to update the index
name as well. Also note that we need to update the sync-metadata logic
for that as well
This commit is contained in:
Weiko
2024-11-28 10:21:03 +01:00
committed by GitHub
parent 2fab2266d5
commit c9fd194695
5 changed files with 487 additions and 313 deletions

View File

@ -13,6 +13,7 @@ import { generateDeterministicIndexName } from 'src/engine/metadata-modules/inde
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
import {
WorkspaceMigrationIndexAction,
WorkspaceMigrationIndexActionType,
WorkspaceMigrationTableAction,
WorkspaceMigrationTableActionType,
@ -103,6 +104,55 @@ export class IndexMetadataService {
);
}
async recomputeIndexMetadataForObject(
workspaceId: string,
updatedObjectMetadata: ObjectMetadataEntity,
) {
const indexesToRecompute = await this.indexMetadataRepository.find({
where: {
objectMetadataId: updatedObjectMetadata.id,
workspaceId,
},
relations: ['indexFieldMetadatas.fieldMetadata'],
});
const recomputedIndexes: {
indexMetadata: IndexMetadataEntity;
previousName: string;
newName: string;
}[] = [];
for (const index of indexesToRecompute) {
const previousIndexName = index.name;
const tableName = computeObjectTargetTable(updatedObjectMetadata);
const indexFieldsMetadataOrdered = index.indexFieldMetadatas.sort(
(a, b) => a.order - b.order,
);
const columnNames = indexFieldsMetadataOrdered.map(
(indexFieldMetadata) => indexFieldMetadata.fieldMetadata.name,
);
const newIndexName = `IDX_${generateDeterministicIndexName([
tableName,
...columnNames,
])}`;
await this.indexMetadataRepository.update(index.id, {
name: newIndexName,
});
recomputedIndexes.push({
indexMetadata: index,
previousName: previousIndexName,
newName: newIndexName,
});
}
return recomputedIndexes;
}
async deleteIndexMetadata(
workspaceId: string,
objectMetadata: ObjectMetadataEntity,
@ -179,4 +229,55 @@ export class IndexMetadataService {
[migration],
);
}
async createIndexRecomputeMigrations(
workspaceId: string,
objectMetadata: ObjectMetadataEntity,
recomputedIndexes: {
indexMetadata: IndexMetadataEntity;
previousName: string;
newName: string;
}[],
) {
for (const recomputedIndex of recomputedIndexes) {
const { previousName, newName, indexMetadata } = recomputedIndex;
const tableName = computeObjectTargetTable(objectMetadata);
const indexFieldsMetadataOrdered = indexMetadata.indexFieldMetadatas.sort(
(a, b) => a.order - b.order,
);
const columnNames = indexFieldsMetadataOrdered.map(
(indexFieldMetadata) => indexFieldMetadata.fieldMetadata.name,
);
const migration = {
name: tableName,
action: WorkspaceMigrationTableActionType.ALTER_INDEXES,
indexes: [
{
action: WorkspaceMigrationIndexActionType.DROP,
name: previousName,
columns: [],
isUnique: indexMetadata.isUnique,
} satisfies WorkspaceMigrationIndexAction,
{
action: WorkspaceMigrationIndexActionType.CREATE,
columns: columnNames,
name: newName,
isUnique: indexMetadata.isUnique,
where: indexMetadata.indexWhereClause,
type: indexMetadata.indexType,
} satisfies WorkspaceMigrationIndexAction,
],
} satisfies WorkspaceMigrationTableAction;
await this.workspaceMigrationService.createCustomMigration(
generateMigrationName(`update-${objectMetadata.nameSingular}-index`),
workspaceId,
[migration],
);
}
}
}

View File

@ -13,6 +13,7 @@ import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { IndexMetadataModule } from 'src/engine/metadata-modules/index-metadata/index-metadata.module';
import { BeforeUpdateOneObject } from 'src/engine/metadata-modules/object-metadata/hooks/before-update-one-object.hook';
import { ObjectMetadataGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/object-metadata/interceptors/object-metadata-graphql-api-exception.interceptor';
import { ObjectMetadataResolver } from 'src/engine/metadata-modules/object-metadata/object-metadata.resolver';
@ -49,6 +50,7 @@ import { UpdateObjectPayload } from './dtos/update-object.input';
WorkspaceMetadataVersionModule,
RemoteTableRelationsModule,
SearchModule,
IndexMetadataModule,
],
services: [
ObjectMetadataService,

View File

@ -10,6 +10,7 @@ import { FindManyOptions, FindOneOptions, In, Not, Repository } from 'typeorm';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { IndexMetadataService } from 'src/engine/metadata-modules/index-metadata/index-metadata.service';
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 {
@ -26,7 +27,6 @@ import {
validateObjectMetadataInputOrThrow,
} from 'src/engine/metadata-modules/object-metadata/utils/validate-object-metadata-input.util';
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 { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
@ -55,6 +55,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
private readonly objectMetadataRelationService: ObjectMetadataRelationService,
private readonly objectMetadataMigrationService: ObjectMetadataMigrationService,
private readonly objectMetadataRelatedRecordsService: ObjectMetadataRelatedRecordsService,
private readonly indexMetadataService: IndexMetadataService,
) {
super(objectMetadataRepository);
}
@ -142,18 +143,29 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
objectMetadataInput.primaryKeyColumnType,
);
} else {
await this.objectMetadataMigrationService.createObjectMigration(
await this.objectMetadataMigrationService.createTableMigration(
createdObjectMetadata,
);
await this.objectMetadataMigrationService.createFieldMigrations(
await this.objectMetadataMigrationService.createColumnsMigrations(
createdObjectMetadata,
createdObjectMetadata.fields,
);
await this.createRelationsMetadataAndMigrations(
objectMetadataInput,
const createdRelatedObjectMetadataCollection =
await this.objectMetadataRelationService.createRelationsAndForeignKeysMetadata(
objectMetadataInput.workspaceId,
createdObjectMetadata,
{
primaryKeyFieldMetadataSettings:
objectMetadataInput.primaryKeyFieldMetadataSettings,
primaryKeyColumnType: objectMetadataInput.primaryKeyColumnType,
},
);
await this.objectMetadataMigrationService.createRelationMigrations(
createdObjectMetadata,
createdRelatedObjectMetadataCollection,
);
await this.searchService.createSearchVectorFieldForObject(
@ -194,26 +206,29 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
);
}
const fullObjectMetadataAfterUpdate = {
const existingObjectMetadataCombinedWithUpdateInput = {
...existingObjectMetadata,
...input.update,
};
await this.validatesNoOtherObjectWithSameNameExistsOrThrows({
objectMetadataNameSingular: fullObjectMetadataAfterUpdate.nameSingular,
objectMetadataNamePlural: fullObjectMetadataAfterUpdate.namePlural,
objectMetadataNameSingular:
existingObjectMetadataCombinedWithUpdateInput.nameSingular,
objectMetadataNamePlural:
existingObjectMetadataCombinedWithUpdateInput.namePlural,
workspaceId: workspaceId,
existingObjectMetadataId: fullObjectMetadataAfterUpdate.id,
existingObjectMetadataId:
existingObjectMetadataCombinedWithUpdateInput.id,
});
if (fullObjectMetadataAfterUpdate.isLabelSyncedWithName) {
if (existingObjectMetadataCombinedWithUpdateInput.isLabelSyncedWithName) {
validateNameAndLabelAreSyncOrThrow(
fullObjectMetadataAfterUpdate.labelSingular,
fullObjectMetadataAfterUpdate.nameSingular,
existingObjectMetadataCombinedWithUpdateInput.labelSingular,
existingObjectMetadataCombinedWithUpdateInput.nameSingular,
);
validateNameAndLabelAreSyncOrThrow(
fullObjectMetadataAfterUpdate.labelPlural,
fullObjectMetadataAfterUpdate.namePlural,
existingObjectMetadataCombinedWithUpdateInput.labelPlural,
existingObjectMetadataCombinedWithUpdateInput.namePlural,
);
}
@ -222,8 +237,8 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
isDefined(input.update.namePlural)
) {
validateNameSingularAndNamePluralAreDifferentOrThrow(
fullObjectMetadataAfterUpdate.nameSingular,
fullObjectMetadataAfterUpdate.namePlural,
existingObjectMetadataCombinedWithUpdateInput.nameSingular,
existingObjectMetadataCombinedWithUpdateInput.namePlural,
);
}
@ -231,12 +246,12 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
await this.handleObjectNameAndLabelUpdates(
existingObjectMetadata,
fullObjectMetadataAfterUpdate,
existingObjectMetadataCombinedWithUpdateInput,
input,
);
if (input.update.isActive !== undefined) {
await this.objectMetadataRelationService.updateObjectRelationships(
await this.objectMetadataRelationService.updateObjectRelationshipsActivationStatus(
input.id,
input.update.isActive,
);
@ -396,38 +411,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
);
}
private async createRelationsMetadataAndMigrations(
objectMetadataInput: CreateObjectInput,
createdObjectMetadata: ObjectMetadataEntity,
) {
const relatedObjectTypes = [
'timelineActivity',
'favorite',
'attachment',
'noteTarget',
'taskTarget',
];
const createdRelatedObjectMetadata = await Promise.all(
relatedObjectTypes.map(async (relationType) =>
this.objectMetadataRelationService.createMetadata(
objectMetadataInput.workspaceId,
createdObjectMetadata,
mapUdtNameToFieldType(
objectMetadataInput.primaryKeyColumnType ?? 'uuid',
),
objectMetadataInput.primaryKeyFieldMetadataSettings,
relationType,
),
),
);
await this.objectMetadataMigrationService.createRelationMigrations(
createdObjectMetadata,
createdRelatedObjectMetadata,
);
}
private async handleObjectNameAndLabelUpdates(
existingObjectMetadata: ObjectMetadataEntity,
objectMetadataForUpdate: ObjectMetadataEntity,
@ -447,17 +430,37 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
objectMetadataForUpdate.workspaceId,
);
await this.objectMetadataMigrationService.createStandardRelationsUpdatesMigrations(
const relationsAndForeignKeysMetadata =
await this.objectMetadataRelationService.updateRelationsAndForeignKeysMetadata(
objectMetadataForUpdate.workspaceId,
objectMetadataForUpdate,
);
await this.objectMetadataMigrationService.createUpdateForeignKeysMigrations(
existingObjectMetadata,
objectMetadataForUpdate,
relationsAndForeignKeysMetadata,
objectMetadataForUpdate.workspaceId,
);
}
if (input.update.labelPlural || input.update.icon) {
const recomputedIndexes =
await this.indexMetadataService.recomputeIndexMetadataForObject(
objectMetadataForUpdate.workspaceId,
objectMetadataForUpdate,
);
// TODO: recompute foreign keys indexes as well (in the related object and not objectMetadataForUpdate)
await this.indexMetadataService.createIndexRecomputeMigrations(
objectMetadataForUpdate.workspaceId,
objectMetadataForUpdate,
recomputedIndexes,
);
if (
!(input.update.labelPlural === existingObjectMetadata.labelPlural) ||
!(input.update.icon === existingObjectMetadata.icon)
(input.update.labelPlural || input.update.icon) &&
(input.update.labelPlural !== existingObjectMetadata.labelPlural ||
input.update.icon !== existingObjectMetadata.icon)
) {
await this.objectMetadataRelatedRecordsService.updateObjectViews(
objectMetadataForUpdate,

View File

@ -6,10 +6,7 @@ import { Repository } from 'typeorm';
import { FieldMetadataEntity } 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 { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { buildDescriptionForRelationFieldMetadataOnFromField } from 'src/engine/metadata-modules/object-metadata/utils/build-description-for-relation-field-on-from-field.util';
import { buildDescriptionForRelationFieldMetadataOnToField } from 'src/engine/metadata-modules/object-metadata/utils/build-description-for-relation-field-on-to-field.util';
import { buildMigrationsForCustomObjectRelations } from 'src/engine/metadata-modules/object-metadata/utils/build-migrations-for-custom-object-relations.util';
import { buildNameLabelAndDescriptionForForeignKeyFieldMetadata } from 'src/engine/metadata-modules/object-metadata/utils/build-name-label-and-description-for-foreign-key-field-metadata.util';
import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { RelationToDelete } from 'src/engine/metadata-modules/relation-metadata/types/relation-to-delete';
import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util';
@ -38,7 +35,7 @@ export class ObjectMetadataMigrationService {
private readonly workspaceMigrationFactory: WorkspaceMigrationFactory,
) {}
public async createObjectMigration(
public async createTableMigration(
createdObjectMetadata: ObjectMetadataEntity,
) {
await this.workspaceMigrationService.createCustomMigration(
@ -53,7 +50,7 @@ export class ObjectMetadataMigrationService {
);
}
public async createFieldMigrations(
public async createColumnsMigrations(
createdObjectMetadata: ObjectMetadataEntity,
fieldMetadataCollection: FieldMetadataEntity[],
) {
@ -118,163 +115,55 @@ export class ObjectMetadataMigrationService {
);
}
public async createStandardRelationsUpdatesMigrations(
public async createUpdateForeignKeysMigrations(
existingObjectMetadata: ObjectMetadataEntity,
updatedObjectMetadata: ObjectMetadataEntity,
relationsAndForeignKeysMetadata: {
relatedObjectMetadata: ObjectMetadataEntity;
foreignKeyFieldMetadata: FieldMetadataEntity;
}[],
workspaceId: string,
) {
const existingTableName = computeObjectTargetTable(existingObjectMetadata);
const newTableName = computeObjectTargetTable(updatedObjectMetadata);
for (const {
relatedObjectMetadata,
foreignKeyFieldMetadata,
} of relationsAndForeignKeysMetadata) {
const relatedObjectTableName = computeObjectTargetTable(
relatedObjectMetadata,
);
const columnName = `${existingObjectMetadata.nameSingular}Id`;
const columnType = fieldMetadataTypeToColumnType(
foreignKeyFieldMetadata.type,
);
if (existingTableName !== newTableName) {
const relatedObjectsIds = await this.relationMetadataRepository
.find({
where: {
workspaceId,
fromObjectMetadataId: existingObjectMetadata.id,
},
})
.then((relations) =>
relations.map((relation) => relation.toObjectMetadataId),
);
const foreignKeyFieldMetadataForStandardRelation =
await this.fieldMetadataRepository.find({
where: {
isCustom: false,
settings: {
isForeignKey: true,
},
name: `${existingObjectMetadata.nameSingular}Id`,
workspaceId: workspaceId,
},
});
await Promise.all(
foreignKeyFieldMetadataForStandardRelation.map(
async (foreignKeyFieldMetadata) => {
if (
relatedObjectsIds.includes(
foreignKeyFieldMetadata.objectMetadataId,
)
) {
const relatedObject =
await this.objectMetadataRepository.findOneBy({
id: foreignKeyFieldMetadata.objectMetadataId,
workspaceId: workspaceId,
});
if (relatedObject) {
// 1. Update to and from relation fieldMetadata
const toFieldRelationFieldMetadataId =
await this.fieldMetadataRepository
.findOneByOrFail({
name: existingObjectMetadata.nameSingular,
objectMetadataId: relatedObject.id,
workspaceId: workspaceId,
})
.then((field) => field.id);
const { description: descriptionForToField } =
buildDescriptionForRelationFieldMetadataOnToField({
relationObjectMetadataNamePlural: relatedObject.namePlural,
targetObjectLabelSingular:
updatedObjectMetadata.labelSingular,
});
await this.fieldMetadataRepository.update(
toFieldRelationFieldMetadataId,
{
name: updatedObjectMetadata.nameSingular,
label: updatedObjectMetadata.labelSingular,
description: descriptionForToField,
},
);
const fromFieldRelationFieldMetadataId =
await this.relationMetadataRepository
.findOneByOrFail({
fromObjectMetadataId: existingObjectMetadata.id,
toObjectMetadataId: relatedObject.id,
toFieldMetadataId: toFieldRelationFieldMetadataId,
workspaceId,
})
.then((relation) => relation?.fromFieldMetadataId);
await this.fieldMetadataRepository.update(
fromFieldRelationFieldMetadataId,
{
description:
buildDescriptionForRelationFieldMetadataOnFromField({
relationObjectMetadataNamePlural:
relatedObject.namePlural,
targetObjectLabelSingular:
updatedObjectMetadata.labelSingular,
}).description,
},
);
// 2. Update foreign key fieldMetadata
const {
name: updatedNameForForeignKeyFieldMetadata,
label: updatedLabelForForeignKeyFieldMetadata,
description: updatedDescriptionForForeignKeyFieldMetadata,
} = buildNameLabelAndDescriptionForForeignKeyFieldMetadata({
targetObjectNameSingular: updatedObjectMetadata.nameSingular,
targetObjectLabelSingular:
updatedObjectMetadata.labelSingular,
relatedObjectLabelSingular: relatedObject.labelSingular,
});
await this.fieldMetadataRepository.update(
foreignKeyFieldMetadata.id,
{
name: updatedNameForForeignKeyFieldMetadata,
label: updatedLabelForForeignKeyFieldMetadata,
description: updatedDescriptionForForeignKeyFieldMetadata,
},
);
const relatedObjectTableName =
computeObjectTargetTable(relatedObject);
const columnName = `${existingObjectMetadata.nameSingular}Id`;
const columnType = fieldMetadataTypeToColumnType(
foreignKeyFieldMetadata.type,
);
await this.workspaceMigrationService.createCustomMigration(
generateMigrationName(
`rename-${existingObjectMetadata.nameSingular}-to-${updatedObjectMetadata.nameSingular}-in-${relatedObject.nameSingular}`,
),
workspaceId,
[
{
name: relatedObjectTableName,
action: WorkspaceMigrationTableActionType.ALTER,
columns: [
{
action: WorkspaceMigrationColumnActionType.ALTER,
currentColumnDefinition: {
columnName,
columnType,
isNullable: true,
defaultValue: null,
},
alteredColumnDefinition: {
columnName: `${updatedObjectMetadata.nameSingular}Id`,
columnType,
isNullable: true,
defaultValue: null,
},
},
],
},
],
);
}
}
},
await this.workspaceMigrationService.createCustomMigration(
generateMigrationName(
`rename-${existingObjectMetadata.nameSingular}-to-${updatedObjectMetadata.nameSingular}-in-${relatedObjectMetadata.nameSingular}`,
),
workspaceId,
[
{
name: relatedObjectTableName,
action: WorkspaceMigrationTableActionType.ALTER,
columns: [
{
action: WorkspaceMigrationColumnActionType.ALTER,
currentColumnDefinition: {
columnName,
columnType,
isNullable: true,
defaultValue: null,
},
alteredColumnDefinition: {
columnName: `${updatedObjectMetadata.nameSingular}Id`,
columnType,
isNullable: true,
defaultValue: null,
},
},
],
},
],
);
}
}

View File

@ -18,17 +18,27 @@ import {
RelationMetadataType,
RelationOnDeleteAction,
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { mapUdtNameToFieldType } from 'src/engine/metadata-modules/remote-server/remote-table/utils/udt-name-mapper.util';
import {
CUSTOM_OBJECT_STANDARD_FIELD_IDS,
STANDARD_OBJECT_FIELD_IDS,
} from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
import { STANDARD_OBJECT_ICONS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-icons';
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
import {
createForeignKeyDeterministicUuid,
createRelationDeterministicUuid,
} from 'src/engine/workspace-manager/workspace-sync-metadata/utils/create-deterministic-uuid.util';
import { capitalize } from 'src/utils/capitalize';
const DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS = [
STANDARD_OBJECT_IDS.timelineActivity,
STANDARD_OBJECT_IDS.favorite,
STANDARD_OBJECT_IDS.attachment,
STANDARD_OBJECT_IDS.noteTarget,
STANDARD_OBJECT_IDS.taskTarget,
];
@Injectable()
export class ObjectMetadataRelationService {
constructor(
@ -40,46 +50,63 @@ export class ObjectMetadataRelationService {
private readonly relationMetadataRepository: Repository<RelationMetadataEntity>,
) {}
public async createMetadata(
public async createRelationsAndForeignKeysMetadata(
workspaceId: string,
createdObjectMetadata: ObjectMetadataEntity,
{ primaryKeyFieldMetadataSettings, primaryKeyColumnType },
) {
const relatedObjectMetadataCollection = await Promise.all(
DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS.map(
async (relationObjectMetadataStandardId) =>
this.createRelationAndForeignKeyMetadata(
workspaceId,
createdObjectMetadata,
mapUdtNameToFieldType(primaryKeyColumnType ?? 'uuid'),
primaryKeyFieldMetadataSettings,
relationObjectMetadataStandardId,
),
),
);
return relatedObjectMetadataCollection;
}
private async createRelationAndForeignKeyMetadata(
workspaceId: string,
createdObjectMetadata: ObjectMetadataEntity,
objectPrimaryKeyType: FieldMetadataType,
objectPrimaryKeyFieldSettings:
| FieldMetadataSettings<FieldMetadataType | 'default'>
| undefined,
relatedObjectMetadataName: string,
relationObjectMetadataStandardId: string,
) {
const relatedObjectMetadata =
await this.objectMetadataRepository.findOneByOrFail({
nameSingular: relatedObjectMetadataName,
standardId: relationObjectMetadataStandardId,
workspaceId: workspaceId,
isCustom: false,
});
await this.createForeignKeyFieldMetadata(
workspaceId,
createdObjectMetadata,
relatedObjectMetadata,
objectPrimaryKeyType,
objectPrimaryKeyFieldSettings,
);
const relationFieldMetadataCollection =
await this.createRelationFieldMetadas(
workspaceId,
createdObjectMetadata,
relatedObjectMetadata,
objectPrimaryKeyType,
objectPrimaryKeyFieldSettings,
);
const relationFieldMetadata = await this.createRelationFields(
await this.createRelationMetadataFromFieldMetadatas(
workspaceId,
createdObjectMetadata,
relatedObjectMetadata,
);
await this.createRelationMetadata(
workspaceId,
createdObjectMetadata,
relatedObjectMetadata,
relationFieldMetadata,
relationFieldMetadataCollection,
);
return relatedObjectMetadata;
}
private async createForeignKeyFieldMetadata(
private async createRelationFieldMetadas(
workspaceId: string,
createdObjectMetadata: ObjectMetadataEntity,
relatedObjectMetadata: ObjectMetadataEntity,
@ -88,99 +115,187 @@ export class ObjectMetadataRelationService {
| FieldMetadataSettings<FieldMetadataType | 'default'>
| undefined,
) {
const customStandardFieldId =
STANDARD_OBJECT_FIELD_IDS[relatedObjectMetadata.nameSingular].custom;
if (!customStandardFieldId) {
throw new Error(
`Custom standard field ID not found for ${relatedObjectMetadata.nameSingular}`,
);
}
const { name, label, description } =
buildNameLabelAndDescriptionForForeignKeyFieldMetadata({
targetObjectNameSingular: createdObjectMetadata.nameSingular,
targetObjectLabelSingular: createdObjectMetadata.labelSingular,
relatedObjectLabelSingular: relatedObjectMetadata.labelSingular,
});
await this.fieldMetadataRepository.save({
standardId: createForeignKeyDeterministicUuid({
objectId: createdObjectMetadata.id,
standardId: customStandardFieldId,
}),
objectMetadataId: relatedObjectMetadata.id,
workspaceId: workspaceId,
isCustom: false,
isActive: true,
type: objectPrimaryKeyType,
name,
label,
description,
icon: undefined,
isNullable: true,
isSystem: true,
defaultValue: undefined,
settings: { ...objectPrimaryKeyFieldSettings, isForeignKey: true },
});
}
private async createRelationFields(
workspaceId: string,
createdObjectMetadata: ObjectMetadataEntity,
relatedObjectMetadata: ObjectMetadataEntity,
) {
return await this.fieldMetadataRepository.save([
this.createFromField(
return this.fieldMetadataRepository.save([
this.buildFromFieldMetadata(
workspaceId,
createdObjectMetadata,
relatedObjectMetadata,
),
this.createToField(
this.buildToFieldMetadata(
workspaceId,
createdObjectMetadata,
relatedObjectMetadata,
),
this.buildForeignKeyFieldMetadata(
workspaceId,
createdObjectMetadata,
relatedObjectMetadata,
objectPrimaryKeyType,
objectPrimaryKeyFieldSettings,
),
]);
}
private createFromField(
public async updateRelationsAndForeignKeysMetadata(
workspaceId: string,
createdObjectMetadata: ObjectMetadataEntity,
updatedObjectMetadata: ObjectMetadataEntity,
): Promise<
{
relatedObjectMetadata: ObjectMetadataEntity;
foreignKeyFieldMetadata: FieldMetadataEntity;
toFieldMetadata: FieldMetadataEntity;
fromFieldMetadata: FieldMetadataEntity;
}[]
> {
return await Promise.all(
DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS.map(
async (relationObjectMetadataStandardId) =>
this.updateRelationAndForeignKeyMetadata(
workspaceId,
updatedObjectMetadata,
relationObjectMetadataStandardId,
),
),
);
}
private async updateRelationAndForeignKeyMetadata(
workspaceId: string,
updatedObjectMetadata: ObjectMetadataEntity,
relationObjectMetadataStandardId: string,
) {
const relatedObjectMetadata =
await this.objectMetadataRepository.findOneByOrFail({
standardId: relationObjectMetadataStandardId,
workspaceId: workspaceId,
isCustom: false,
});
const toFieldMetadataUpdateCriteria = {
standardId: createRelationDeterministicUuid({
objectId: updatedObjectMetadata.id,
standardId:
STANDARD_OBJECT_FIELD_IDS[relatedObjectMetadata.nameSingular].custom,
}),
objectMetadataId: relatedObjectMetadata.id,
workspaceId: workspaceId,
};
const toFieldMetadataUpdateData = this.buildToFieldMetadata(
workspaceId,
updatedObjectMetadata,
relatedObjectMetadata,
true,
);
const toFieldMetadataToUpdate =
await this.fieldMetadataRepository.findOneBy(
toFieldMetadataUpdateCriteria,
);
const toFieldMetadata = await this.fieldMetadataRepository.save({
...toFieldMetadataToUpdate,
...toFieldMetadataUpdateData,
});
const fromFieldMetadataUpdateCriteria = {
standardId:
CUSTOM_OBJECT_STANDARD_FIELD_IDS[relatedObjectMetadata.namePlural],
objectMetadataId: updatedObjectMetadata.id,
workspaceId: workspaceId,
};
const fromFieldMetadataUpdateData = this.buildFromFieldMetadata(
workspaceId,
updatedObjectMetadata,
relatedObjectMetadata,
true,
);
const fromFieldMetadataToUpdate =
await this.fieldMetadataRepository.findOneBy(
fromFieldMetadataUpdateCriteria,
);
const fromFieldMetadata = await this.fieldMetadataRepository.save({
...fromFieldMetadataToUpdate,
...fromFieldMetadataUpdateData,
});
const foreignKeyFieldMetadataUpdateCriteria = {
standardId: createForeignKeyDeterministicUuid({
objectId: updatedObjectMetadata.id,
standardId:
STANDARD_OBJECT_FIELD_IDS[relatedObjectMetadata.nameSingular].custom,
}),
objectMetadataId: relatedObjectMetadata.id,
workspaceId: workspaceId,
};
const foreignKeyFieldMetadataUpdateData = this.buildForeignKeyFieldMetadata(
workspaceId,
updatedObjectMetadata,
relatedObjectMetadata,
FieldMetadataType.UUID,
undefined,
true,
);
const foreignKeyFieldMetadataToUpdate =
await this.fieldMetadataRepository.findOneBy(
foreignKeyFieldMetadataUpdateCriteria,
);
const foreignKeyFieldMetadata = await this.fieldMetadataRepository.save({
...foreignKeyFieldMetadataToUpdate,
...foreignKeyFieldMetadataUpdateData,
});
return {
relatedObjectMetadata,
foreignKeyFieldMetadata,
toFieldMetadata,
fromFieldMetadata,
};
}
private buildFromFieldMetadata(
workspaceId: string,
objectMetadata: ObjectMetadataEntity,
relatedObjectMetadata: ObjectMetadataEntity,
isUpdate = false,
) {
const relationObjectMetadataNamePlural = relatedObjectMetadata.namePlural;
const { description } = buildDescriptionForRelationFieldMetadataOnFromField(
{
relationObjectMetadataNamePlural,
targetObjectLabelSingular: createdObjectMetadata.labelSingular,
targetObjectLabelSingular: objectMetadata.labelSingular,
},
);
return {
standardId:
CUSTOM_OBJECT_STANDARD_FIELD_IDS[relationObjectMetadataNamePlural],
objectMetadataId: createdObjectMetadata.id,
workspaceId: workspaceId,
isCustom: false,
isActive: true,
isSystem: true,
type: FieldMetadataType.RELATION,
name: relatedObjectMetadata.namePlural,
label: capitalize(relationObjectMetadataNamePlural),
description,
icon:
STANDARD_OBJECT_ICONS[relatedObjectMetadata.nameSingular] ||
'IconBuildingSkyscraper',
isNullable: true,
...(!isUpdate
? {
standardId:
CUSTOM_OBJECT_STANDARD_FIELD_IDS[
relationObjectMetadataNamePlural
],
objectMetadataId: objectMetadata.id,
workspaceId: workspaceId,
isCustom: false,
isActive: true,
isSystem: true,
type: FieldMetadataType.RELATION,
name: relatedObjectMetadata.namePlural,
label: capitalize(relationObjectMetadataNamePlural),
description,
icon:
STANDARD_OBJECT_ICONS[relatedObjectMetadata.nameSingular] ||
'IconBuildingSkyscraper',
isNullable: true,
}
: {}),
};
}
private createToField(
private buildToFieldMetadata(
workspaceId: string,
createdObjectMetadata: ObjectMetadataEntity,
objectMetadata: ObjectMetadataEntity,
relatedObjectMetadata: ObjectMetadataEntity,
isUpdate = false,
) {
const customStandardFieldId =
STANDARD_OBJECT_FIELD_IDS[relatedObjectMetadata.nameSingular].custom;
@ -193,35 +308,96 @@ export class ObjectMetadataRelationService {
const { description } = buildDescriptionForRelationFieldMetadataOnToField({
relationObjectMetadataNamePlural: relatedObjectMetadata.namePlural,
targetObjectLabelSingular: createdObjectMetadata.labelSingular,
targetObjectLabelSingular: objectMetadata.labelSingular,
});
return {
standardId: createRelationDeterministicUuid({
objectId: createdObjectMetadata.id,
standardId: customStandardFieldId,
}),
objectMetadataId: relatedObjectMetadata.id,
workspaceId: workspaceId,
isCustom: false,
isActive: true,
isSystem: true,
type: FieldMetadataType.RELATION,
name: createdObjectMetadata.nameSingular,
label: createdObjectMetadata.labelSingular,
name: objectMetadata.nameSingular,
label: objectMetadata.labelSingular,
description,
icon: 'IconBuildingSkyscraper',
isNullable: true,
...(!isUpdate
? {
standardId: createRelationDeterministicUuid({
objectId: objectMetadata.id,
standardId: customStandardFieldId,
}),
objectMetadataId: relatedObjectMetadata.id,
workspaceId: workspaceId,
isCustom: false,
isActive: true,
isSystem: true,
type: FieldMetadataType.RELATION,
name: objectMetadata.nameSingular,
label: objectMetadata.labelSingular,
description,
icon: 'IconBuildingSkyscraper',
isNullable: true,
}
: {}),
};
}
private async createRelationMetadata(
private buildForeignKeyFieldMetadata(
workspaceId: string,
objectMetadata: ObjectMetadataEntity,
relatedObjectMetadata: ObjectMetadataEntity,
objectPrimaryKeyType: FieldMetadataType,
objectPrimaryKeyFieldSettings:
| FieldMetadataSettings<FieldMetadataType | 'default'>
| undefined,
isUpdate = false,
) {
const customStandardFieldId =
STANDARD_OBJECT_FIELD_IDS[relatedObjectMetadata.nameSingular].custom;
if (!customStandardFieldId) {
throw new Error(
`Custom standard field ID not found for ${relatedObjectMetadata.nameSingular}`,
);
}
const { name, label, description } =
buildNameLabelAndDescriptionForForeignKeyFieldMetadata({
targetObjectNameSingular: objectMetadata.nameSingular,
targetObjectLabelSingular: objectMetadata.labelSingular,
relatedObjectLabelSingular: relatedObjectMetadata.labelSingular,
});
return {
name,
label,
description,
...(!isUpdate
? {
standardId: createForeignKeyDeterministicUuid({
objectId: objectMetadata.id,
standardId: customStandardFieldId,
}),
objectMetadataId: relatedObjectMetadata.id,
workspaceId: workspaceId,
isCustom: false,
isActive: true,
type: objectPrimaryKeyType,
name,
label,
description,
icon: undefined,
isNullable: true,
isSystem: true,
defaultValue: undefined,
settings: { ...objectPrimaryKeyFieldSettings, isForeignKey: true },
}
: {}),
};
}
private async createRelationMetadataFromFieldMetadatas(
workspaceId: string,
createdObjectMetadata: ObjectMetadataEntity,
relatedObjectMetadata: ObjectMetadataEntity,
relationFieldMetadata: FieldMetadataEntity[],
relationFieldMetadataCollection: FieldMetadataEntity[],
) {
const relationFieldMetadataMap = relationFieldMetadata.reduce(
const relationFieldMetadataMap = relationFieldMetadataCollection.reduce(
(acc, fieldMetadata: FieldMetadataEntity) => {
if (fieldMetadata.type === FieldMetadataType.RELATION) {
acc[fieldMetadata.objectMetadataId] = fieldMetadata;
@ -247,7 +423,10 @@ export class ObjectMetadataRelationService {
]);
}
async updateObjectRelationships(objectMetadataId: string, isActive: boolean) {
async updateObjectRelationshipsActivationStatus(
objectMetadataId: string,
isActive: boolean,
) {
const affectedRelations = await this.relationMetadataRepository.find({
where: [
{ fromObjectMetadataId: objectMetadataId },