diff --git a/packages/twenty-server/src/engine/core-modules/application/application-sync.service.ts b/packages/twenty-server/src/engine/core-modules/application/application-sync.service.ts index bee24bc02f..e1cf7696dd 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-sync.service.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-sync.service.ts @@ -6,11 +6,12 @@ import { ApplicationManifest, FieldManifest, ObjectManifest, + RelationFieldManifest, RoleManifest, ServerlessFunctionManifest, ServerlessFunctionTriggerManifest, } from 'twenty-shared/application'; -import { HTTPMethod, Sources } from 'twenty-shared/types'; +import { FieldMetadataType, HTTPMethod, Sources } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; @@ -89,6 +90,12 @@ export class ApplicationSyncService { applicationId: application.id, }); + await this.syncRelations({ + objectsToSync: manifest.objects, + workspaceId, + applicationId: application.id, + }); + if (manifest.serverlessFunctions.length > 0) { if (!isDefined(application.serverlessFunctionLayerId)) { throw new ApplicationException( @@ -350,6 +357,12 @@ export class ApplicationSyncService { } } + private isRelationFieldManifest( + field: FieldManifest | RelationFieldManifest, + ): field is RelationFieldManifest { + return field.type === FieldMetadataType.RELATION; + } + private async syncFields({ objectId, fieldsToSync, @@ -359,12 +372,39 @@ export class ApplicationSyncService { objectId: string; workspaceId: string; applicationId: string; - fieldsToSync?: FieldManifest[]; + fieldsToSync?: (FieldManifest | RelationFieldManifest)[]; }) { if (!isDefined(fieldsToSync)) { return; } + const regularFields = fieldsToSync.filter( + (field): field is FieldManifest => !this.isRelationFieldManifest(field), + ); + + await this.syncRegularFields({ + objectId, + fieldsToSync: regularFields, + workspaceId, + applicationId, + }); + } + + private async syncRegularFields({ + objectId, + fieldsToSync, + workspaceId, + applicationId, + }: { + objectId: string; + workspaceId: string; + applicationId: string; + fieldsToSync: FieldManifest[]; + }) { + if (fieldsToSync.length === 0) { + return; + } + const { flatFieldMetadataMaps: existingFlatFieldMetadataMaps } = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { @@ -476,6 +516,82 @@ export class ApplicationSyncService { } } + private async syncRelationFields({ + objectId, + relationsToSync, + workspaceId, + applicationId, + flatObjectMetadataMaps, + }: { + objectId: string; + relationsToSync: RelationFieldManifest[]; + workspaceId: string; + applicationId: string; + flatObjectMetadataMaps: { + idByUniversalIdentifier: Partial>; + }; + }) { + const { flatFieldMetadataMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatFieldMetadataMaps'], + }, + ); + + for (const relation of relationsToSync) { + const existingRelationField = Object.values( + flatFieldMetadataMaps.byId, + ).find( + (field) => + isDefined(field) && + field.universalIdentifier === relation.universalIdentifier, + ); + + if (isDefined(existingRelationField)) { + continue; + } + + const targetObjectId = + flatObjectMetadataMaps.idByUniversalIdentifier[ + relation.targetObjectUniversalIdentifier + ]; + + if (!isDefined(targetObjectId)) { + throw new ApplicationException( + `Failed to find target object with universalIdentifier ${relation.targetObjectUniversalIdentifier}`, + ApplicationExceptionCode.OBJECT_NOT_FOUND, + ); + } + + const createFieldInput: CreateFieldInput = { + name: computeMetadataNameFromLabelOrThrow(relation.label), + type: FieldMetadataType.RELATION, + label: relation.label, + description: relation.description ?? undefined, + icon: relation.icon ?? undefined, + objectMetadataId: objectId, + universalIdentifier: relation.universalIdentifier, + standardId: relation.universalIdentifier, + applicationId, + isCustom: true, + workspaceId, + relationCreationPayload: { + type: relation.relationType, + targetObjectMetadataId: targetObjectId, + targetFieldLabel: relation.targetFieldLabel, + targetFieldIcon: relation.targetFieldIcon ?? 'IconRelationOneToMany', + }, + }; + + await this.fieldMetadataService.createOneField({ + createFieldInput, + workspaceId, + applicationId, + }); + } + } + private async syncObjects({ objectsToSync, workspaceId, @@ -607,6 +723,55 @@ export class ApplicationSyncService { } } + private async syncRelations({ + objectsToSync, + workspaceId, + applicationId, + }: { + objectsToSync: ObjectManifest[]; + workspaceId: string; + applicationId: string; + }) { + const { flatObjectMetadataMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatObjectMetadataMaps'], + }, + ); + + for (const objectToSync of objectsToSync) { + const relationFields = objectToSync.fields.filter( + (field): field is RelationFieldManifest => + this.isRelationFieldManifest(field), + ); + + if (relationFields.length === 0) { + continue; + } + + const sourceObjectId = + flatObjectMetadataMaps.idByUniversalIdentifier[ + objectToSync.universalIdentifier + ]; + + if (!isDefined(sourceObjectId)) { + throw new ApplicationException( + `Failed to find source object with universalIdentifier ${objectToSync.universalIdentifier}`, + ApplicationExceptionCode.OBJECT_NOT_FOUND, + ); + } + + await this.syncRelationFields({ + objectId: sourceObjectId, + relationsToSync: relationFields, + workspaceId, + applicationId, + flatObjectMetadataMaps, + }); + } + } + private async syncServerlessFunctions({ serverlessFunctionsToSync, code, diff --git a/packages/twenty-shared/src/application/index.ts b/packages/twenty-shared/src/application/index.ts index 4f0cfcb68e..622a2efe70 100644 --- a/packages/twenty-shared/src/application/index.ts +++ b/packages/twenty-shared/src/application/index.ts @@ -15,6 +15,7 @@ export { DEFAULT_API_URL_NAME } from './constants/DefaultApiUrlName'; export type { FieldManifest } from './fieldManifestType'; export type { ObjectManifest } from './objectManifestType'; export type { PackageJson } from './packageJsonType'; +export type { RelationFieldManifest } from './relationFieldManifestType'; export type { RoleManifest } from './roleManifestType'; export type { InputJsonSchema, diff --git a/packages/twenty-shared/src/application/objectManifestType.ts b/packages/twenty-shared/src/application/objectManifestType.ts index 467880130f..501b50d1b3 100644 --- a/packages/twenty-shared/src/application/objectManifestType.ts +++ b/packages/twenty-shared/src/application/objectManifestType.ts @@ -1,4 +1,5 @@ import { type FieldManifest } from '@/application'; +import { type RelationFieldManifest } from '@/application/relationFieldManifestType'; import { type SyncableEntityOptions } from '@/application/syncableEntityOptionsType'; export type ObjectManifest = SyncableEntityOptions & { @@ -8,5 +9,5 @@ export type ObjectManifest = SyncableEntityOptions & { labelPlural: string; description?: string; icon?: string; - fields: FieldManifest[]; + fields: (FieldManifest | RelationFieldManifest)[]; }; diff --git a/packages/twenty-shared/src/application/relationFieldManifestType.ts b/packages/twenty-shared/src/application/relationFieldManifestType.ts new file mode 100644 index 0000000000..e9bb5e8203 --- /dev/null +++ b/packages/twenty-shared/src/application/relationFieldManifestType.ts @@ -0,0 +1,17 @@ +import { type SyncableEntityOptions } from '@/application/syncableEntityOptionsType'; +import { type FieldMetadataType } from '@/types'; +import { type RelationOnDeleteAction } from '@/types/RelationOnDeleteAction.type'; +import { type RelationType } from '@/types/RelationType'; + +export type RelationFieldManifest = SyncableEntityOptions & { + name: string; + label: string; + description?: string; + icon?: string; + relationType: RelationType; + targetObjectUniversalIdentifier: string; + targetFieldLabel: string; + targetFieldIcon?: string; + onDelete?: RelationOnDeleteAction; + type: FieldMetadataType; +};