diff --git a/.vscode/twenty.code-workspace b/.vscode/twenty.code-workspace index 5abed2559c..581e3ed4c0 100644 --- a/.vscode/twenty.code-workspace +++ b/.vscode/twenty.code-workspace @@ -42,10 +42,59 @@ }, ], "settings": { + "editor.formatOnSave": false, + "files.eol": "auto", + "[typescript]": { + "editor.formatOnSave": false, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true, + "source.addMissingImports": "always" + } + }, + "[javascript]": { + "editor.formatOnSave": false, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true, + "source.addMissingImports": "always" + } + }, + "[typescriptreact]": { + "editor.formatOnSave": false, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true, + "source.addMissingImports": "always" + } + }, + "[json]": { + "editor.formatOnSave": true + }, + "javascript.format.enable": false, + "typescript.format.enable": false, + "cSpell.enableFiletypes": [ + "!javascript", + "!json", + "!typescript", + "!typescriptreact", + "md", + "mdx" + ], + "cSpell.words": [ + "twentyhq" + ], + "typescript.preferences.importModuleSpecifier": "non-relative", + "[javascript][typescript][typescriptreact]": { + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit", + "source.addMissingImports": "always" + } + }, + "search.exclude": { + "**/.yarn": true, + }, "files.exclude": { "packages/": true }, - "jest.autoEnable": false, + "jest.runMode": "on-demand", "jest.disabledWorkspaceFolders": [ "ROOT", "packages/twenty-zapier", diff --git a/packages/twenty-front/src/modules/activities/hooks/useInjectIntoActivityTargetsQueries.ts b/packages/twenty-front/src/modules/activities/hooks/useInjectIntoActivityTargetsQueries.ts index 1b70bc0533..195d21cbf8 100644 --- a/packages/twenty-front/src/modules/activities/hooks/useInjectIntoActivityTargetsQueries.ts +++ b/packages/twenty-front/src/modules/activities/hooks/useInjectIntoActivityTargetsQueries.ts @@ -72,6 +72,7 @@ export const useInjectIntoActivityTargetsQueries = () => { overwriteFindManyActivityTargetsQueryInCache({ objectRecordsToOverwrite: newActivityTargets, queryVariables: findManyActivitiyTargetsQueryVariables, + depth: 2, }); }; diff --git a/packages/twenty-front/src/modules/activities/hooks/useRemoveFromActivityTargetsQueries.ts b/packages/twenty-front/src/modules/activities/hooks/useRemoveFromActivityTargetsQueries.ts index dd490f6851..d95b3c96d4 100644 --- a/packages/twenty-front/src/modules/activities/hooks/useRemoveFromActivityTargetsQueries.ts +++ b/packages/twenty-front/src/modules/activities/hooks/useRemoveFromActivityTargetsQueries.ts @@ -63,6 +63,7 @@ export const useRemoveFromActivityTargetsQueries = () => { overwriteFindManyActivityTargetsQueryInCache({ objectRecordsToOverwrite: newActivityTargetsForTargetableObject, queryVariables: findManyActivityTargetsQueryVariables, + depth: 2, }); }; diff --git a/packages/twenty-front/src/modules/activities/inline-cell/hooks/useInjectIntoActivityTargetInlineCellCache.ts b/packages/twenty-front/src/modules/activities/inline-cell/hooks/useInjectIntoActivityTargetInlineCellCache.ts index d3c52f1bf3..f3de9a2041 100644 --- a/packages/twenty-front/src/modules/activities/inline-cell/hooks/useInjectIntoActivityTargetInlineCellCache.ts +++ b/packages/twenty-front/src/modules/activities/inline-cell/hooks/useInjectIntoActivityTargetInlineCellCache.ts @@ -34,6 +34,7 @@ export const useInjectIntoActivityTargetInlineCellCache = () => { overwriteFindManyActivityTargetsQueryInCache({ queryVariables: activityTargetInlineCellQueryVariables, objectRecordsToOverwrite: activityTargetsToInject, + depth: 2, }); }; diff --git a/packages/twenty-front/src/modules/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache.ts b/packages/twenty-front/src/modules/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache.ts index 210ecee55c..9a11224b13 100644 --- a/packages/twenty-front/src/modules/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache.ts +++ b/packages/twenty-front/src/modules/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache.ts @@ -20,14 +20,16 @@ export const useUpsertFindManyRecordsQueryInCache = ({ T extends ObjectRecord = ObjectRecord, >({ queryVariables, + depth = MAX_QUERY_DEPTH_FOR_CACHE_INJECTION, objectRecordsToOverwrite, }: { queryVariables: ObjectRecordQueryVariables; + depth?: number; objectRecordsToOverwrite: T[]; }) => { const findManyRecordsQueryForCacheOverwrite = generateFindManyRecordsQuery({ objectMetadataItem, - depth: MAX_QUERY_DEPTH_FOR_CACHE_INJECTION, // TODO: fix this + depth, // TODO: fix this }); const newObjectRecordConnection = getRecordConnectionFromRecords({ diff --git a/packages/twenty-server/@types/common.d.ts b/packages/twenty-server/@types/common.d.ts index c4fc63d673..fb78d4d60f 100644 --- a/packages/twenty-server/@types/common.d.ts +++ b/packages/twenty-server/@types/common.d.ts @@ -3,3 +3,6 @@ type DeepPartial = { ? Array> : DeepPartial; }; + +// eslint-disable-next-line @typescript-eslint/ban-types +type ExcludeFunctions = T extends Function ? never : T; diff --git a/packages/twenty-server/package.json b/packages/twenty-server/package.json index 8e00c0c356..d008ff97c3 100644 --- a/packages/twenty-server/package.json +++ b/packages/twenty-server/package.json @@ -28,6 +28,7 @@ "database:seed:dev": "npx nx command -- workspace:seed:dev", "database:seed:demo": "npx nx command -- workspace:seed:demo", "database:reset": "npx nx database:truncate && npx nx database:init", + "command": "node dist/src/command", "queue:work": "node dist/src/queue-worker" }, "dependencies": { diff --git a/packages/twenty-server/src/database/commands/data-seed-dev-workspace.command.ts b/packages/twenty-server/src/database/commands/data-seed-dev-workspace.command.ts index b2aa57ecd5..da8810ddaf 100644 --- a/packages/twenty-server/src/database/commands/data-seed-dev-workspace.command.ts +++ b/packages/twenty-server/src/database/commands/data-seed-dev-workspace.command.ts @@ -61,12 +61,10 @@ export class DataSeedWorkspaceCommand extends CommandRunner { schemaName, ); - await this.workspaceSyncMetadataService.syncStandardObjectsAndFieldsMetadata( - { - workspaceId: this.workspaceId, - dataSourceId: dataSourceMetadata.id, - }, - ); + await this.workspaceSyncMetadataService.synchronize({ + workspaceId: this.workspaceId, + dataSourceId: dataSourceMetadata.id, + }); } catch (error) { console.error(error); diff --git a/packages/twenty-server/src/metadata/object-metadata/object-metadata.service.ts b/packages/twenty-server/src/metadata/object-metadata/object-metadata.service.ts index 080da1774f..191527ad54 100644 --- a/packages/twenty-server/src/metadata/object-metadata/object-metadata.service.ts +++ b/packages/twenty-server/src/metadata/object-metadata/object-metadata.service.ts @@ -340,8 +340,9 @@ export class ObjectMetadataService extends TypeOrmQueryService + Partial > = {}; // Double security to only compare non-custom fields @@ -69,7 +69,7 @@ export class WorkspaceFieldComparator { standardObjectMetadata.fields, { shouldIgnoreProperty: (property, originalMetadata) => { - if (['options'].includes(property)) { + if (['options', 'gate'].includes(property)) { return true; } diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/comparators/workspace-object.comparator.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/comparators/workspace-object.comparator.ts index 45bd6ce181..d7ac607afa 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/comparators/workspace-object.comparator.ts +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/comparators/workspace-object.comparator.ts @@ -7,7 +7,7 @@ import { ComparatorAction, ObjectComparatorResult, } from 'src/workspace/workspace-sync-metadata/interfaces/comparator.interface'; -import { PartialObjectMetadata } from 'src/workspace/workspace-sync-metadata/interfaces/partial-object-metadata.interface'; +import { ComputedPartialObjectMetadata } from 'src/workspace/workspace-sync-metadata/interfaces/partial-object-metadata.interface'; import { transformMetadataForComparison } from 'src/workspace/workspace-sync-metadata/comparators/utils/transform-metadata-for-comparison.util'; import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity'; @@ -28,7 +28,7 @@ export class WorkspaceObjectComparator { public compare( originalObjectMetadata: ObjectMetadataEntity | undefined, - standardObjectMetadata: PartialObjectMetadata, + standardObjectMetadata: ComputedPartialObjectMetadata, ): ObjectComparatorResult { // If the object doesn't exist in the original metadata, we need to create it if (!originalObjectMetadata) { @@ -38,7 +38,7 @@ export class WorkspaceObjectComparator { }; } - const objectPropertiesToUpdate: Partial = {}; + const objectPropertiesToUpdate: Partial = {}; // Only compare properties that are not ignored const partialOriginalObjectMetadata = transformMetadataForComparison( diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/custom-objects/custom.object-metadata.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/custom-objects/custom.object-metadata.ts new file mode 100644 index 0000000000..9350dfe194 --- /dev/null +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/custom-objects/custom.object-metadata.ts @@ -0,0 +1,81 @@ +import { BaseCustomObjectMetadata } from 'src/workspace/workspace-sync-metadata/decorators/base-custom-object-metadata.decorator'; +import { FieldMetadata } from 'src/workspace/workspace-sync-metadata/decorators/field-metadata.decorator'; +import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; +import { IsNullable } from 'src/workspace/workspace-sync-metadata/decorators/is-nullable.decorator'; +import { IsSystem } from 'src/workspace/workspace-sync-metadata/decorators/is-system.decorator'; +import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata'; +import { + RelationMetadataType, + RelationOnDeleteAction, +} from 'src/metadata/relation-metadata/relation-metadata.entity'; +import { ActivityTargetObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/activity-target.object-metadata'; +import { RelationMetadata } from 'src/workspace/workspace-sync-metadata/decorators/relation-metadata.decorator'; +import { FavoriteObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/favorite.object-metadata'; +import { AttachmentObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/attachment.object-metadata'; + +@BaseCustomObjectMetadata() +export class CustomObjectMetadata extends BaseObjectMetadata { + @FieldMetadata({ + label: 'Name', + description: 'Name', + type: FieldMetadataType.TEXT, + icon: 'IconAbc', + defaultValue: { value: 'Untitled' }, + }) + name: string; + + @FieldMetadata({ + label: 'Position', + description: 'Position', + type: FieldMetadataType.POSITION, + icon: 'IconHierarchy2', + }) + @IsNullable() + @IsSystem() + position: number; + + @FieldMetadata({ + type: FieldMetadataType.RELATION, + label: 'Activities', + description: (objectMetadata) => + `Activities tied to the ${objectMetadata.labelSingular}`, + icon: 'IconCheckbox', + }) + @RelationMetadata({ + type: RelationMetadataType.ONE_TO_MANY, + inverseSideTarget: () => ActivityTargetObjectMetadata, + onDelete: RelationOnDeleteAction.CASCADE, + }) + @IsNullable() + activityTargets: ActivityTargetObjectMetadata[]; + + @FieldMetadata({ + type: FieldMetadataType.RELATION, + label: 'Favorites', + description: (objectMetadata) => + `Favorites tied to the ${objectMetadata.labelSingular}`, + icon: 'IconHeart', + }) + @RelationMetadata({ + type: RelationMetadataType.ONE_TO_MANY, + inverseSideTarget: () => FavoriteObjectMetadata, + onDelete: RelationOnDeleteAction.CASCADE, + }) + @IsNullable() + favorites: FavoriteObjectMetadata[]; + + @FieldMetadata({ + type: FieldMetadataType.RELATION, + label: 'Attachments', + description: (objectMetadata) => + `Attachments tied to the ${objectMetadata.labelSingular}`, + icon: 'IconFileImport', + }) + @RelationMetadata({ + type: RelationMetadataType.ONE_TO_MANY, + inverseSideTarget: () => AttachmentObjectMetadata, + onDelete: RelationOnDeleteAction.CASCADE, + }) + @IsNullable() + attachments: AttachmentObjectMetadata[]; +} diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/decorators/base-custom-object-metadata.decorator.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/decorators/base-custom-object-metadata.decorator.ts new file mode 100644 index 0000000000..78864d4ab2 --- /dev/null +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/decorators/base-custom-object-metadata.decorator.ts @@ -0,0 +1,20 @@ +import { BaseCustomObjectMetadataDecoratorParams } from 'src/workspace/workspace-sync-metadata/interfaces/reflect-custom-object-metadata.interface'; + +import { TypedReflect } from 'src/utils/typed-reflect'; + +export function BaseCustomObjectMetadata( + params?: BaseCustomObjectMetadataDecoratorParams, +): ClassDecorator { + return (target) => { + const gate = TypedReflect.getMetadata('gate', target); + + TypedReflect.defineMetadata( + 'extendObjectMetadata', + { + ...params, + gate, + }, + target, + ); + }; +} diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/decorators/dynamic-field-metadata.interface.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/decorators/dynamic-field-metadata.interface.ts new file mode 100644 index 0000000000..86b6598044 --- /dev/null +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/decorators/dynamic-field-metadata.interface.ts @@ -0,0 +1,27 @@ +import { DynamicRelationFieldMetadataDecoratorParams } from 'src/workspace/workspace-sync-metadata/interfaces/reflect-computed-relation-field-metadata.interface'; + +import { TypedReflect } from 'src/utils/typed-reflect'; +import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; + +export function DynamicRelationFieldMetadata( + params: DynamicRelationFieldMetadataDecoratorParams, +): PropertyDecorator { + return (target: object, fieldKey: string) => { + const isSystem = + TypedReflect.getMetadata('isSystem', target, fieldKey) ?? false; + const gate = TypedReflect.getMetadata('gate', target, fieldKey); + + TypedReflect.defineMetadata( + 'dynamicRelationFieldMetadataMap', + { + type: FieldMetadataType.RELATION, + paramsFactory: params, + isCustom: false, + isNullable: true, + isSystem, + gate, + }, + target.constructor, + ); + }; +} diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/decorators/relation-metadata.decorator.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/decorators/relation-metadata.decorator.ts index 448f7380ba..28c6e5d55a 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/decorators/relation-metadata.decorator.ts +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/decorators/relation-metadata.decorator.ts @@ -6,33 +6,27 @@ import { } from 'src/workspace/workspace-sync-metadata/interfaces/reflect-relation-metadata.interface'; import { TypedReflect } from 'src/utils/typed-reflect'; -import { convertClassNameToObjectMetadataName } from 'src/workspace/workspace-sync-metadata/utils/convert-class-to-object-metadata-name.util'; import { RelationOnDeleteAction } from 'src/metadata/relation-metadata/relation-metadata.entity'; -export function RelationMetadata( - params: RelationMetadataDecoratorParams, +export function RelationMetadata( + params: RelationMetadataDecoratorParams, ): PropertyDecorator { return (target: object, fieldKey: string) => { const relationMetadataCollection = TypedReflect.getMetadata( - 'relationMetadataCollection', + 'reflectRelationMetadataCollection', target.constructor, ) ?? []; const gate = TypedReflect.getMetadata('gate', target, fieldKey); - const objectName = convertClassNameToObjectMetadataName( - target.constructor.name, - ); - Reflect.defineMetadata( - 'relationMetadataCollection', + TypedReflect.defineMetadata( + 'reflectRelationMetadataCollection', [ ...relationMetadataCollection, { - type: params.type, - fromObjectNameSingular: objectName, - toObjectNameSingular: params.objectName, - fromFieldMetadataName: fieldKey, - toFieldMetadataName: params.inverseSideFieldName ?? objectName, + target, + fieldKey, + ...params, onDelete: params.onDelete ?? RelationOnDeleteAction.SET_NULL, gate, } satisfies ReflectRelationMetadata, diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/factories/index.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/factories/index.ts index b1c95a507e..7218543692 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/factories/index.ts +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/factories/index.ts @@ -1,9 +1,11 @@ import { FeatureFlagFactory } from './feature-flags.factory'; +import { StandardFieldFactory } from './standard-field.factory'; import { StandardObjectFactory } from './standard-object.factory'; import { StandardRelationFactory } from './standard-relation.factory'; export const workspaceSyncMetadataFactories = [ FeatureFlagFactory, + StandardFieldFactory, StandardObjectFactory, StandardRelationFactory, ]; diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/factories/standard-field.factory.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/factories/standard-field.factory.ts new file mode 100644 index 0000000000..eddb3edb4f --- /dev/null +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/factories/standard-field.factory.ts @@ -0,0 +1,102 @@ +import { Injectable } from '@nestjs/common'; + +import { WorkspaceSyncContext } from 'src/workspace/workspace-sync-metadata/interfaces/workspace-sync-context.interface'; +import { FeatureFlagMap } from 'src/core/feature-flag/interfaces/feature-flag-map.interface'; +import { + PartialComputedFieldMetadata, + PartialFieldMetadata, +} from 'src/workspace/workspace-sync-metadata/interfaces/partial-field-metadata.interface'; +import { ReflectFieldMetadata } from 'src/workspace/workspace-sync-metadata/interfaces/reflect-field-metadata.interface'; +import { ReflectObjectMetadata } from 'src/workspace/workspace-sync-metadata/interfaces/reflect-object-metadata.interface'; +import { ReflectDynamicRelationFieldMetadata } from 'src/workspace/workspace-sync-metadata/interfaces/reflect-computed-relation-field-metadata.interface'; + +import { TypedReflect } from 'src/utils/typed-reflect'; +import { isGatedAndNotEnabled } from 'src/workspace/workspace-sync-metadata/utils/is-gate-and-not-enabled.util'; + +@Injectable() +export class StandardFieldFactory { + create( + target: object, + context: WorkspaceSyncContext, + workspaceFeatureFlagsMap: FeatureFlagMap, + ): (PartialFieldMetadata | PartialComputedFieldMetadata)[] { + const reflectObjectMetadata = TypedReflect.getMetadata( + 'objectMetadata', + target, + ); + const reflectFieldMetadataMap = + TypedReflect.getMetadata('fieldMetadataMap', target) ?? []; + const reflectDynamicRelationFieldMetadataMap = TypedReflect.getMetadata( + 'dynamicRelationFieldMetadataMap', + target, + ); + const partialFieldMetadataCollection: ( + | PartialFieldMetadata + | PartialComputedFieldMetadata + )[] = Object.values(reflectFieldMetadataMap) + .map((reflectFieldMetadata) => + this.createFieldMetadata( + reflectObjectMetadata, + reflectFieldMetadata, + context, + workspaceFeatureFlagsMap, + ), + ) + .filter((metadata): metadata is PartialFieldMetadata => !!metadata); + const partialComputedFieldMetadata = this.createComputedFieldMetadata( + reflectDynamicRelationFieldMetadataMap, + context, + workspaceFeatureFlagsMap, + ); + + if (partialComputedFieldMetadata) { + partialFieldMetadataCollection.push(partialComputedFieldMetadata); + } + + return partialFieldMetadataCollection; + } + + private createFieldMetadata( + reflectObjectMetadata: ReflectObjectMetadata | undefined, + reflectFieldMetadata: ReflectFieldMetadata[string], + context: WorkspaceSyncContext, + workspaceFeatureFlagsMap: FeatureFlagMap, + ): PartialFieldMetadata | undefined { + if ( + isGatedAndNotEnabled(reflectFieldMetadata.gate, workspaceFeatureFlagsMap) + ) { + return undefined; + } + + return { + ...reflectFieldMetadata, + workspaceId: context.workspaceId, + isSystem: + reflectObjectMetadata?.isSystem || reflectFieldMetadata.isSystem, + }; + } + + private createComputedFieldMetadata( + reflectDynamicRelationFieldMetadata: + | ReflectDynamicRelationFieldMetadata + | undefined, + context: WorkspaceSyncContext, + workspaceFeatureFlagsMap: FeatureFlagMap, + ): PartialComputedFieldMetadata | undefined { + if ( + !reflectDynamicRelationFieldMetadata || + isGatedAndNotEnabled( + reflectDynamicRelationFieldMetadata.gate, + workspaceFeatureFlagsMap, + ) + ) { + return undefined; + } + + return { + ...reflectDynamicRelationFieldMetadata, + workspaceId: context.workspaceId, + isSystem: reflectDynamicRelationFieldMetadata.isSystem, + }; + } +} diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/factories/standard-object.factory.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/factories/standard-object.factory.ts index f1b1b5825a..87b2a2e6c1 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/factories/standard-object.factory.ts +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/factories/standard-object.factory.ts @@ -3,20 +3,23 @@ import { Injectable } from '@nestjs/common'; import { WorkspaceSyncContext } from 'src/workspace/workspace-sync-metadata/interfaces/workspace-sync-context.interface'; import { PartialObjectMetadata } from 'src/workspace/workspace-sync-metadata/interfaces/partial-object-metadata.interface'; import { FeatureFlagMap } from 'src/core/feature-flag/interfaces/feature-flag-map.interface'; -import { PartialFieldMetadata } from 'src/workspace/workspace-sync-metadata/interfaces/partial-field-metadata.interface'; import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata'; -import { standardObjectMetadataCollection } from 'src/workspace/workspace-sync-metadata/standard-objects'; import { TypedReflect } from 'src/utils/typed-reflect'; import { isGatedAndNotEnabled } from 'src/workspace/workspace-sync-metadata/utils/is-gate-and-not-enabled.util'; +import { StandardFieldFactory } from './standard-field.factory'; + @Injectable() export class StandardObjectFactory { + constructor(private readonly standardFieldFactory: StandardFieldFactory) {} + create( + standardObjectMetadataDefinitions: (typeof BaseObjectMetadata)[], context: WorkspaceSyncContext, workspaceFeatureFlagsMap: FeatureFlagMap, ): PartialObjectMetadata[] { - return standardObjectMetadataCollection + return standardObjectMetadataDefinitions .map((metadata) => this.createObjectMetadata(metadata, context, workspaceFeatureFlagsMap), ) @@ -29,8 +32,6 @@ export class StandardObjectFactory { workspaceFeatureFlagsMap: FeatureFlagMap, ): PartialObjectMetadata | undefined { const objectMetadata = TypedReflect.getMetadata('objectMetadata', metadata); - const fieldMetadataMap = - TypedReflect.getMetadata('fieldMetadataMap', metadata) ?? []; if (!objectMetadata) { throw new Error( @@ -42,22 +43,10 @@ export class StandardObjectFactory { return undefined; } - const fields = Object.values(fieldMetadataMap).reduce( - // Omit gate as we don't want to store it in the DB - (acc, { gate, ...fieldMetadata }) => { - if (isGatedAndNotEnabled(gate, workspaceFeatureFlagsMap)) { - return acc; - } - - acc.push({ - ...fieldMetadata, - workspaceId: context.workspaceId, - isSystem: objectMetadata.isSystem || fieldMetadata.isSystem, - }); - - return acc; - }, - [] as PartialFieldMetadata[], + const fields = this.standardFieldFactory.create( + metadata, + context, + workspaceFeatureFlagsMap, ); return { diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/factories/standard-relation.factory.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/factories/standard-relation.factory.ts index a18adacd0e..bb97cdde1a 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/factories/standard-relation.factory.ts +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/factories/standard-relation.factory.ts @@ -4,42 +4,80 @@ import { WorkspaceSyncContext } from 'src/workspace/workspace-sync-metadata/inte import { FeatureFlagMap } from 'src/core/feature-flag/interfaces/feature-flag-map.interface'; import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata'; -import { standardObjectMetadataCollection } from 'src/workspace/workspace-sync-metadata/standard-objects'; import { TypedReflect } from 'src/utils/typed-reflect'; import { isGatedAndNotEnabled } from 'src/workspace/workspace-sync-metadata/utils/is-gate-and-not-enabled.util'; import { assert } from 'src/utils/assert'; import { RelationMetadataEntity } from 'src/metadata/relation-metadata/relation-metadata.entity'; import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity'; +import { convertClassNameToObjectMetadataName } from 'src/workspace/workspace-sync-metadata/utils/convert-class-to-object-metadata-name.util'; + +interface CustomRelationFactory { + object: ObjectMetadataEntity; + metadata: typeof BaseObjectMetadata; +} @Injectable() export class StandardRelationFactory { create( + customObjectFactories: CustomRelationFactory[], + context: WorkspaceSyncContext, + originalObjectMetadataMap: Record, + workspaceFeatureFlagsMap: FeatureFlagMap, + ): Partial[]; + + create( + standardObjectMetadataDefinitions: (typeof BaseObjectMetadata)[], + context: WorkspaceSyncContext, + originalObjectMetadataMap: Record, + workspaceFeatureFlagsMap: FeatureFlagMap, + ): Partial[]; + + create( + standardObjectMetadataDefinitionsOrCustomObjectFactories: + | (typeof BaseObjectMetadata)[] + | { + object: ObjectMetadataEntity; + metadata: typeof BaseObjectMetadata; + }[], context: WorkspaceSyncContext, originalObjectMetadataMap: Record, workspaceFeatureFlagsMap: FeatureFlagMap, ): Partial[] { - return standardObjectMetadataCollection.flatMap((standardObjectMetadata) => - this.createRelationMetadata( - standardObjectMetadata, - context, - originalObjectMetadataMap, - workspaceFeatureFlagsMap, - ), + return standardObjectMetadataDefinitionsOrCustomObjectFactories.flatMap( + ( + standardObjectMetadata: + | typeof BaseObjectMetadata + | CustomRelationFactory, + ) => + this.createRelationMetadata( + standardObjectMetadata, + context, + originalObjectMetadataMap, + workspaceFeatureFlagsMap, + ), ); } private createRelationMetadata( - standardObjectMetadata: typeof BaseObjectMetadata, + standardObjectMetadataOrCustomRelationFactory: + | typeof BaseObjectMetadata + | CustomRelationFactory, context: WorkspaceSyncContext, originalObjectMetadataMap: Record, workspaceFeatureFlagsMap: FeatureFlagMap, ): Partial[] { + const standardObjectMetadata = + 'metadata' in standardObjectMetadataOrCustomRelationFactory + ? standardObjectMetadataOrCustomRelationFactory.metadata + : standardObjectMetadataOrCustomRelationFactory; const objectMetadata = TypedReflect.getMetadata( - 'objectMetadata', + 'metadata' in standardObjectMetadataOrCustomRelationFactory + ? 'extendObjectMetadata' + : 'objectMetadata', standardObjectMetadata, ); - const relationMetadataCollection = TypedReflect.getMetadata( - 'relationMetadataCollection', + const reflectRelationMetadataCollection = TypedReflect.getMetadata( + 'reflectRelationMetadataCollection', standardObjectMetadata, ); @@ -50,67 +88,81 @@ export class StandardRelationFactory { } if ( - !relationMetadataCollection || - isGatedAndNotEnabled(objectMetadata.gate, workspaceFeatureFlagsMap) + !reflectRelationMetadataCollection || + isGatedAndNotEnabled(objectMetadata?.gate, workspaceFeatureFlagsMap) ) { return []; } - return relationMetadataCollection + return reflectRelationMetadataCollection .filter( - (relationMetadata) => + (reflectRelationMetadata) => !isGatedAndNotEnabled( - relationMetadata.gate, + reflectRelationMetadata.gate, workspaceFeatureFlagsMap, ), ) - .map((relationMetadata) => { + .map((reflectRelationMetadata) => { + // Compute reflect relation metadata + const fromObjectNameSingular = + 'object' in standardObjectMetadataOrCustomRelationFactory + ? standardObjectMetadataOrCustomRelationFactory.object.nameSingular + : convertClassNameToObjectMetadataName( + reflectRelationMetadata.target.constructor.name, + ); + const toObjectNameSingular = convertClassNameToObjectMetadataName( + reflectRelationMetadata.inverseSideTarget().name, + ); + const fromFieldMetadataName = reflectRelationMetadata.fieldKey; + const toFieldMetadataName = + (reflectRelationMetadata.inverseSideFieldKey as string | undefined) ?? + fromObjectNameSingular; const fromObjectMetadata = - originalObjectMetadataMap[relationMetadata.fromObjectNameSingular]; + originalObjectMetadataMap[fromObjectNameSingular]; assert( fromObjectMetadata, - `Object ${relationMetadata.fromObjectNameSingular} not found in DB - for relation FROM defined in class ${objectMetadata.nameSingular}`, + `Object ${fromObjectNameSingular} not found in DB + for relation FROM defined in class ${fromObjectNameSingular}`, ); const toObjectMetadata = - originalObjectMetadataMap[relationMetadata.toObjectNameSingular]; + originalObjectMetadataMap[toObjectNameSingular]; assert( toObjectMetadata, - `Object ${relationMetadata.toObjectNameSingular} not found in DB - for relation TO defined in class ${objectMetadata.nameSingular}`, + `Object ${toObjectNameSingular} not found in DB + for relation TO defined in class ${fromObjectNameSingular}`, ); const fromFieldMetadata = fromObjectMetadata?.fields.find( - (field) => field.name === relationMetadata.fromFieldMetadataName, + (field) => field.name === fromFieldMetadataName, ); assert( fromFieldMetadata, - `Field ${relationMetadata.fromFieldMetadataName} not found in object ${relationMetadata.fromObjectNameSingular} - for relation FROM defined in class ${objectMetadata.nameSingular}`, + `Field ${fromFieldMetadataName} not found in object ${fromObjectNameSingular} + for relation FROM defined in class ${fromObjectNameSingular}`, ); const toFieldMetadata = toObjectMetadata?.fields.find( - (field) => field.name === relationMetadata.toFieldMetadataName, + (field) => field.name === toFieldMetadataName, ); assert( toFieldMetadata, - `Field ${relationMetadata.toFieldMetadataName} not found in object ${relationMetadata.toObjectNameSingular} - for relation TO defined in class ${objectMetadata.nameSingular}`, + `Field ${toFieldMetadataName} not found in object ${toObjectNameSingular} + for relation TO defined in class ${fromObjectNameSingular}`, ); return { - relationType: relationMetadata.type, + relationType: reflectRelationMetadata.type, fromObjectMetadataId: fromObjectMetadata?.id, toObjectMetadataId: toObjectMetadata?.id, fromFieldMetadataId: fromFieldMetadata?.id, toFieldMetadataId: toFieldMetadata?.id, workspaceId: context.workspaceId, - onDeleteAction: relationMetadata.onDelete, + onDeleteAction: reflectRelationMetadata.onDelete, }; }); } diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/interfaces/comparator.interface.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/interfaces/comparator.interface.ts index 8b4c722c27..b60b9996d6 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/interfaces/comparator.interface.ts +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/interfaces/comparator.interface.ts @@ -1,8 +1,8 @@ import { FieldMetadataEntity } from 'src/metadata/field-metadata/field-metadata.entity'; import { RelationMetadataEntity } from 'src/metadata/relation-metadata/relation-metadata.entity'; -import { PartialFieldMetadata } from './partial-field-metadata.interface'; -import { PartialObjectMetadata } from './partial-object-metadata.interface'; +import { ComputedPartialFieldMetadata } from './partial-field-metadata.interface'; +import { ComputedPartialObjectMetadata } from './partial-object-metadata.interface'; export const enum ComparatorAction { SKIP = 'SKIP', @@ -32,13 +32,15 @@ export interface ComparatorDeleteResult { export type ObjectComparatorResult = | ComparatorSkipResult - | ComparatorCreateResult - | ComparatorUpdateResult>; + | ComparatorCreateResult + | ComparatorUpdateResult>; export type FieldComparatorResult = | ComparatorSkipResult - | ComparatorCreateResult - | ComparatorUpdateResult & { id: string }> + | ComparatorCreateResult + | ComparatorUpdateResult< + Partial & { id: string } + > | ComparatorDeleteResult; export type RelationComparatorResult = diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/interfaces/partial-field-metadata.interface.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/interfaces/partial-field-metadata.interface.ts index 6e21f7c184..fa90f9275e 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/interfaces/partial-field-metadata.interface.ts +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/interfaces/partial-field-metadata.interface.ts @@ -1,6 +1,20 @@ +import { ReflectDynamicRelationFieldMetadata } from 'src/workspace/workspace-sync-metadata/interfaces/reflect-computed-relation-field-metadata.interface'; import { ReflectFieldMetadata } from 'src/workspace/workspace-sync-metadata/interfaces/reflect-field-metadata.interface'; -export type PartialFieldMetadata = ReflectFieldMetadata[string] & { +export type PartialFieldMetadata = Omit< + ReflectFieldMetadata[string], + 'joinColumn' +> & { workspaceId: string; objectMetadataId?: string; }; + +export type PartialComputedFieldMetadata = + ReflectDynamicRelationFieldMetadata & { + workspaceId: string; + objectMetadataId?: string; + }; + +export type ComputedPartialFieldMetadata = { + [K in keyof PartialFieldMetadata]: ExcludeFunctions; +}; diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/interfaces/partial-object-metadata.interface.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/interfaces/partial-object-metadata.interface.ts index 1080b34cf7..e535931028 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/interfaces/partial-object-metadata.interface.ts +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/interfaces/partial-object-metadata.interface.ts @@ -1,9 +1,20 @@ -import { PartialFieldMetadata } from 'src/workspace/workspace-sync-metadata/interfaces/partial-field-metadata.interface'; +import { + ComputedPartialFieldMetadata, + PartialComputedFieldMetadata, + PartialFieldMetadata, +} from 'src/workspace/workspace-sync-metadata/interfaces/partial-field-metadata.interface'; import { ReflectObjectMetadata } from 'src/workspace/workspace-sync-metadata/interfaces/reflect-object-metadata.interface'; export type PartialObjectMetadata = ReflectObjectMetadata & { id?: string; workspaceId: string; dataSourceId: string; - fields: PartialFieldMetadata[]; + fields: (PartialFieldMetadata | PartialComputedFieldMetadata)[]; +}; + +export type ComputedPartialObjectMetadata = Omit< + PartialObjectMetadata, + 'fields' +> & { + fields: ComputedPartialFieldMetadata[]; }; diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/interfaces/reflect-computed-relation-field-metadata.interface.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/interfaces/reflect-computed-relation-field-metadata.interface.ts new file mode 100644 index 0000000000..14fa484678 --- /dev/null +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/interfaces/reflect-computed-relation-field-metadata.interface.ts @@ -0,0 +1,23 @@ +import { GateDecoratorParams } from 'src/workspace/workspace-sync-metadata/interfaces/gate-decorator.interface'; + +import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; +import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity'; + +export type DynamicRelationFieldMetadataDecoratorParams = ( + oppositeObjectMetadata: ObjectMetadataEntity, +) => { + name: string; + label: string; + joinColumn: string; + description?: string; + icon?: string; +}; + +export interface ReflectDynamicRelationFieldMetadata { + type: FieldMetadataType.RELATION; + paramsFactory: DynamicRelationFieldMetadataDecoratorParams; + isNullable: boolean; + isSystem: boolean; + isCustom: boolean; + gate?: GateDecoratorParams; +} diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/interfaces/reflect-custom-object-metadata.interface.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/interfaces/reflect-custom-object-metadata.interface.ts new file mode 100644 index 0000000000..2e000bfa94 --- /dev/null +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/interfaces/reflect-custom-object-metadata.interface.ts @@ -0,0 +1,10 @@ +import { GateDecoratorParams } from 'src/workspace/workspace-sync-metadata/interfaces/gate-decorator.interface'; + +export type BaseCustomObjectMetadataDecoratorParams = + | { allowObjectNameList?: string[] } + | { denyObjectNameList?: string[] }; + +export type ReflectBaseCustomObjectMetadata = + BaseCustomObjectMetadataDecoratorParams & { + gate?: GateDecoratorParams; + }; diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/interfaces/reflect-field-metadata.interface.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/interfaces/reflect-field-metadata.interface.ts index 873ed5b75e..6b21f65b99 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/interfaces/reflect-field-metadata.interface.ts +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/interfaces/reflect-field-metadata.interface.ts @@ -4,13 +4,14 @@ import { FieldMetadataOptions } from 'src/metadata/field-metadata/interfaces/fie import { FieldMetadataTargetColumnMap } from 'src/metadata/field-metadata/interfaces/field-metadata-target-column-map.interface'; import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; +import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity'; export interface FieldMetadataDecoratorParams< T extends FieldMetadataType | 'default', > { type: T; - label: string; - description?: string; + label: string | ((objectMetadata: ObjectMetadataEntity) => string); + description?: string | ((objectMetadata: ObjectMetadataEntity) => string); icon?: string; defaultValue?: FieldMetadataDefaultValue; joinColumn?: string; @@ -28,7 +29,6 @@ export interface ReflectFieldMetadata { isNullable: boolean; isSystem: boolean; isCustom: boolean; - description?: string; defaultValue: FieldMetadataDefaultValue<'default'> | null; gate?: GateDecoratorParams; options?: FieldMetadataOptions<'default'> | null; diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/interfaces/reflect-relation-metadata.interface.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/interfaces/reflect-relation-metadata.interface.ts index 06393424f4..5e67f14a52 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/interfaces/reflect-relation-metadata.interface.ts +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/interfaces/reflect-relation-metadata.interface.ts @@ -1,3 +1,5 @@ +import { ObjectType } from 'typeorm'; + import { GateDecoratorParams } from 'src/workspace/workspace-sync-metadata/interfaces/gate-decorator.interface'; import { @@ -5,19 +7,17 @@ import { RelationMetadataType, } from 'src/metadata/relation-metadata/relation-metadata.entity'; -export interface RelationMetadataDecoratorParams { +export interface RelationMetadataDecoratorParams { type: RelationMetadataType; - objectName: string; - inverseSideFieldName?: string; + inverseSideTarget: () => ObjectType; + inverseSideFieldKey?: keyof T; onDelete?: RelationOnDeleteAction; } -export interface ReflectRelationMetadata { - type: RelationMetadataType; - fromObjectNameSingular: string; - toObjectNameSingular: string; - fromFieldMetadataName: string; - toFieldMetadataName: string; +export interface ReflectRelationMetadata + extends RelationMetadataDecoratorParams { + target: object; + fieldKey: string; gate?: GateDecoratorParams; onDelete: RelationOnDeleteAction; } diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/services/workspace-metadata-updater.service.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/services/workspace-metadata-updater.service.ts index cc1620164d..569d723330 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/services/workspace-metadata-updater.service.ts +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/services/workspace-metadata-updater.service.ts @@ -153,8 +153,8 @@ export class WorkspaceMetadataUpdaterService { // https://github.com/typeorm/typeorm/issues/3490 // To avoid calling update in a for loop, we did this hack. return { - ...oldFieldMetadata, - ...updateFieldMetadata, + ...omit(oldFieldMetadata, ['objectMetadataId', 'workspaceId']), + ...omit(updateFieldMetadata, ['objectMetadataId', 'workspaceId']), options: updateFieldMetadata.options ?? oldFieldMetadata.options, }; }); @@ -197,7 +197,11 @@ export class WorkspaceMetadataUpdaterService { return { current: oldFieldMetadata as FieldMetadataEntity, - altered: alteredFieldMetadata as FieldMetadataEntity, + altered: { + ...alteredFieldMetadata, + objectMetadataId: oldFieldMetadata.objectMetadataId, + workspaceId: oldFieldMetadata.workspaceId, + } as FieldMetadataEntity, }; }, ), diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/services/workspace-sync-field-metadata.service.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/services/workspace-sync-field-metadata.service.ts new file mode 100644 index 0000000000..68f7dd4e1a --- /dev/null +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/services/workspace-sync-field-metadata.service.ts @@ -0,0 +1,140 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import { EntityManager } from 'typeorm'; + +import { WorkspaceSyncContext } from 'src/workspace/workspace-sync-metadata/interfaces/workspace-sync-context.interface'; +import { ComparatorAction } from 'src/workspace/workspace-sync-metadata/interfaces/comparator.interface'; +import { FeatureFlagMap } from 'src/core/feature-flag/interfaces/feature-flag-map.interface'; +import { WorkspaceMigrationBuilderAction } from 'src/workspace/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface'; + +import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity'; +import { WorkspaceMigrationEntity } from 'src/metadata/workspace-migration/workspace-migration.entity'; +import { WorkspaceFieldComparator } from 'src/workspace/workspace-sync-metadata/comparators/workspace-field.comparator'; +import { WorkspaceMetadataUpdaterService } from 'src/workspace/workspace-sync-metadata/services/workspace-metadata-updater.service'; +import { WorkspaceSyncStorage } from 'src/workspace/workspace-sync-metadata/storage/workspace-sync.storage'; +import { WorkspaceMigrationFieldFactory } from 'src/workspace/workspace-migration-builder/factories/workspace-migration-field.factory'; +import { StandardFieldFactory } from 'src/workspace/workspace-sync-metadata/factories/standard-field.factory'; +import { CustomObjectMetadata } from 'src/workspace/workspace-sync-metadata/custom-objects/custom.object-metadata'; +import { computeStandardObject } from 'src/workspace/workspace-sync-metadata/utils/compute-standard-object.util'; + +@Injectable() +export class WorkspaceSyncFieldMetadataService { + private readonly logger = new Logger(WorkspaceSyncFieldMetadataService.name); + + constructor( + private readonly standardFieldFactory: StandardFieldFactory, + private readonly workspaceFieldComparator: WorkspaceFieldComparator, + private readonly workspaceMetadataUpdaterService: WorkspaceMetadataUpdaterService, + private readonly workspaceMigrationFieldFactory: WorkspaceMigrationFieldFactory, + ) {} + + async synchronize( + context: WorkspaceSyncContext, + manager: EntityManager, + storage: WorkspaceSyncStorage, + workspaceFeatureFlagsMap: FeatureFlagMap, + ): Promise[]> { + const objectMetadataRepository = + manager.getRepository(ObjectMetadataEntity); + + // Retrieve object metadata collection from DB + const originalObjectMetadataCollection = + await objectMetadataRepository.find({ + where: { + workspaceId: context.workspaceId, + // We're only interested in standard fields + fields: { isCustom: false }, + }, + relations: ['dataSource', 'fields'], + }); + + // Filter out custom objects + const customObjectMetadataCollection = + originalObjectMetadataCollection.filter( + (objectMetadata) => objectMetadata.isCustom, + ); + + // Create standard field metadata collection + const standardFieldMetadataCollection = this.standardFieldFactory.create( + CustomObjectMetadata, + context, + workspaceFeatureFlagsMap, + ); + + // Loop over all standard objects and compare them with the objects in DB + for (const customObjectMetadata of customObjectMetadataCollection) { + // Also, maybe it's better to refactor a bit and move generation part into a separate module ? + const standardObjectMetadata = computeStandardObject( + { + ...customObjectMetadata, + fields: standardFieldMetadataCollection, + }, + customObjectMetadata, + ); + + /** + * COMPARE FIELD METADATA + */ + const fieldComparatorResults = this.workspaceFieldComparator.compare( + customObjectMetadata, + standardObjectMetadata, + ); + + for (const fieldComparatorResult of fieldComparatorResults) { + switch (fieldComparatorResult.action) { + case ComparatorAction.CREATE: { + storage.addCreateFieldMetadata(fieldComparatorResult.object); + break; + } + case ComparatorAction.UPDATE: { + storage.addUpdateFieldMetadata(fieldComparatorResult.object); + break; + } + case ComparatorAction.DELETE: { + storage.addDeleteFieldMetadata(fieldComparatorResult.object); + break; + } + } + } + } + + this.logger.log('Updating workspace metadata'); + + const metadataFieldUpdaterResult = + await this.workspaceMetadataUpdaterService.updateFieldMetadata( + manager, + storage, + ); + + this.logger.log('Generating migrations'); + + const createFieldWorkspaceMigrations = + await this.workspaceMigrationFieldFactory.create( + originalObjectMetadataCollection, + metadataFieldUpdaterResult.createdFieldMetadataCollection, + WorkspaceMigrationBuilderAction.CREATE, + ); + + const updateFieldWorkspaceMigrations = + await this.workspaceMigrationFieldFactory.create( + originalObjectMetadataCollection, + metadataFieldUpdaterResult.updatedFieldMetadataCollection, + WorkspaceMigrationBuilderAction.UPDATE, + ); + + const deleteFieldWorkspaceMigrations = + await this.workspaceMigrationFieldFactory.create( + originalObjectMetadataCollection, + storage.fieldMetadataDeleteCollection, + WorkspaceMigrationBuilderAction.DELETE, + ); + + this.logger.log('Saving migrations'); + + return [ + ...createFieldWorkspaceMigrations, + ...updateFieldWorkspaceMigrations, + ...deleteFieldWorkspaceMigrations, + ]; + } +} diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/services/workspace-sync-object-metadata.service.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/services/workspace-sync-object-metadata.service.ts index 43c3b2e428..685029858b 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/services/workspace-sync-object-metadata.service.ts +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/services/workspace-sync-object-metadata.service.ts @@ -16,7 +16,8 @@ import { WorkspaceFieldComparator } from 'src/workspace/workspace-sync-metadata/ import { WorkspaceMetadataUpdaterService } from 'src/workspace/workspace-sync-metadata/services/workspace-metadata-updater.service'; import { WorkspaceSyncStorage } from 'src/workspace/workspace-sync-metadata/storage/workspace-sync.storage'; import { WorkspaceMigrationObjectFactory } from 'src/workspace/workspace-migration-builder/factories/workspace-migration-object.factory'; -import { WorkspaceMigrationFieldFactory } from 'src/workspace/workspace-migration-builder/factories/workspace-migration-field.factory'; +import { computeStandardObject } from 'src/workspace/workspace-sync-metadata/utils/compute-standard-object.util'; +import { standardObjectMetadataDefinitions } from 'src/workspace/workspace-sync-metadata/standard-objects'; @Injectable() export class WorkspaceSyncObjectMetadataService { @@ -28,7 +29,6 @@ export class WorkspaceSyncObjectMetadataService { private readonly workspaceFieldComparator: WorkspaceFieldComparator, private readonly workspaceMetadataUpdaterService: WorkspaceMetadataUpdaterService, private readonly workspaceMigrationObjectFactory: WorkspaceMigrationObjectFactory, - private readonly workspaceMigrationFieldFactory: WorkspaceMigrationFieldFactory, ) {} async synchronize( @@ -45,14 +45,18 @@ export class WorkspaceSyncObjectMetadataService { await objectMetadataRepository.find({ where: { workspaceId: context.workspaceId, - isCustom: false, fields: { isCustom: false }, }, relations: ['dataSource', 'fields'], }); + const customObjectMetadataCollection = + originalObjectMetadataCollection.filter( + (objectMetadata) => objectMetadata.isCustom, + ); // Create standard object metadata collection const standardObjectMetadataCollection = this.standardObjectFactory.create( + standardObjectMetadataDefinitions, context, workspaceFeatureFlagsMap, ); @@ -68,7 +72,9 @@ export class WorkspaceSyncObjectMetadataService { this.logger.log('Comparing standard objects and fields metadata'); // Store object that need to be deleted - for (const originalObjectMetadata of originalObjectMetadataCollection) { + for (const originalObjectMetadata of originalObjectMetadataCollection.filter( + (object) => !object.isCustom, + )) { if (!standardObjectMetadataMap[originalObjectMetadata.nameSingular]) { storage.addDeleteObjectMetadata(originalObjectMetadata); } @@ -78,8 +84,11 @@ export class WorkspaceSyncObjectMetadataService { for (const standardObjectName in standardObjectMetadataMap) { const originalObjectMetadata = originalObjectMetadataMap[standardObjectName]; - const standardObjectMetadata = - standardObjectMetadataMap[standardObjectName]; + const standardObjectMetadata = computeStandardObject( + standardObjectMetadataMap[standardObjectName], + originalObjectMetadata, + customObjectMetadataCollection, + ); /** * COMPARE OBJECT METADATA @@ -132,11 +141,6 @@ export class WorkspaceSyncObjectMetadataService { manager, storage, ); - const metadataFieldUpdaterResult = - await this.workspaceMetadataUpdaterService.updateFieldMetadata( - manager, - storage, - ); this.logger.log('Generating migrations'); @@ -153,35 +157,11 @@ export class WorkspaceSyncObjectMetadataService { WorkspaceMigrationBuilderAction.DELETE, ); - const createFieldWorkspaceMigrations = - await this.workspaceMigrationFieldFactory.create( - originalObjectMetadataCollection, - metadataFieldUpdaterResult.createdFieldMetadataCollection, - WorkspaceMigrationBuilderAction.CREATE, - ); - - const updateFieldWorkspaceMigrations = - await this.workspaceMigrationFieldFactory.create( - originalObjectMetadataCollection, - metadataFieldUpdaterResult.updatedFieldMetadataCollection, - WorkspaceMigrationBuilderAction.UPDATE, - ); - - const deleteFieldWorkspaceMigrations = - await this.workspaceMigrationFieldFactory.create( - originalObjectMetadataCollection, - storage.fieldMetadataDeleteCollection, - WorkspaceMigrationBuilderAction.DELETE, - ); - this.logger.log('Saving migrations'); return [ ...createObjectWorkspaceMigrations, ...deleteObjectWorkspaceMigrations, - ...createFieldWorkspaceMigrations, - ...updateFieldWorkspaceMigrations, - ...deleteFieldWorkspaceMigrations, ]; } } diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/services/workspace-sync-relation-metadata.service.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/services/workspace-sync-relation-metadata.service.ts index 171678c97e..e85bfbe673 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/services/workspace-sync-relation-metadata.service.ts +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/services/workspace-sync-relation-metadata.service.ts @@ -16,6 +16,8 @@ import { WorkspaceMetadataUpdaterService } from 'src/workspace/workspace-sync-me import { WorkspaceMigrationEntity } from 'src/metadata/workspace-migration/workspace-migration.entity'; import { WorkspaceSyncStorage } from 'src/workspace/workspace-sync-metadata/storage/workspace-sync.storage'; import { WorkspaceMigrationRelationFactory } from 'src/workspace/workspace-migration-builder/factories/workspace-migration-relation.factory'; +import { standardObjectMetadataDefinitions } from 'src/workspace/workspace-sync-metadata/standard-objects'; +import { CustomObjectMetadata } from 'src/workspace/workspace-sync-metadata/custom-objects/custom.object-metadata'; @Injectable() export class WorkspaceSyncRelationMetadataService { @@ -44,11 +46,14 @@ export class WorkspaceSyncRelationMetadataService { await objectMetadataRepository.find({ where: { workspaceId: context.workspaceId, - isCustom: false, fields: { isCustom: false }, }, relations: ['dataSource', 'fields'], }); + const customObjectMetadataCollection = + originalObjectMetadataCollection.filter( + (objectMetadata) => objectMetadata.isCustom, + ); // Create map of object metadata & field metadata by unique identifier const originalObjectMetadataMap = mapObjectMetadataByUniqueIdentifier( @@ -71,6 +76,18 @@ export class WorkspaceSyncRelationMetadataService { // Create standard relation metadata collection const standardRelationMetadataCollection = this.standardRelationFactory.create( + standardObjectMetadataDefinitions, + context, + originalObjectMetadataMap, + workspaceFeatureFlagsMap, + ); + + const customRelationMetadataCollection = + this.standardRelationFactory.create( + customObjectMetadataCollection.map((objectMetadata) => ({ + object: objectMetadata, + metadata: CustomObjectMetadata, + })), context, originalObjectMetadataMap, workspaceFeatureFlagsMap, @@ -78,7 +95,10 @@ export class WorkspaceSyncRelationMetadataService { const relationComparatorResults = this.workspaceRelationComparator.compare( originalRelationMetadataCollection, - standardRelationMetadataCollection, + [ + ...standardRelationMetadataCollection, + ...customRelationMetadataCollection, + ], ); for (const relationComparatorResult of relationComparatorResults) { diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/activity-target.object-metadata.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/activity-target.object-metadata.ts index c8b7a72ae0..531989bde0 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/activity-target.object-metadata.ts +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/activity-target.object-metadata.ts @@ -1,4 +1,6 @@ import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; +import { CustomObjectMetadata } from 'src/workspace/workspace-sync-metadata/custom-objects/custom.object-metadata'; +import { DynamicRelationFieldMetadata } from 'src/workspace/workspace-sync-metadata/decorators/dynamic-field-metadata.interface'; import { FieldMetadata } from 'src/workspace/workspace-sync-metadata/decorators/field-metadata.decorator'; import { IsNullable } from 'src/workspace/workspace-sync-metadata/decorators/is-nullable.decorator'; import { IsSystem } from 'src/workspace/workspace-sync-metadata/decorators/is-system.decorator'; @@ -57,4 +59,13 @@ export class ActivityTargetObjectMetadata extends BaseObjectMetadata { }) @IsNullable() opportunity: OpportunityObjectMetadata; + + @DynamicRelationFieldMetadata((oppositeObjectMetadata) => ({ + name: oppositeObjectMetadata.nameSingular, + label: oppositeObjectMetadata.labelSingular, + description: `ActivityTarget ${oppositeObjectMetadata.labelSingular}`, + joinColumn: `${oppositeObjectMetadata.nameSingular}Id`, + icon: 'IconBuildingSkyscraper', + })) + custom: CustomObjectMetadata; } diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/activity.object-metadata.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/activity.object-metadata.ts index df001c73a4..56ec7f22fc 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/activity.object-metadata.ts +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/activity.object-metadata.ts @@ -80,7 +80,7 @@ export class ActivityObjectMetadata extends BaseObjectMetadata { }) @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, - objectName: 'activityTarget', + inverseSideTarget: () => ActivityTargetObjectMetadata, }) @IsNullable() activityTargets: ActivityTargetObjectMetadata[]; @@ -93,7 +93,7 @@ export class ActivityObjectMetadata extends BaseObjectMetadata { }) @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, - objectName: 'attachment', + inverseSideTarget: () => AttachmentObjectMetadata, }) @IsNullable() attachments: AttachmentObjectMetadata[]; @@ -106,7 +106,7 @@ export class ActivityObjectMetadata extends BaseObjectMetadata { }) @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, - objectName: 'comment', + inverseSideTarget: () => CommentObjectMetadata, }) @IsNullable() comments: CommentObjectMetadata[]; @@ -123,7 +123,7 @@ export class ActivityObjectMetadata extends BaseObjectMetadata { @FieldMetadata({ type: FieldMetadataType.RELATION, label: 'Assignee', - description: 'Acitivity assignee', + description: 'Activity assignee', icon: 'IconUserCircle', joinColumn: 'assigneeId', }) diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/attachment.object-metadata.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/attachment.object-metadata.ts index 282c750afc..516566a247 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/attachment.object-metadata.ts +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/attachment.object-metadata.ts @@ -1,4 +1,6 @@ import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; +import { CustomObjectMetadata } from 'src/workspace/workspace-sync-metadata/custom-objects/custom.object-metadata'; +import { DynamicRelationFieldMetadata } from 'src/workspace/workspace-sync-metadata/decorators/dynamic-field-metadata.interface'; import { FieldMetadata } from 'src/workspace/workspace-sync-metadata/decorators/field-metadata.decorator'; import { IsNullable } from 'src/workspace/workspace-sync-metadata/decorators/is-nullable.decorator'; import { IsSystem } from 'src/workspace/workspace-sync-metadata/decorators/is-system.decorator'; @@ -91,4 +93,13 @@ export class AttachmentObjectMetadata extends BaseObjectMetadata { }) @IsNullable() opportunity: OpportunityObjectMetadata; + + @DynamicRelationFieldMetadata((oppositeObjectMetadata) => ({ + name: oppositeObjectMetadata.nameSingular, + label: oppositeObjectMetadata.labelSingular, + description: `Attachment ${oppositeObjectMetadata.labelSingular}`, + joinColumn: `${oppositeObjectMetadata.nameSingular}Id`, + icon: 'IconBuildingSkyscraper', + })) + custom: CustomObjectMetadata; } diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata.ts index bfd8c5fc4e..29b86f3de4 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata.ts +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata.ts @@ -6,7 +6,9 @@ export abstract class BaseObjectMetadata { @FieldMetadata({ type: FieldMetadataType.UUID, label: 'Id', + description: 'Id', defaultValue: { type: 'uuid' }, + icon: 'Icon123', }) @IsSystem() id: string; @@ -14,6 +16,7 @@ export abstract class BaseObjectMetadata { @FieldMetadata({ type: FieldMetadataType.DATE_TIME, label: 'Creation date', + description: 'Creation date', icon: 'IconCalendar', defaultValue: { type: 'now' }, }) @@ -22,6 +25,7 @@ export abstract class BaseObjectMetadata { @FieldMetadata({ type: FieldMetadataType.DATE_TIME, label: 'Update date', + description: 'Update date', icon: 'IconCalendar', defaultValue: { type: 'now' }, }) diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/calendar-event.object-metadata.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/calendar-event.object-metadata.ts index ef45311ecb..7aa7e7af58 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/calendar-event.object-metadata.ts +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/calendar-event.object-metadata.ts @@ -136,7 +136,7 @@ export class CalendarEventObjectMetadata extends BaseObjectMetadata { }) @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, - objectName: 'calendarEventAttendee', + inverseSideTarget: () => CalendarEventAttendeeObjectMetadata, onDelete: RelationOnDeleteAction.CASCADE, }) eventAttendees: CalendarEventAttendeeObjectMetadata[]; diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/company.object-metadata.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/company.object-metadata.ts index dd47505bb7..e789e13161 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/company.object-metadata.ts +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/company.object-metadata.ts @@ -117,7 +117,7 @@ export class CompanyObjectMetadata extends BaseObjectMetadata { }) @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, - objectName: 'person', + inverseSideTarget: () => PersonObjectMetadata, }) @IsNullable() people: PersonObjectMetadata[]; @@ -141,7 +141,7 @@ export class CompanyObjectMetadata extends BaseObjectMetadata { }) @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, - objectName: 'activityTarget', + inverseSideTarget: () => ActivityTargetObjectMetadata, onDelete: RelationOnDeleteAction.CASCADE, }) @IsNullable() @@ -155,7 +155,7 @@ export class CompanyObjectMetadata extends BaseObjectMetadata { }) @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, - objectName: 'opportunity', + inverseSideTarget: () => OpportunityObjectMetadata, }) @IsNullable() opportunities: OpportunityObjectMetadata[]; @@ -168,7 +168,7 @@ export class CompanyObjectMetadata extends BaseObjectMetadata { }) @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, - objectName: 'favorite', + inverseSideTarget: () => FavoriteObjectMetadata, onDelete: RelationOnDeleteAction.CASCADE, }) @IsNullable() @@ -182,7 +182,7 @@ export class CompanyObjectMetadata extends BaseObjectMetadata { }) @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, - objectName: 'attachment', + inverseSideTarget: () => AttachmentObjectMetadata, onDelete: RelationOnDeleteAction.CASCADE, }) @IsNullable() diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/connected-account.object-metadata.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/connected-account.object-metadata.ts index f54870b2e3..661e2549f8 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/connected-account.object-metadata.ts +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/connected-account.object-metadata.ts @@ -80,7 +80,7 @@ export class ConnectedAccountObjectMetadata extends BaseObjectMetadata { }) @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, - objectName: 'messageChannel', + inverseSideTarget: () => MessageChannelObjectMetadata, onDelete: RelationOnDeleteAction.CASCADE, }) messageChannels: MessageChannelObjectMetadata[]; @@ -93,7 +93,7 @@ export class ConnectedAccountObjectMetadata extends BaseObjectMetadata { }) @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, - objectName: 'calendarChannel', + inverseSideTarget: () => CalendarChannelObjectMetadata, onDelete: RelationOnDeleteAction.CASCADE, }) @Gate({ diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/favorite.object-metadata.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/favorite.object-metadata.ts index b7ae721e1b..01fc6af372 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/favorite.object-metadata.ts +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/favorite.object-metadata.ts @@ -1,4 +1,6 @@ import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; +import { CustomObjectMetadata } from 'src/workspace/workspace-sync-metadata/custom-objects/custom.object-metadata'; +import { DynamicRelationFieldMetadata } from 'src/workspace/workspace-sync-metadata/decorators/dynamic-field-metadata.interface'; import { FieldMetadata } from 'src/workspace/workspace-sync-metadata/decorators/field-metadata.decorator'; import { IsNullable } from 'src/workspace/workspace-sync-metadata/decorators/is-nullable.decorator'; import { IsSystem } from 'src/workspace/workspace-sync-metadata/decorators/is-system.decorator'; @@ -66,4 +68,13 @@ export class FavoriteObjectMetadata extends BaseObjectMetadata { }) @IsNullable() opportunity: OpportunityObjectMetadata; + + @DynamicRelationFieldMetadata((oppositeObjectMetadata) => ({ + name: oppositeObjectMetadata.nameSingular, + label: oppositeObjectMetadata.labelSingular, + description: `Favorite ${oppositeObjectMetadata.labelSingular}`, + joinColumn: `${oppositeObjectMetadata.nameSingular}Id`, + icon: 'IconBuildingSkyscraper', + })) + custom: CustomObjectMetadata; } diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/index.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/index.ts index 79120e30a0..6c476298d4 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/index.ts +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/index.ts @@ -25,7 +25,7 @@ import { ViewObjectMetadata } from 'src/workspace/workspace-sync-metadata/standa import { WebhookObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/webhook.object-metadata'; import { WorkspaceMemberObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/workspace-member.object-metadata'; -export const standardObjectMetadataCollection = [ +export const standardObjectMetadataDefinitions = [ ActivityTargetObjectMetadata, ActivityObjectMetadata, ApiKeyObjectMetadata, diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/message-channel.object-metadata.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/message-channel.object-metadata.ts index 224f4340da..0b610a69de 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/message-channel.object-metadata.ts +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/message-channel.object-metadata.ts @@ -87,7 +87,7 @@ export class MessageChannelObjectMetadata extends BaseObjectMetadata { }) @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, - objectName: 'messageChannelMessageAssociation', + inverseSideTarget: () => MessageChannelMessageAssociationObjectMetadata, onDelete: RelationOnDeleteAction.CASCADE, }) @IsNullable() diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/message-thread.object-metadata.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/message-thread.object-metadata.ts index ac8957119a..0dcb58d251 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/message-thread.object-metadata.ts +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/message-thread.object-metadata.ts @@ -29,7 +29,7 @@ export class MessageThreadObjectMetadata extends BaseObjectMetadata { }) @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, - objectName: 'message', + inverseSideTarget: () => MessageObjectMetadata, onDelete: RelationOnDeleteAction.CASCADE, }) @IsNullable() @@ -43,7 +43,7 @@ export class MessageThreadObjectMetadata extends BaseObjectMetadata { }) @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, - objectName: 'messageChannelMessageAssociation', + inverseSideTarget: () => MessageChannelMessageAssociationObjectMetadata, onDelete: RelationOnDeleteAction.RESTRICT, }) @IsNullable() diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/message.object-metadata.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/message.object-metadata.ts index 0d2e337990..012d9bf67b 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/message.object-metadata.ts +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/message.object-metadata.ts @@ -86,8 +86,8 @@ export class MessageObjectMetadata extends BaseObjectMetadata { }) @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, - objectName: 'messageParticipant', - inverseSideFieldName: 'message', + inverseSideTarget: () => MessageParticipantObjectMetadata, + inverseSideFieldKey: 'message', onDelete: RelationOnDeleteAction.CASCADE, }) @IsNullable() @@ -101,7 +101,7 @@ export class MessageObjectMetadata extends BaseObjectMetadata { }) @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, - objectName: 'messageChannelMessageAssociation', + inverseSideTarget: () => MessageChannelMessageAssociationObjectMetadata, onDelete: RelationOnDeleteAction.CASCADE, }) @IsNullable() diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/opportunity.object-metadata.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/opportunity.object-metadata.ts index 8a3928a964..8828553b53 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/opportunity.object-metadata.ts +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/opportunity.object-metadata.ts @@ -130,7 +130,7 @@ export class OpportunityObjectMetadata extends BaseObjectMetadata { }) @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, - objectName: 'favorite', + inverseSideTarget: () => FavoriteObjectMetadata, onDelete: RelationOnDeleteAction.CASCADE, }) @IsNullable() @@ -144,7 +144,7 @@ export class OpportunityObjectMetadata extends BaseObjectMetadata { }) @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, - objectName: 'activityTarget', + inverseSideTarget: () => ActivityTargetObjectMetadata, onDelete: RelationOnDeleteAction.CASCADE, }) @IsNullable() @@ -158,7 +158,7 @@ export class OpportunityObjectMetadata extends BaseObjectMetadata { }) @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, - objectName: 'attachment', + inverseSideTarget: () => AttachmentObjectMetadata, }) @IsNullable() attachments: AttachmentObjectMetadata[]; diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/person.object-metadata.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/person.object-metadata.ts index 5d0a0d3a16..30a9f018d6 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/person.object-metadata.ts +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/person.object-metadata.ts @@ -124,8 +124,8 @@ export class PersonObjectMetadata extends BaseObjectMetadata { }) @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, - objectName: 'opportunity', - inverseSideFieldName: 'pointOfContact', + inverseSideTarget: () => OpportunityObjectMetadata, + inverseSideFieldKey: 'pointOfContact', }) pointOfContactForOpportunities: OpportunityObjectMetadata[]; @@ -137,7 +137,7 @@ export class PersonObjectMetadata extends BaseObjectMetadata { }) @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, - objectName: 'activityTarget', + inverseSideTarget: () => ActivityTargetObjectMetadata, onDelete: RelationOnDeleteAction.CASCADE, }) activityTargets: ActivityTargetObjectMetadata[]; @@ -150,7 +150,7 @@ export class PersonObjectMetadata extends BaseObjectMetadata { }) @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, - objectName: 'favorite', + inverseSideTarget: () => FavoriteObjectMetadata, onDelete: RelationOnDeleteAction.CASCADE, }) favorites: FavoriteObjectMetadata[]; @@ -163,7 +163,7 @@ export class PersonObjectMetadata extends BaseObjectMetadata { }) @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, - objectName: 'attachment', + inverseSideTarget: () => AttachmentObjectMetadata, onDelete: RelationOnDeleteAction.CASCADE, }) attachments: AttachmentObjectMetadata[]; @@ -176,8 +176,8 @@ export class PersonObjectMetadata extends BaseObjectMetadata { }) @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, - objectName: 'messageParticipant', - inverseSideFieldName: 'person', + inverseSideTarget: () => MessageParticipantObjectMetadata, + inverseSideFieldKey: 'person', }) messageParticipants: MessageParticipantObjectMetadata[]; @@ -189,8 +189,8 @@ export class PersonObjectMetadata extends BaseObjectMetadata { }) @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, - objectName: 'calendarEventAttendee', - inverseSideFieldName: 'person', + inverseSideTarget: () => CalendarEventAttendeeObjectMetadata, + inverseSideFieldKey: 'person', }) @Gate({ featureFlag: 'IS_CALENDAR_ENABLED', diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/pipeline-step.object-metadata.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/pipeline-step.object-metadata.ts index 4f11772fc7..3648e44b3c 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/pipeline-step.object-metadata.ts +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/pipeline-step.object-metadata.ts @@ -52,7 +52,7 @@ export class PipelineStepObjectMetadata extends BaseObjectMetadata { }) @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, - objectName: 'opportunity', + inverseSideTarget: () => OpportunityObjectMetadata, }) @IsNullable() opportunities: OpportunityObjectMetadata[]; diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/view.object-metadata.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/view.object-metadata.ts index e0d70a79dc..9d323f328d 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/view.object-metadata.ts +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/view.object-metadata.ts @@ -57,7 +57,7 @@ export class ViewObjectMetadata extends BaseObjectMetadata { }) @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, - objectName: 'viewField', + inverseSideTarget: () => ViewFieldObjectMetadata, }) @IsNullable() viewFields: ViewFieldObjectMetadata[]; @@ -70,7 +70,7 @@ export class ViewObjectMetadata extends BaseObjectMetadata { }) @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, - objectName: 'viewFilter', + inverseSideTarget: () => ViewFilterObjectMetadata, }) @IsNullable() viewFilters: ViewFilterObjectMetadata[]; @@ -83,7 +83,7 @@ export class ViewObjectMetadata extends BaseObjectMetadata { }) @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, - objectName: 'viewSort', + inverseSideTarget: () => ViewSortObjectMetadata, }) @IsNullable() viewSorts: ViewSortObjectMetadata[]; diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/workspace-member.object-metadata.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/workspace-member.object-metadata.ts index fe213ca38a..dbd5591255 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/workspace-member.object-metadata.ts +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/workspace-member.object-metadata.ts @@ -6,7 +6,6 @@ import { } from 'src/metadata/relation-metadata/relation-metadata.entity'; import { FieldMetadata } from 'src/workspace/workspace-sync-metadata/decorators/field-metadata.decorator'; import { Gate } from 'src/workspace/workspace-sync-metadata/decorators/gate.decorator'; -import { IsNullable } from 'src/workspace/workspace-sync-metadata/decorators/is-nullable.decorator'; import { IsSystem } from 'src/workspace/workspace-sync-metadata/decorators/is-system.decorator'; import { ObjectMetadata } from 'src/workspace/workspace-sync-metadata/decorators/object-metadata.decorator'; import { RelationMetadata } from 'src/workspace/workspace-sync-metadata/decorators/relation-metadata.decorator'; @@ -89,8 +88,8 @@ export class WorkspaceMemberObjectMetadata extends BaseObjectMetadata { }) @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, - objectName: 'activity', - inverseSideFieldName: 'author', + inverseSideTarget: () => ActivityObjectMetadata, + inverseSideFieldKey: 'author', }) authoredActivities: ActivityObjectMetadata[]; @@ -102,10 +101,11 @@ export class WorkspaceMemberObjectMetadata extends BaseObjectMetadata { }) @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, - objectName: 'activity', - inverseSideFieldName: 'assignee', + inverseSideTarget: () => ActivityObjectMetadata, + inverseSideFieldKey: 'assignee', }) - @IsNullable() + assignedActivities: ActivityObjectMetadata[]; + @FieldMetadata({ type: FieldMetadataType.RELATION, label: 'Favorites', @@ -114,7 +114,7 @@ export class WorkspaceMemberObjectMetadata extends BaseObjectMetadata { }) @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, - objectName: 'favorite', + inverseSideTarget: () => FavoriteObjectMetadata, onDelete: RelationOnDeleteAction.CASCADE, }) favorites: FavoriteObjectMetadata[]; @@ -127,8 +127,8 @@ export class WorkspaceMemberObjectMetadata extends BaseObjectMetadata { }) @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, - objectName: 'company', - inverseSideFieldName: 'accountOwner', + inverseSideTarget: () => CompanyObjectMetadata, + inverseSideFieldKey: 'accountOwner', }) accountOwnerForCompanies: CompanyObjectMetadata[]; @@ -140,8 +140,8 @@ export class WorkspaceMemberObjectMetadata extends BaseObjectMetadata { }) @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, - objectName: 'attachment', - inverseSideFieldName: 'author', + inverseSideTarget: () => AttachmentObjectMetadata, + inverseSideFieldKey: 'author', }) authoredAttachments: AttachmentObjectMetadata[]; @@ -153,8 +153,8 @@ export class WorkspaceMemberObjectMetadata extends BaseObjectMetadata { }) @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, - objectName: 'comment', - inverseSideFieldName: 'author', + inverseSideTarget: () => CommentObjectMetadata, + inverseSideFieldKey: 'author', }) authoredComments: CommentObjectMetadata[]; @@ -166,8 +166,8 @@ export class WorkspaceMemberObjectMetadata extends BaseObjectMetadata { }) @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, - objectName: 'connectedAccount', - inverseSideFieldName: 'accountOwner', + inverseSideTarget: () => ConnectedAccountObjectMetadata, + inverseSideFieldKey: 'accountOwner', }) connectedAccounts: ConnectedAccountObjectMetadata[]; @@ -179,8 +179,8 @@ export class WorkspaceMemberObjectMetadata extends BaseObjectMetadata { }) @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, - objectName: 'messageParticipant', - inverseSideFieldName: 'workspaceMember', + inverseSideTarget: () => MessageParticipantObjectMetadata, + inverseSideFieldKey: 'workspaceMember', }) messageParticipants: MessageParticipantObjectMetadata[]; @@ -192,8 +192,8 @@ export class WorkspaceMemberObjectMetadata extends BaseObjectMetadata { }) @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, - objectName: 'blocklist', - inverseSideFieldName: 'workspaceMember', + inverseSideTarget: () => BlocklistObjectMetadata, + inverseSideFieldKey: 'workspaceMember', }) blocklist: BlocklistObjectMetadata[]; @@ -205,8 +205,8 @@ export class WorkspaceMemberObjectMetadata extends BaseObjectMetadata { }) @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, - objectName: 'calendarEventAttendee', - inverseSideFieldName: 'workspaceMember', + inverseSideTarget: () => CalendarEventAttendeeObjectMetadata, + inverseSideFieldKey: 'workspaceMember', }) @Gate({ featureFlag: 'IS_CALENDAR_ENABLED', diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/storage/workspace-sync.storage.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/storage/workspace-sync.storage.ts index 4820c36da8..fe78c99534 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/storage/workspace-sync.storage.ts +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/storage/workspace-sync.storage.ts @@ -1,5 +1,5 @@ -import { PartialObjectMetadata } from 'src/workspace/workspace-sync-metadata/interfaces/partial-object-metadata.interface'; -import { PartialFieldMetadata } from 'src/workspace/workspace-sync-metadata/interfaces/partial-field-metadata.interface'; +import { ComputedPartialObjectMetadata } from 'src/workspace/workspace-sync-metadata/interfaces/partial-object-metadata.interface'; +import { ComputedPartialFieldMetadata } from 'src/workspace/workspace-sync-metadata/interfaces/partial-field-metadata.interface'; import { PartialRelationMetadata } from 'src/workspace/workspace-sync-metadata/interfaces/partial-relation-metadata.interface'; import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity'; @@ -8,15 +8,16 @@ import { RelationMetadataEntity } from 'src/metadata/relation-metadata/relation- export class WorkspaceSyncStorage { // Object metadata - private readonly _objectMetadataCreateCollection: PartialObjectMetadata[] = + private readonly _objectMetadataCreateCollection: ComputedPartialObjectMetadata[] = []; - private readonly _objectMetadataUpdateCollection: Partial[] = + private readonly _objectMetadataUpdateCollection: Partial[] = []; private readonly _objectMetadataDeleteCollection: ObjectMetadataEntity[] = []; // Field metadata - private readonly _fieldMetadataCreateCollection: PartialFieldMetadata[] = []; - private readonly _fieldMetadataUpdateCollection: (Partial & { + private readonly _fieldMetadataCreateCollection: ComputedPartialFieldMetadata[] = + []; + private readonly _fieldMetadataUpdateCollection: (Partial & { id: string; })[] = []; private readonly _fieldMetadataDeleteCollection: FieldMetadataEntity[] = []; @@ -67,11 +68,11 @@ export class WorkspaceSyncStorage { return this._relationMetadataDeleteCollection; } - addCreateObjectMetadata(object: PartialObjectMetadata) { + addCreateObjectMetadata(object: ComputedPartialObjectMetadata) { this._objectMetadataCreateCollection.push(object); } - addUpdateObjectMetadata(object: Partial) { + addUpdateObjectMetadata(object: Partial) { this._objectMetadataUpdateCollection.push(object); } @@ -79,12 +80,12 @@ export class WorkspaceSyncStorage { this._objectMetadataDeleteCollection.push(object); } - addCreateFieldMetadata(field: PartialFieldMetadata) { + addCreateFieldMetadata(field: ComputedPartialFieldMetadata) { this._fieldMetadataCreateCollection.push(field); } addUpdateFieldMetadata( - field: Partial & { id: string }, + field: Partial & { id: string }, ) { this._fieldMetadataUpdateCollection.push(field); } diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/utils/compute-standard-object.util.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/utils/compute-standard-object.util.ts new file mode 100644 index 0000000000..64147d2954 --- /dev/null +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/utils/compute-standard-object.util.ts @@ -0,0 +1,72 @@ +import { + ComputedPartialObjectMetadata, + PartialObjectMetadata, +} from 'src/workspace/workspace-sync-metadata/interfaces/partial-object-metadata.interface'; +import { ComputedPartialFieldMetadata } from 'src/workspace/workspace-sync-metadata/interfaces/partial-field-metadata.interface'; + +import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity'; +import { generateTargetColumnMap } from 'src/metadata/field-metadata/utils/generate-target-column-map.util'; +import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; + +export const computeStandardObject = ( + standardObjectMetadata: PartialObjectMetadata, + originalObjectMetadata: ObjectMetadataEntity, + customObjectMetadataCollection: ObjectMetadataEntity[] = [], +): ComputedPartialObjectMetadata => { + const fields: ComputedPartialFieldMetadata[] = []; + + for (const partialFieldMetadata of standardObjectMetadata.fields) { + if ('paramsFactory' in partialFieldMetadata) { + // Compute standard fields of custom object + for (const customObjectMetadata of customObjectMetadataCollection) { + const { paramsFactory, ...rest } = partialFieldMetadata; + const { joinColumn, ...data } = paramsFactory(customObjectMetadata); + + // Relation + fields.push({ + ...data, + ...rest, + defaultValue: null, + targetColumnMap: {}, + }); + + // Foreign key + fields.push({ + ...rest, + name: joinColumn, + type: FieldMetadataType.UUID, + label: `${data.label} ID (foreign key)`, + description: `${data.description} id foreign key`, + defaultValue: null, + icon: undefined, + targetColumnMap: generateTargetColumnMap( + FieldMetadataType.UUID, + rest.isCustom, + joinColumn, + ), + isSystem: true, + }); + } + } else { + const labelText = + typeof partialFieldMetadata.label === 'function' + ? partialFieldMetadata.label(originalObjectMetadata) + : partialFieldMetadata.label; + const descriptionText = + typeof partialFieldMetadata.description === 'function' + ? partialFieldMetadata.description(originalObjectMetadata) + : partialFieldMetadata.description; + + fields.push({ + ...partialFieldMetadata, + label: labelText, + description: descriptionText, + }); + } + } + + return { + ...standardObjectMetadata, + fields, + }; +}; diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/workspace-sync-metadata.module.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/workspace-sync-metadata.module.ts index a7b12a0e3e..81f9988e26 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/workspace-sync-metadata.module.ts +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/workspace-sync-metadata.module.ts @@ -13,6 +13,7 @@ import { workspaceSyncMetadataComparators } from 'src/workspace/workspace-sync-m import { WorkspaceMetadataUpdaterService } from 'src/workspace/workspace-sync-metadata/services/workspace-metadata-updater.service'; import { WorkspaceSyncObjectMetadataService } from 'src/workspace/workspace-sync-metadata/services/workspace-sync-object-metadata.service'; import { WorkspaceSyncRelationMetadataService } from 'src/workspace/workspace-sync-metadata/services/workspace-sync-relation-metadata.service'; +import { WorkspaceSyncFieldMetadataService } from 'src/workspace/workspace-sync-metadata/services/workspace-sync-field-metadata.service'; import { WorkspaceMigrationBuilderModule } from 'src/workspace/workspace-migration-builder/workspace-migration-builder.module'; @Module({ @@ -36,6 +37,7 @@ import { WorkspaceMigrationBuilderModule } from 'src/workspace/workspace-migrati WorkspaceMetadataUpdaterService, WorkspaceSyncObjectMetadataService, WorkspaceSyncRelationMetadataService, + WorkspaceSyncFieldMetadataService, WorkspaceSyncMetadataService, ], exports: [WorkspaceSyncMetadataService], diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/workspace-sync-metadata.service.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/workspace-sync-metadata.service.ts index c12fcc6073..e1b63bbeb3 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/workspace-sync-metadata.service.ts +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/workspace-sync-metadata.service.ts @@ -9,9 +9,14 @@ import { WorkspaceMigrationRunnerService } from 'src/workspace/workspace-migrati import { FeatureFlagFactory } from 'src/workspace/workspace-sync-metadata/factories/feature-flags.factory'; import { WorkspaceSyncObjectMetadataService } from 'src/workspace/workspace-sync-metadata/services/workspace-sync-object-metadata.service'; import { WorkspaceSyncRelationMetadataService } from 'src/workspace/workspace-sync-metadata/services/workspace-sync-relation-metadata.service'; +import { WorkspaceSyncFieldMetadataService } from 'src/workspace/workspace-sync-metadata/services/workspace-sync-field-metadata.service'; import { WorkspaceSyncStorage } from 'src/workspace/workspace-sync-metadata/storage/workspace-sync.storage'; import { WorkspaceMigrationEntity } from 'src/metadata/workspace-migration/workspace-migration.entity'; +interface SynchronizeOptions { + applyChanges?: boolean; +} + @Injectable() export class WorkspaceSyncMetadataService { private readonly logger = new Logger(WorkspaceSyncMetadataService.name); @@ -23,6 +28,7 @@ export class WorkspaceSyncMetadataService { private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService, private readonly workspaceSyncObjectMetadataService: WorkspaceSyncObjectMetadataService, private readonly workspaceSyncRelationMetadataService: WorkspaceSyncRelationMetadataService, + private readonly workspaceSyncFieldMetadataService: WorkspaceSyncFieldMetadataService, ) {} /** @@ -33,9 +39,9 @@ export class WorkspaceSyncMetadataService { * @param dataSourceId * @param workspaceId */ - public async syncStandardObjectsAndFieldsMetadata( + public async synchronize( context: WorkspaceSyncContext, - options: { applyChanges?: boolean } = { applyChanges: true }, + options: SynchronizeOptions = { applyChanges: true }, ): Promise<{ workspaceMigrations: WorkspaceMigrationEntity[]; storage: WorkspaceSyncStorage; @@ -62,6 +68,7 @@ export class WorkspaceSyncMetadataService { this.logger.log('Syncing standard objects and fields metadata'); + // 1 - Sync standard objects const workspaceObjectMigrations = await this.workspaceSyncObjectMetadataService.synchronize( context, @@ -70,6 +77,16 @@ export class WorkspaceSyncMetadataService { workspaceFeatureFlagsMap, ); + // 2 - Sync standard fields on custom objects + const workspaceFieldMigrations = + await this.workspaceSyncFieldMetadataService.synchronize( + context, + manager, + storage, + workspaceFeatureFlagsMap, + ); + + // 3 - Sync standard relations on standard and custom objects const workspaceRelationMigrations = await this.workspaceSyncRelationMetadataService.synchronize( context, @@ -81,6 +98,7 @@ export class WorkspaceSyncMetadataService { // Save workspace migrations into the database workspaceMigrations = await workspaceMigrationRepository.save([ ...workspaceObjectMigrations, + ...workspaceFieldMigrations, ...workspaceRelationMigrations, ]);