Improve performance on metadata computation (#12785)
In this PR: ## Improve recompute metadata cache performance. We are aiming for ~100ms Deleting relationMetadata table and FKs pointing on it Fetching indexMetadata and indexFieldMetadata in a separate query as typeorm is suboptimizing ## Remove caching lock As recomputing the metadata cache is lighter, we try to stop preventing multiple concurrent computations. This also simplifies interfaces ## Introduce self recovery mecanisms to recompute cache automatically if corrupted Aka getFreshObjectMetadataMaps ## custom object resolver performance improvement: 1sec to 200ms Double check queries and indexes used while creating a custom object Remove the queries to db to use the cached objectMetadataMap ## reduce objectMetadataMaps to 500kb <img width="222" alt="image" src="https://github.com/user-attachments/assets/2370dc80-49b6-4b63-8d5e-30c5ebdaa062" /> We used to stored 3 fieldMetadataMaps (byId, byName, byJoinColumnName). While this is great for devXP, this is not great for performances. Using the same mecanisme as for objectMetadataMap: we only keep byIdMap and introduce two otherMaps to idByName, idByJoinColumnName to make the bridge ## Add dataloader on IndexMetadata (aka indexMetadataList in the API) ## Improve field resolver performances too ## Deprecate ClientConfig
This commit is contained in:
@ -26,6 +26,7 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi
|
||||
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
|
||||
import { RemoteTableRelationsModule } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/remote-table-relations.module';
|
||||
import { SearchVectorModule } from 'src/engine/metadata-modules/search-vector/search-vector.module';
|
||||
import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module';
|
||||
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
|
||||
import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module';
|
||||
import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module';
|
||||
@ -59,6 +60,7 @@ import { UpdateObjectPayload } from './dtos/update-object.input';
|
||||
PermissionsModule,
|
||||
WorkspacePermissionsCacheModule,
|
||||
WorkspaceCacheStorageModule,
|
||||
WorkspaceMetadataCacheModule,
|
||||
],
|
||||
services: [
|
||||
ObjectMetadataService,
|
||||
|
||||
@ -17,6 +17,7 @@ import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorat
|
||||
import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto';
|
||||
import { IndexMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-metadata.dto';
|
||||
import { DeleteOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/delete-object.input';
|
||||
import { ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto';
|
||||
import {
|
||||
@ -150,4 +151,26 @@ export class ObjectMetadataResolver {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@ResolveField(() => [IndexMetadataDTO], { nullable: false })
|
||||
async indexMetadataList(
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
@Parent() objectMetadata: ObjectMetadataDTO,
|
||||
@Context() context: { loaders: IDataloaders },
|
||||
): Promise<IndexMetadataDTO[]> {
|
||||
try {
|
||||
const indexMetadataItems = await context.loaders.indexMetadataLoader.load(
|
||||
{
|
||||
objectMetadata,
|
||||
workspaceId: workspace.id,
|
||||
},
|
||||
);
|
||||
|
||||
return indexMetadataItems;
|
||||
} catch (error) {
|
||||
objectMetadataGraphqlApiExceptionHandler(error);
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,11 +4,9 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { i18n } from '@lingui/core';
|
||||
import { Query, QueryOptions } from '@ptc-org/nestjs-query-core';
|
||||
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
||||
import { APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations';
|
||||
import { APP_LOCALES } from 'twenty-shared/translations';
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
import { FindManyOptions, FindOneOptions, In, Not, Repository } from 'typeorm';
|
||||
|
||||
import { ObjectMetadataStandardIdToIdMap } from 'src/engine/metadata-modules/object-metadata/interfaces/object-metadata-standard-id-to-id-map';
|
||||
import { FindManyOptions, FindOneOptions, In, Repository } from 'typeorm';
|
||||
|
||||
import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
@ -35,7 +33,10 @@ import {
|
||||
} 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 { SearchVectorService } from 'src/engine/metadata-modules/search-vector/search-vector.service';
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { validateNameAndLabelAreSyncOrThrow } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util';
|
||||
import { validatesNoOtherObjectWithSameNameExistsOrThrows } from 'src/engine/metadata-modules/utils/validate-no-other-object-with-same-name-exists-or-throw.util';
|
||||
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
|
||||
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
|
||||
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
|
||||
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
||||
@ -58,6 +59,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
|
||||
private readonly remoteTableRelationsService: RemoteTableRelationsService,
|
||||
private readonly dataSourceService: DataSourceService,
|
||||
private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService,
|
||||
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
|
||||
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
|
||||
private readonly searchVectorService: SearchVectorService,
|
||||
@ -89,6 +91,13 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
override async createOne(
|
||||
objectMetadataInput: CreateObjectInput,
|
||||
): Promise<ObjectMetadataEntity> {
|
||||
const { objectMetadataMaps } =
|
||||
await this.workspaceMetadataCacheService.getExistingOrRecomputeMetadataMaps(
|
||||
{
|
||||
workspaceId: objectMetadataInput.workspaceId,
|
||||
},
|
||||
);
|
||||
|
||||
const lastDataSourceMetadata =
|
||||
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
|
||||
objectMetadataInput.workspaceId,
|
||||
@ -131,13 +140,28 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
);
|
||||
}
|
||||
|
||||
await this.validatesNoOtherObjectWithSameNameExistsOrThrows({
|
||||
validatesNoOtherObjectWithSameNameExistsOrThrows({
|
||||
objectMetadataNamePlural: objectMetadataInput.namePlural,
|
||||
objectMetadataNameSingular: objectMetadataInput.nameSingular,
|
||||
workspaceId: objectMetadataInput.workspaceId,
|
||||
objectMetadataMaps,
|
||||
});
|
||||
|
||||
const createdObjectMetadata = await super.createOne({
|
||||
const baseCustomFields = buildDefaultFieldsForCustomObject(
|
||||
objectMetadataInput.workspaceId,
|
||||
);
|
||||
|
||||
const labelIdentifierFieldMetadataId = baseCustomFields.find(
|
||||
(field) => field.standardId === CUSTOM_OBJECT_STANDARD_FIELD_IDS.name,
|
||||
)?.id;
|
||||
|
||||
if (!labelIdentifierFieldMetadataId) {
|
||||
throw new ObjectMetadataException(
|
||||
'Label identifier field metadata not created properly',
|
||||
ObjectMetadataExceptionCode.MISSING_CUSTOM_OBJECT_DEFAULT_LABEL_IDENTIFIER_FIELD,
|
||||
);
|
||||
}
|
||||
|
||||
const createdObjectMetadata = await this.objectMetadataRepository.save({
|
||||
...objectMetadataInput,
|
||||
dataSourceId: lastDataSourceMetadata.id,
|
||||
targetTableName: 'DEPRECATED',
|
||||
@ -146,24 +170,8 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
isSystem: false,
|
||||
isRemote: objectMetadataInput.isRemote,
|
||||
isSearchable: !objectMetadataInput.isRemote,
|
||||
fields: objectMetadataInput.isRemote
|
||||
? []
|
||||
: buildDefaultFieldsForCustomObject(objectMetadataInput.workspaceId),
|
||||
});
|
||||
|
||||
const labelIdentifierFieldMetadata = createdObjectMetadata.fields.find(
|
||||
(field) => field.standardId === CUSTOM_OBJECT_STANDARD_FIELD_IDS.name,
|
||||
);
|
||||
|
||||
if (!labelIdentifierFieldMetadata) {
|
||||
throw new ObjectMetadataException(
|
||||
'Label identifier field metadata not created properly',
|
||||
ObjectMetadataExceptionCode.MISSING_CUSTOM_OBJECT_DEFAULT_LABEL_IDENTIFIER_FIELD,
|
||||
);
|
||||
}
|
||||
|
||||
await this.objectMetadataRepository.update(createdObjectMetadata.id, {
|
||||
labelIdentifierFieldMetadataId: labelIdentifierFieldMetadata.id,
|
||||
fields: objectMetadataInput.isRemote ? [] : baseCustomFields,
|
||||
labelIdentifierFieldMetadataId,
|
||||
});
|
||||
|
||||
if (objectMetadataInput.isRemote) {
|
||||
@ -174,6 +182,13 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
objectMetadataInput.primaryKeyColumnType,
|
||||
);
|
||||
} else {
|
||||
const createdRelatedObjectMetadataCollection =
|
||||
await this.objectMetadataFieldRelationService.createRelationsAndForeignKeysMetadata(
|
||||
objectMetadataInput.workspaceId,
|
||||
createdObjectMetadata,
|
||||
objectMetadataMaps,
|
||||
);
|
||||
|
||||
await this.objectMetadataMigrationService.createTableMigration(
|
||||
createdObjectMetadata,
|
||||
);
|
||||
@ -183,12 +198,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
createdObjectMetadata.fields,
|
||||
);
|
||||
|
||||
const createdRelatedObjectMetadataCollection =
|
||||
await this.objectMetadataFieldRelationService.createRelationsAndForeignKeysMetadata(
|
||||
objectMetadataInput.workspaceId,
|
||||
createdObjectMetadata,
|
||||
);
|
||||
|
||||
await this.objectMetadataMigrationService.createRelationMigrations(
|
||||
createdObjectMetadata,
|
||||
createdRelatedObjectMetadataCollection,
|
||||
@ -214,7 +223,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
|
||||
await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache({
|
||||
workspaceId: objectMetadataInput.workspaceId,
|
||||
ignoreLock: true,
|
||||
});
|
||||
|
||||
return createdObjectMetadata;
|
||||
@ -224,6 +232,11 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
input: UpdateOneObjectInput,
|
||||
workspaceId: string,
|
||||
): Promise<ObjectMetadataEntity> {
|
||||
const { objectMetadataMaps } =
|
||||
await this.workspaceMetadataCacheService.getExistingOrRecomputeMetadataMaps(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const inputId = input.id;
|
||||
|
||||
const inputPayload = {
|
||||
@ -238,9 +251,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
|
||||
validateObjectMetadataInputNamesOrThrow(inputPayload);
|
||||
|
||||
const existingObjectMetadata = await this.objectMetadataRepository.findOne({
|
||||
where: { id: inputId, workspaceId: workspaceId },
|
||||
});
|
||||
const existingObjectMetadata = objectMetadataMaps.byId[inputId];
|
||||
|
||||
if (!existingObjectMetadata) {
|
||||
throw new ObjectMetadataException(
|
||||
@ -254,14 +265,14 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
...inputPayload,
|
||||
};
|
||||
|
||||
await this.validatesNoOtherObjectWithSameNameExistsOrThrows({
|
||||
validatesNoOtherObjectWithSameNameExistsOrThrows({
|
||||
objectMetadataNameSingular:
|
||||
existingObjectMetadataCombinedWithUpdateInput.nameSingular,
|
||||
objectMetadataNamePlural:
|
||||
existingObjectMetadataCombinedWithUpdateInput.namePlural,
|
||||
workspaceId: workspaceId,
|
||||
existingObjectMetadataId:
|
||||
existingObjectMetadataCombinedWithUpdateInput.id,
|
||||
objectMetadataMaps,
|
||||
});
|
||||
|
||||
if (existingObjectMetadataCombinedWithUpdateInput.isLabelSyncedWithName) {
|
||||
@ -395,7 +406,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
|
||||
await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache({
|
||||
workspaceId,
|
||||
ignoreLock: true,
|
||||
});
|
||||
|
||||
return objectMetadata;
|
||||
@ -436,44 +446,26 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
});
|
||||
}
|
||||
|
||||
public async findMany(options?: FindManyOptions<ObjectMetadataEntity>) {
|
||||
return this.objectMetadataRepository.find({
|
||||
relations: ['fields'],
|
||||
...options,
|
||||
where: {
|
||||
...options?.where,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public async deleteObjectsMetadata(workspaceId: string) {
|
||||
await this.objectMetadataRepository.delete({ workspaceId });
|
||||
}
|
||||
|
||||
public async getObjectMetadataStandardIdToIdMap(workspaceId: string) {
|
||||
const objectMetadata = await this.findManyWithinWorkspace(workspaceId);
|
||||
|
||||
const objectMetadataStandardIdToIdMap =
|
||||
objectMetadata.reduce<ObjectMetadataStandardIdToIdMap>((acc, object) => {
|
||||
acc[object.standardId ?? ''] = {
|
||||
id: object.id,
|
||||
fields: object.fields.reduce((acc, field) => {
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
acc[field.standardId ?? ''] = field.id;
|
||||
|
||||
return acc;
|
||||
}, {}),
|
||||
};
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return { objectMetadataStandardIdToIdMap };
|
||||
}
|
||||
|
||||
private async handleObjectNameAndLabelUpdates(
|
||||
existingObjectMetadata: ObjectMetadataEntity,
|
||||
objectMetadataForUpdate: ObjectMetadataEntity,
|
||||
existingObjectMetadata: Pick<
|
||||
ObjectMetadataItemWithFieldMaps,
|
||||
'nameSingular' | 'isCustom' | 'id' | 'labelPlural' | 'icon' | 'fieldsById'
|
||||
>,
|
||||
objectMetadataForUpdate: Pick<
|
||||
ObjectMetadataItemWithFieldMaps,
|
||||
| 'nameSingular'
|
||||
| 'isCustom'
|
||||
| 'workspaceId'
|
||||
| 'id'
|
||||
| 'labelSingular'
|
||||
| 'labelPlural'
|
||||
| 'icon'
|
||||
| 'fieldsById'
|
||||
>,
|
||||
inputPayload: UpdateObjectPayload,
|
||||
) {
|
||||
const newTargetTableName = computeObjectTargetTable(
|
||||
@ -533,45 +525,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
async resolveOverridableString(
|
||||
objectMetadata: ObjectMetadataDTO,
|
||||
labelKey: 'labelPlural' | 'labelSingular' | 'description' | 'icon',
|
||||
@ -581,17 +534,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
return objectMetadata[labelKey];
|
||||
}
|
||||
|
||||
if (!locale || locale === SOURCE_LOCALE) {
|
||||
if (
|
||||
objectMetadata.standardOverrides &&
|
||||
isDefined(objectMetadata.standardOverrides[labelKey])
|
||||
) {
|
||||
return objectMetadata.standardOverrides[labelKey] as string;
|
||||
}
|
||||
|
||||
return objectMetadata[labelKey];
|
||||
}
|
||||
|
||||
const translationValue =
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
objectMetadata.standardOverrides?.translations?.[locale]?.[labelKey];
|
||||
|
||||
@ -14,6 +14,8 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat
|
||||
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 { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-on-delete-action.type';
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
import {
|
||||
CUSTOM_OBJECT_STANDARD_FIELD_IDS,
|
||||
STANDARD_OBJECT_FIELD_IDS,
|
||||
@ -41,33 +43,51 @@ export class ObjectMetadataFieldRelationService {
|
||||
|
||||
public async createRelationsAndForeignKeysMetadata(
|
||||
workspaceId: string,
|
||||
sourceObjectMetadata: ObjectMetadataEntity,
|
||||
sourceObjectMetadata: Pick<
|
||||
ObjectMetadataItemWithFieldMaps,
|
||||
'id' | 'nameSingular' | 'labelSingular'
|
||||
>,
|
||||
objectMetadataMaps: ObjectMetadataMaps,
|
||||
) {
|
||||
const relatedObjectMetadataCollection = await Promise.all(
|
||||
DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS.map(
|
||||
async (relationObjectMetadataStandardId) =>
|
||||
this.createRelationAndForeignKeyMetadata(
|
||||
this.createRelationAndForeignKeyMetadata({
|
||||
workspaceId,
|
||||
sourceObjectMetadata,
|
||||
relationObjectMetadataStandardId,
|
||||
),
|
||||
objectMetadataMaps,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
return relatedObjectMetadataCollection;
|
||||
}
|
||||
|
||||
private async createRelationAndForeignKeyMetadata(
|
||||
workspaceId: string,
|
||||
sourceObjectMetadata: ObjectMetadataEntity,
|
||||
relationObjectMetadataStandardId: string,
|
||||
) {
|
||||
const targetObjectMetadata =
|
||||
await this.objectMetadataRepository.findOneByOrFail({
|
||||
standardId: relationObjectMetadataStandardId,
|
||||
workspaceId: workspaceId,
|
||||
isCustom: false,
|
||||
});
|
||||
private async createRelationAndForeignKeyMetadata({
|
||||
workspaceId,
|
||||
sourceObjectMetadata,
|
||||
relationObjectMetadataStandardId,
|
||||
objectMetadataMaps,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
sourceObjectMetadata: Pick<
|
||||
ObjectMetadataItemWithFieldMaps,
|
||||
'id' | 'nameSingular' | 'labelSingular'
|
||||
>;
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
relationObjectMetadataStandardId: string;
|
||||
}) {
|
||||
const targetObjectMetadata = Object.values(objectMetadataMaps.byId).find(
|
||||
(objectMetadata) =>
|
||||
objectMetadata.standardId === relationObjectMetadataStandardId,
|
||||
);
|
||||
|
||||
if (!targetObjectMetadata) {
|
||||
throw new Error(
|
||||
`Target object metadata not found for standard ID: ${relationObjectMetadataStandardId}`,
|
||||
);
|
||||
}
|
||||
|
||||
await this.createFieldMetadataRelation(
|
||||
workspaceId,
|
||||
@ -80,8 +100,11 @@ export class ObjectMetadataFieldRelationService {
|
||||
|
||||
private async createFieldMetadataRelation(
|
||||
workspaceId: string,
|
||||
sourceObjectMetadata: ObjectMetadataEntity,
|
||||
targetObjectMetadata: ObjectMetadataEntity,
|
||||
sourceObjectMetadata: Pick<
|
||||
ObjectMetadataItemWithFieldMaps,
|
||||
'id' | 'nameSingular' | 'labelSingular'
|
||||
>,
|
||||
targetObjectMetadata: ObjectMetadataItemWithFieldMaps,
|
||||
): Promise<FieldMetadataEntity<FieldMetadataType.RELATION>[]> {
|
||||
const sourceFieldMetadata = this.createSourceFieldMetadata(
|
||||
workspaceId,
|
||||
@ -119,7 +142,10 @@ export class ObjectMetadataFieldRelationService {
|
||||
|
||||
public async updateRelationsAndForeignKeysMetadata(
|
||||
workspaceId: string,
|
||||
updatedObjectMetadata: ObjectMetadataEntity,
|
||||
updatedObjectMetadata: Pick<
|
||||
ObjectMetadataEntity,
|
||||
'nameSingular' | 'isCustom' | 'id' | 'labelSingular'
|
||||
>,
|
||||
): Promise<
|
||||
{
|
||||
targetObjectMetadata: ObjectMetadataEntity;
|
||||
@ -141,7 +167,10 @@ export class ObjectMetadataFieldRelationService {
|
||||
|
||||
private async updateRelationAndForeignKeyMetadata(
|
||||
workspaceId: string,
|
||||
sourceObjectMetadata: ObjectMetadataEntity,
|
||||
sourceObjectMetadata: Pick<
|
||||
ObjectMetadataEntity,
|
||||
'nameSingular' | 'id' | 'isCustom' | 'labelSingular'
|
||||
>,
|
||||
targetObjectMetadataStandardId: string,
|
||||
) {
|
||||
const targetObjectMetadata =
|
||||
@ -226,8 +255,14 @@ export class ObjectMetadataFieldRelationService {
|
||||
|
||||
private createSourceFieldMetadata(
|
||||
workspaceId: string,
|
||||
sourceObjectMetadata: ObjectMetadataEntity,
|
||||
targetObjectMetadata: ObjectMetadataEntity,
|
||||
sourceObjectMetadata: Pick<
|
||||
ObjectMetadataItemWithFieldMaps,
|
||||
'labelSingular' | 'id'
|
||||
>,
|
||||
targetObjectMetadata: Pick<
|
||||
ObjectMetadataItemWithFieldMaps,
|
||||
'namePlural' | 'labelSingular'
|
||||
>,
|
||||
): Partial<FieldMetadataEntity<FieldMetadataType.RELATION>> {
|
||||
const relationObjectMetadataNamePlural = targetObjectMetadata.namePlural;
|
||||
|
||||
@ -261,8 +296,8 @@ export class ObjectMetadataFieldRelationService {
|
||||
}
|
||||
|
||||
private updateSourceFieldMetadata(
|
||||
sourceObjectMetadata: ObjectMetadataEntity,
|
||||
targetObjectMetadata: ObjectMetadataEntity,
|
||||
sourceObjectMetadata: Pick<ObjectMetadataEntity, 'labelSingular'>,
|
||||
targetObjectMetadata: Pick<ObjectMetadataEntity, 'namePlural'>,
|
||||
) {
|
||||
const relationObjectMetadataNamePlural = targetObjectMetadata.namePlural;
|
||||
|
||||
@ -280,8 +315,14 @@ export class ObjectMetadataFieldRelationService {
|
||||
|
||||
private createTargetFieldMetadata(
|
||||
workspaceId: string,
|
||||
sourceObjectMetadata: ObjectMetadataEntity,
|
||||
targetObjectMetadata: ObjectMetadataEntity,
|
||||
sourceObjectMetadata: Pick<
|
||||
ObjectMetadataItemWithFieldMaps,
|
||||
'labelSingular' | 'id' | 'nameSingular'
|
||||
>,
|
||||
targetObjectMetadata: Pick<
|
||||
ObjectMetadataItemWithFieldMaps,
|
||||
'namePlural' | 'labelSingular' | 'id' | 'nameSingular'
|
||||
>,
|
||||
): Partial<FieldMetadataEntity<FieldMetadataType.RELATION>> {
|
||||
const customStandardFieldId =
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
@ -319,8 +360,14 @@ export class ObjectMetadataFieldRelationService {
|
||||
}
|
||||
|
||||
private updateTargetFieldMetadata(
|
||||
sourceObjectMetadata: ObjectMetadataEntity,
|
||||
targetObjectMetadata: ObjectMetadataEntity,
|
||||
sourceObjectMetadata: Pick<
|
||||
ObjectMetadataEntity,
|
||||
'nameSingular' | 'labelSingular'
|
||||
>,
|
||||
targetObjectMetadata: Pick<
|
||||
ObjectMetadataEntity,
|
||||
'nameSingular' | 'namePlural'
|
||||
>,
|
||||
) {
|
||||
const customStandardFieldId =
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
|
||||
@ -2,13 +2,14 @@ import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { In, Repository } from 'typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { buildMigrationsForCustomObjectRelations } from 'src/engine/metadata-modules/object-metadata/utils/build-migrations-for-custom-object-relations.util';
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
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 {
|
||||
@ -73,8 +74,14 @@ export class ObjectMetadataMigrationService {
|
||||
}
|
||||
|
||||
public async createRelationMigrations(
|
||||
createdObjectMetadata: ObjectMetadataEntity,
|
||||
relatedObjectMetadataCollection: ObjectMetadataEntity[],
|
||||
createdObjectMetadata: Pick<
|
||||
ObjectMetadataItemWithFieldMaps,
|
||||
'nameSingular' | 'workspaceId' | 'isCustom'
|
||||
>,
|
||||
relatedObjectMetadataCollection: Pick<
|
||||
ObjectMetadataItemWithFieldMaps,
|
||||
'nameSingular' | 'isCustom'
|
||||
>[],
|
||||
) {
|
||||
await this.workspaceMigrationService.createCustomMigration(
|
||||
generateMigrationName(
|
||||
@ -89,8 +96,14 @@ export class ObjectMetadataMigrationService {
|
||||
}
|
||||
|
||||
public async createRenameTableMigration(
|
||||
existingObjectMetadata: ObjectMetadataEntity,
|
||||
objectMetadataForUpdate: ObjectMetadataEntity,
|
||||
existingObjectMetadata: Pick<
|
||||
ObjectMetadataEntity,
|
||||
'nameSingular' | 'isCustom'
|
||||
>,
|
||||
objectMetadataForUpdate: Pick<
|
||||
ObjectMetadataEntity,
|
||||
'nameSingular' | 'isCustom'
|
||||
>,
|
||||
workspaceId: string,
|
||||
) {
|
||||
const newTargetTableName = computeObjectTargetTable(
|
||||
@ -114,8 +127,8 @@ export class ObjectMetadataMigrationService {
|
||||
}
|
||||
|
||||
public async updateRelationMigrations(
|
||||
currentObjectMetadata: ObjectMetadataEntity,
|
||||
alteredObjectMetadata: ObjectMetadataEntity,
|
||||
currentObjectMetadata: Pick<ObjectMetadataEntity, 'nameSingular'>,
|
||||
alteredObjectMetadata: Pick<ObjectMetadataEntity, 'nameSingular'>,
|
||||
relationMetadataCollection: {
|
||||
targetObjectMetadata: ObjectMetadataEntity;
|
||||
targetFieldMetadata: FieldMetadataEntity;
|
||||
@ -282,21 +295,22 @@ export class ObjectMetadataMigrationService {
|
||||
}
|
||||
|
||||
public async recomputeEnumNames(
|
||||
updatedObjectMetadata: ObjectMetadataEntity,
|
||||
updatedObjectMetadata: Pick<
|
||||
ObjectMetadataItemWithFieldMaps,
|
||||
'nameSingular' | 'isCustom' | 'id' | 'fieldsById'
|
||||
>,
|
||||
workspaceId: string,
|
||||
) {
|
||||
const fieldMetadataToUpdate = await this.fieldMetadataRepository.find({
|
||||
where: {
|
||||
objectMetadataId: updatedObjectMetadata.id,
|
||||
workspaceId,
|
||||
type: In([
|
||||
FieldMetadataType.SELECT,
|
||||
FieldMetadataType.MULTI_SELECT,
|
||||
FieldMetadataType.RATING,
|
||||
FieldMetadataType.ACTOR,
|
||||
]),
|
||||
},
|
||||
});
|
||||
const enumFieldMetadataTypes = [
|
||||
FieldMetadataType.SELECT,
|
||||
FieldMetadataType.MULTI_SELECT,
|
||||
FieldMetadataType.RATING,
|
||||
FieldMetadataType.ACTOR,
|
||||
];
|
||||
|
||||
const fieldMetadataToUpdate = Object.values(
|
||||
updatedObjectMetadata.fieldsById,
|
||||
).filter((field) => enumFieldMetadataTypes.includes(field.type));
|
||||
|
||||
for (const fieldMetadata of fieldMetadataToUpdate) {
|
||||
await this.workspaceMigrationService.createCustomMigration(
|
||||
|
||||
@ -83,7 +83,10 @@ export class ObjectMetadataRelatedRecordsService {
|
||||
}
|
||||
|
||||
public async updateObjectViews(
|
||||
updatedObjectMetadata: ObjectMetadataEntity,
|
||||
updatedObjectMetadata: Pick<
|
||||
ObjectMetadataEntity,
|
||||
'id' | 'labelPlural' | 'icon'
|
||||
>,
|
||||
workspaceId: string,
|
||||
) {
|
||||
const viewRepository =
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import {
|
||||
@ -10,6 +11,7 @@ export const buildDefaultFieldsForCustomObject = (
|
||||
workspaceId: string,
|
||||
): Partial<FieldMetadataEntity>[] => [
|
||||
{
|
||||
id: v4(),
|
||||
standardId: BASE_OBJECT_STANDARD_FIELD_IDS.id,
|
||||
type: FieldMetadataType.UUID,
|
||||
name: 'id',
|
||||
@ -24,6 +26,7 @@ export const buildDefaultFieldsForCustomObject = (
|
||||
defaultValue: 'uuid',
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.name,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'name',
|
||||
@ -37,6 +40,7 @@ export const buildDefaultFieldsForCustomObject = (
|
||||
defaultValue: "'Untitled'",
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
standardId: BASE_OBJECT_STANDARD_FIELD_IDS.createdAt,
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
name: 'createdAt',
|
||||
@ -50,6 +54,7 @@ export const buildDefaultFieldsForCustomObject = (
|
||||
defaultValue: 'now',
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
standardId: BASE_OBJECT_STANDARD_FIELD_IDS.updatedAt,
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
name: 'updatedAt',
|
||||
@ -64,6 +69,7 @@ export const buildDefaultFieldsForCustomObject = (
|
||||
defaultValue: 'now',
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
standardId: BASE_OBJECT_STANDARD_FIELD_IDS.deletedAt,
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
name: 'deletedAt',
|
||||
@ -78,6 +84,7 @@ export const buildDefaultFieldsForCustomObject = (
|
||||
defaultValue: null,
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.createdBy,
|
||||
type: FieldMetadataType.ACTOR,
|
||||
name: 'createdBy',
|
||||
@ -92,6 +99,7 @@ export const buildDefaultFieldsForCustomObject = (
|
||||
defaultValue: { name: "''", source: "'MANUAL'" },
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.position,
|
||||
type: FieldMetadataType.POSITION,
|
||||
name: 'position',
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
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 { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-on-delete-action.type';
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import {
|
||||
WorkspaceMigrationColumnActionType,
|
||||
WorkspaceMigrationColumnCreate,
|
||||
@ -10,8 +10,14 @@ import {
|
||||
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
||||
|
||||
export const buildMigrationsForCustomObjectRelations = (
|
||||
createdObjectMetadata: ObjectMetadataEntity,
|
||||
relatedObjectMetadataCollection: ObjectMetadataEntity[],
|
||||
createdObjectMetadata: Pick<
|
||||
ObjectMetadataItemWithFieldMaps,
|
||||
'nameSingular' | 'isCustom'
|
||||
>,
|
||||
relatedObjectMetadataCollection: Pick<
|
||||
ObjectMetadataItemWithFieldMaps,
|
||||
'nameSingular' | 'isCustom'
|
||||
>[],
|
||||
): WorkspaceMigrationTableAction[] => {
|
||||
const migrations: WorkspaceMigrationTableAction[] = [];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user