diff --git a/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapFieldMetadataToGraphQLQuery.test.ts b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapFieldMetadataToGraphQLQuery.test.ts index 0522cea6c..fbef6f38b 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapFieldMetadataToGraphQLQuery.test.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapFieldMetadataToGraphQLQuery.test.ts @@ -14,7 +14,8 @@ describe('mapFieldMetadataToGraphQLQuery', () => { it('should return fieldName if simpleValue', async () => { const res = mapFieldMetadataToGraphQLQuery({ objectMetadataItems: generatedMockObjectMetadataItems, - field: personObjectMetadataItem.fields.find( + gqlField: 'id', + fieldMetadata: personObjectMetadataItem.fields.find( (field) => field.name === 'id', )!, }); @@ -23,7 +24,8 @@ describe('mapFieldMetadataToGraphQLQuery', () => { it('should return fieldName if composite', async () => { const res = mapFieldMetadataToGraphQLQuery({ objectMetadataItems: generatedMockObjectMetadataItems, - field: personObjectMetadataItem.fields.find( + gqlField: 'name', + fieldMetadata: personObjectMetadataItem.fields.find( (field) => field.name === 'name', )!, }); @@ -39,7 +41,8 @@ describe('mapFieldMetadataToGraphQLQuery', () => { it('should return non relation subFields if relation', async () => { const res = mapFieldMetadataToGraphQLQuery({ objectMetadataItems: generatedMockObjectMetadataItems, - field: personObjectMetadataItem.fields.find( + gqlField: 'company', + fieldMetadata: personObjectMetadataItem.fields.find( (field) => field.name === 'company', )!, }); @@ -115,7 +118,8 @@ idealCustomerProfile id: true, idealCustomerProfile: true, }, - field: personObjectMetadataItem.fields.find( + gqlField: 'company', + fieldMetadata: personObjectMetadataItem.fields.find( (field) => field.name === 'company', )!, }); diff --git a/packages/twenty-front/src/modules/object-metadata/utils/__tests__/shouldFieldBeQueried.test.ts b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/shouldFieldBeQueried.test.ts index f59d9409c..5e4c0127e 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/__tests__/shouldFieldBeQueried.test.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/shouldFieldBeQueried.test.ts @@ -5,14 +5,16 @@ describe('shouldFieldBeQueried', () => { describe('if recordGqlFields is absent, we query all except relations', () => { it('should be queried if the field is not a relation', () => { const res = shouldFieldBeQueried({ - field: { name: 'fieldName', type: FieldMetadataType.BOOLEAN }, + gqlField: 'fieldName', + fieldMetadata: { name: 'fieldName', type: FieldMetadataType.BOOLEAN }, }); expect(res).toBe(true); }); it('should not be queried if the field is a relation', () => { const res = shouldFieldBeQueried({ - field: { name: 'fieldName', type: FieldMetadataType.RELATION }, + gqlField: 'fieldName', + fieldMetadata: { name: 'fieldName', type: FieldMetadataType.RELATION }, }); expect(res).toBe(false); }); @@ -21,8 +23,9 @@ describe('shouldFieldBeQueried', () => { describe('if recordGqlFields is present, we respect it', () => { it('should be queried if true', () => { const res = shouldFieldBeQueried({ + gqlField: 'fieldName', + fieldMetadata: { name: 'fieldName', type: FieldMetadataType.RELATION }, recordGqlFields: { fieldName: true }, - field: { name: 'fieldName', type: FieldMetadataType.RELATION }, }); expect(res).toBe(true); }); @@ -30,23 +33,26 @@ describe('shouldFieldBeQueried', () => { it('should be queried if object', () => { const res = shouldFieldBeQueried({ recordGqlFields: { fieldName: { subFieldName: false } }, - field: { name: 'fieldName', type: FieldMetadataType.RELATION }, + fieldMetadata: { name: 'fieldName', type: FieldMetadataType.RELATION }, + gqlField: 'fieldName', }); expect(res).toBe(true); }); it('should not be queried if false', () => { const res = shouldFieldBeQueried({ + gqlField: 'fieldName', + fieldMetadata: { name: 'fieldName', type: FieldMetadataType.RELATION }, recordGqlFields: { fieldName: false }, - field: { name: 'fieldName', type: FieldMetadataType.RELATION }, }); expect(res).toBe(false); }); it('should not be queried if absent', () => { const res = shouldFieldBeQueried({ + gqlField: 'fieldName', + fieldMetadata: { name: 'fieldName', type: FieldMetadataType.RELATION }, recordGqlFields: { otherFieldName: false }, - field: { name: 'fieldName', type: FieldMetadataType.RELATION }, }); expect(res).toBe(false); }); diff --git a/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts b/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts index 0599d68fb..4c7c6e649 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts @@ -12,40 +12,50 @@ import { FieldMetadataItem } from '../types/FieldMetadataItem'; type MapFieldMetadataToGraphQLQueryArgs = { objectMetadataItems: ObjectMetadataItem[]; - field: Pick; + gqlField: string; + fieldMetadata: Pick< + FieldMetadataItem, + 'name' | 'type' | 'relationDefinition' | 'settings' + >; relationRecordGqlFields?: RecordGqlFields; computeReferences?: boolean; }; // TODO: change ObjectMetadataItems mock before refactoring with relationDefinition computed field export const mapFieldMetadataToGraphQLQuery = ({ objectMetadataItems, - field, + gqlField, + fieldMetadata, relationRecordGqlFields, computeReferences = false, }: MapFieldMetadataToGraphQLQueryArgs): string => { - const fieldType = field.type; + const fieldType = fieldMetadata.type; const fieldIsNonCompositeField = isNonCompositeField(fieldType); if (fieldIsNonCompositeField) { - return field.name; + return gqlField; } if ( fieldType === FieldMetadataType.RELATION && - field.relationDefinition?.direction === RelationDefinitionType.MANY_TO_ONE + fieldMetadata.relationDefinition?.direction === + RelationDefinitionType.MANY_TO_ONE ) { const relationMetadataItem = objectMetadataItems.find( (objectMetadataItem) => objectMetadataItem.id === - field.relationDefinition?.targetObjectMetadata.id, + fieldMetadata.relationDefinition?.targetObjectMetadata.id, ); if (isUndefined(relationMetadataItem)) { return ''; } - return `${field.name} + if (gqlField === fieldMetadata.settings?.joinColumnName) { + return `${gqlField}`; + } + + return `${gqlField} ${mapObjectMetadataToGraphQLQuery({ objectMetadataItems, objectMetadataItem: relationMetadataItem, @@ -57,19 +67,20 @@ ${mapObjectMetadataToGraphQLQuery({ if ( fieldType === FieldMetadataType.RELATION && - field.relationDefinition?.direction === RelationDefinitionType.ONE_TO_MANY + fieldMetadata.relationDefinition?.direction === + RelationDefinitionType.ONE_TO_MANY ) { const relationMetadataItem = objectMetadataItems.find( (objectMetadataItem) => objectMetadataItem.id === - field.relationDefinition?.targetObjectMetadata.id, + fieldMetadata.relationDefinition?.targetObjectMetadata.id, ); if (isUndefined(relationMetadataItem)) { return ''; } - return `${field.name} + return `${gqlField} { edges { node ${mapObjectMetadataToGraphQLQuery({ @@ -84,7 +95,7 @@ ${mapObjectMetadataToGraphQLQuery({ } if (fieldType === FieldMetadataType.LINKS) { - return `${field.name} + return `${gqlField} { primaryLinkUrl primaryLinkLabel @@ -93,7 +104,7 @@ ${mapObjectMetadataToGraphQLQuery({ } if (fieldType === FieldMetadataType.CURRENCY) { - return `${field.name} + return `${gqlField} { amountMicros currencyCode @@ -102,7 +113,7 @@ ${mapObjectMetadataToGraphQLQuery({ } if (fieldType === FieldMetadataType.FULL_NAME) { - return `${field.name} + return `${gqlField} { firstName lastName @@ -110,7 +121,7 @@ ${mapObjectMetadataToGraphQLQuery({ } if (fieldType === FieldMetadataType.ADDRESS) { - return `${field.name} + return `${gqlField} { addressStreet1 addressStreet2 @@ -124,7 +135,7 @@ ${mapObjectMetadataToGraphQLQuery({ } if (fieldType === FieldMetadataType.ACTOR) { - return `${field.name} + return `${gqlField} { source workspaceMemberId @@ -134,7 +145,7 @@ ${mapObjectMetadataToGraphQLQuery({ } if (fieldType === FieldMetadataType.EMAILS) { - return `${field.name} + return `${gqlField} { primaryEmail additionalEmails @@ -142,7 +153,7 @@ ${mapObjectMetadataToGraphQLQuery({ } if (fieldType === FieldMetadataType.PHONES) { - return `${field.name} + return `${gqlField} { primaryPhoneNumber primaryPhoneCountryCode @@ -152,7 +163,7 @@ ${mapObjectMetadataToGraphQLQuery({ } if (fieldType === FieldMetadataType.RICH_TEXT_V2) { - return `${field.name} + return `${gqlField} { blocknote markdown diff --git a/packages/twenty-front/src/modules/object-metadata/utils/mapObjectMetadataToGraphQLQuery.ts b/packages/twenty-front/src/modules/object-metadata/utils/mapObjectMetadataToGraphQLQuery.ts index faec027f0..af38cb265 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/mapObjectMetadataToGraphQLQuery.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/mapObjectMetadataToGraphQLQuery.ts @@ -3,6 +3,8 @@ import { mapFieldMetadataToGraphQLQuery } from '@/object-metadata/utils/mapField import { shouldFieldBeQueried } from '@/object-metadata/utils/shouldFieldBeQueried'; import { RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields'; import { isRecordGqlFieldsNode } from '@/object-record/graphql/utils/isRecordGraphlFieldsNode'; +import { FieldMetadataType } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; type MapObjectMetadataToGraphQLQueryArgs = { objectMetadataItems: ObjectMetadataItem[]; @@ -18,16 +20,40 @@ export const mapObjectMetadataToGraphQLQuery = ({ computeReferences = false, isRootLevel = true, }: MapObjectMetadataToGraphQLQueryArgs): string => { - const fieldsThatShouldBeQueried = - objectMetadataItem?.fields - .filter((field) => field.isActive) - .sort((fieldA, fieldB) => fieldA.name.localeCompare(fieldB.name)) - .filter((field) => + const manyToOneRelationFields = objectMetadataItem?.fields + .filter((field) => field.isActive) + .filter((field) => field.type === FieldMetadataType.RELATION) + .filter((field) => isDefined(field.settings?.joinColumnName)); + + const manyToOneRelationGqlFieldWithFieldMetadata = + manyToOneRelationFields.map((field) => ({ + gqlField: field.settings?.joinColumnName, + fieldMetadata: field, + })); + + const gqlFieldWithFieldMetadataThatCouldBeQueried = [ + ...objectMetadataItem.fields + .filter((fieldMetadata) => fieldMetadata.isActive) + .map((fieldMetadata) => ({ + gqlField: fieldMetadata.name, + fieldMetadata, + })), + ...manyToOneRelationGqlFieldWithFieldMetadata, + ].sort((gqlFieldWithFieldMetadataA, gqlFieldWithFieldMetadataB) => + gqlFieldWithFieldMetadataA.gqlField.localeCompare( + gqlFieldWithFieldMetadataB.gqlField, + ), + ); + + const gqlFieldWithFieldMetadataThatSouldBeQueried = + gqlFieldWithFieldMetadataThatCouldBeQueried.filter( + (gqlFieldWithFieldMetadata) => shouldFieldBeQueried({ - field, + gqlField: gqlFieldWithFieldMetadata.gqlField, + fieldMetadata: gqlFieldWithFieldMetadata.fieldMetadata, recordGqlFields, }), - ) ?? []; + ); if (!isRootLevel && computeReferences) { return `{ @@ -37,9 +63,10 @@ export const mapObjectMetadataToGraphQLQuery = ({ return `{ __typename -${fieldsThatShouldBeQueried - .map((field) => { - const currentRecordGqlFields = recordGqlFields?.[field.name]; +${gqlFieldWithFieldMetadataThatSouldBeQueried + .map((gqlFieldWithFieldMetadata) => { + const currentRecordGqlFields = + recordGqlFields?.[gqlFieldWithFieldMetadata.gqlField]; const relationRecordGqlFields = isRecordGqlFieldsNode( currentRecordGqlFields, ) @@ -47,7 +74,8 @@ ${fieldsThatShouldBeQueried : undefined; return mapFieldMetadataToGraphQLQuery({ objectMetadataItems, - field, + gqlField: gqlFieldWithFieldMetadata.gqlField, + fieldMetadata: gqlFieldWithFieldMetadata.fieldMetadata, relationRecordGqlFields, computeReferences, }); diff --git a/packages/twenty-front/src/modules/object-metadata/utils/shouldFieldBeQueried.ts b/packages/twenty-front/src/modules/object-metadata/utils/shouldFieldBeQueried.ts index a9613fddf..f9b2f4a21 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/shouldFieldBeQueried.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/shouldFieldBeQueried.ts @@ -3,27 +3,30 @@ import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { FieldMetadataType } from '~/generated-metadata/graphql'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; -import { FieldMetadataItem } from '../types/FieldMetadataItem'; import { isDefined } from 'twenty-shared/utils'; +import { FieldMetadataItem } from '../types/FieldMetadataItem'; export const shouldFieldBeQueried = ({ - field, + gqlField, + fieldMetadata, recordGqlFields, }: { - field: Pick; + gqlField: string; + fieldMetadata: Pick; objectRecord?: ObjectRecord; recordGqlFields?: RecordGqlOperationGqlRecordFields; }): any => { if ( isUndefinedOrNull(recordGqlFields) && - field.type !== FieldMetadataType.RELATION + fieldMetadata.type !== FieldMetadataType.RELATION ) { return true; } + if ( isDefined(recordGqlFields) && - isDefined(recordGqlFields[field.name]) && - recordGqlFields[field.name] !== false + isDefined(recordGqlFields[gqlField]) && + recordGqlFields[gqlField] !== false ) { return true; } diff --git a/packages/twenty-front/src/modules/object-record/cache/utils/getRecordNodeFromRecord.ts b/packages/twenty-front/src/modules/object-record/cache/utils/getRecordNodeFromRecord.ts index da9f06c2c..26754c4ac 100644 --- a/packages/twenty-front/src/modules/object-record/cache/utils/getRecordNodeFromRecord.ts +++ b/packages/twenty-front/src/modules/object-record/cache/utils/getRecordNodeFromRecord.ts @@ -7,12 +7,12 @@ import { getRecordConnectionFromRecords } from '@/object-record/cache/utils/getR import { getRefName } from '@/object-record/cache/utils/getRefName'; import { RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { isDefined } from 'twenty-shared/utils'; import { FieldMetadataType, RelationDefinitionType, } from '~/generated-metadata/graphql'; import { pascalCase } from '~/utils/string/pascalCase'; -import { isDefined } from 'twenty-shared/utils'; export const getRecordNodeFromRecord = ({ objectMetadataItems, @@ -51,9 +51,11 @@ export const getRecordNodeFromRecord = ({ return undefined; } - const field = objectMetadataItem.fields.find( - (field) => field.name === fieldName, - ); + const field = + objectMetadataItem.fields.find((field) => field.name === fieldName) ?? + objectMetadataItem.fields.find( + (field) => field.settings?.joinColumnName === fieldName, + ); if (isUndefined(field)) { return undefined; @@ -94,6 +96,12 @@ export const getRecordNodeFromRecord = ({ switch (field.type) { case FieldMetadataType.RELATION: { + const isJoinColumn = field.settings?.joinColumnName === fieldName; + + if (isJoinColumn) { + return [fieldName, value]; + } + if ( isUndefined( field.relationDefinition?.targetObjectMetadata.nameSingular, diff --git a/packages/twenty-front/src/modules/object-record/cache/utils/updateRecordFromCache.ts b/packages/twenty-front/src/modules/object-record/cache/utils/updateRecordFromCache.ts index 33890500f..a616340c7 100644 --- a/packages/twenty-front/src/modules/object-record/cache/utils/updateRecordFromCache.ts +++ b/packages/twenty-front/src/modules/object-record/cache/utils/updateRecordFromCache.ts @@ -6,8 +6,8 @@ import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObje import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord'; import { RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; import { capitalize } from 'twenty-shared/utils'; +import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; export const updateRecordFromCache = ({ objectMetadataItems, diff --git a/packages/twenty-front/src/modules/object-record/graphql/utils/generateDepthOneRecordGqlFields.ts b/packages/twenty-front/src/modules/object-record/graphql/utils/generateDepthOneRecordGqlFields.ts index 0e779f988..96d0184f2 100644 --- a/packages/twenty-front/src/modules/object-record/graphql/utils/generateDepthOneRecordGqlFields.ts +++ b/packages/twenty-front/src/modules/object-record/graphql/utils/generateDepthOneRecordGqlFields.ts @@ -1,4 +1,5 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { isDefined } from 'twenty-shared/utils'; export type GenerateDepthOneRecordGqlFields = { objectMetadataItem: ObjectMetadataItem; @@ -10,6 +11,11 @@ export const generateDepthOneRecordGqlFields = ({ objectMetadataItem.fields.reduce>((acc, field) => { return { ...acc, + ...(isDefined(field.settings?.joinColumnName) + ? { + [field.settings.joinColumnName]: true, + } + : {}), [field.name]: true, }; }, {}); diff --git a/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecord.ts index abb2e63eb..f41baa71c 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecord.ts @@ -18,8 +18,8 @@ import { getUpdateOneRecordMutationResponseField } from '@/object-record/utils/g import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput'; import { isNull } from '@sniptt/guards'; import { useRecoilValue } from 'recoil'; -import { buildRecordFromKeysWithSameValue } from '~/utils/array/buildRecordFromKeysWithSameValue'; import { isDefined } from 'twenty-shared/utils'; +import { buildRecordFromKeysWithSameValue } from '~/utils/array/buildRecordFromKeysWithSameValue'; type useUpdateOneRecordProps = { objectNameSingular: string; @@ -91,6 +91,7 @@ export const useUpdateOneRecord = < id: idToUpdate, __typename: getObjectTypename(objectMetadataItem.nameSingular), }; + const optimisticRecordWithConnection = getRecordNodeFromRecord({ record: computedOptimisticRecord, @@ -110,6 +111,7 @@ export const useUpdateOneRecord = < objectMetadataItem, record: optimisticRecordInput, }); + updateRecordFromCache({ objectMetadataItems, objectMetadataItem, diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts index fd05fad3e..83a722dca 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts @@ -38,9 +38,9 @@ import { isMatchingRichTextV2Filter } from '@/object-record/record-filter/utils/ import { isMatchingSelectFilter } from '@/object-record/record-filter/utils/isMatchingSelectFilter'; import { isMatchingStringFilter } from '@/object-record/record-filter/utils/isMatchingStringFilter'; import { isMatchingUUIDFilter } from '@/object-record/record-filter/utils/isMatchingUUIDFilter'; +import { isDefined } from 'twenty-shared/utils'; import { FieldMetadataType } from '~/generated-metadata/graphql'; import { isEmptyObject } from '~/utils/isEmptyObject'; -import { isDefined } from 'twenty-shared/utils'; const isLeafFilter = ( filter: RecordGqlOperationFilter, @@ -167,9 +167,13 @@ export const isRecordMatchingFilter = ({ if (isEmptyObject(filterValue)) return true; - const objectMetadataField = objectMetadataItem.fields.find( - (field) => field.name === filterKey, - ); + const objectMetadataField = + objectMetadataItem.fields.find((field) => field.name === filterKey) ?? + objectMetadataItem.fields.find( + (field) => + field.type === FieldMetadataType.RELATION && + field.settings?.joinColumnName === filterKey, + ); if (!isDefined(objectMetadataField)) { throw new Error( @@ -359,6 +363,16 @@ export const isRecordMatchingFilter = ({ }); } case FieldMetadataType.RELATION: { + const isJoinColumn = + objectMetadataField.settings?.joinColumnName === filterKey; + + if (isJoinColumn) { + return isMatchingUUIDFilter({ + uuidFilter: filterValue as UUIDFilter, + value: record[filterKey], + }); + } + throw new Error( `Not implemented yet, use UUID filter instead on the corredponding "${filterKey}Id" field`, ); diff --git a/packages/twenty-front/src/modules/object-record/utils/computeOptimisticRecordFromInput.ts b/packages/twenty-front/src/modules/object-record/utils/computeOptimisticRecordFromInput.ts index db3c98094..f0edf6d67 100644 --- a/packages/twenty-front/src/modules/object-record/utils/computeOptimisticRecordFromInput.ts +++ b/packages/twenty-front/src/modules/object-record/utils/computeOptimisticRecordFromInput.ts @@ -14,9 +14,9 @@ import { isFieldUuid } from '@/object-record/record-field/types/guards/isFieldUu import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { buildOptimisticActorFieldValueFromCurrentWorkspaceMember } from '@/object-record/utils/buildOptimisticActorFieldValueFromCurrentWorkspaceMember'; import { getForeignKeyNameFromRelationFieldName } from '@/object-record/utils/getForeignKeyNameFromRelationFieldName'; +import { isDefined } from 'twenty-shared/utils'; import { RelationDefinitionType } from '~/generated-metadata/graphql'; import { FieldMetadataType } from '~/generated/graphql'; -import { isDefined } from 'twenty-shared/utils'; type ComputeOptimisticCacheRecordInputArgs = { objectMetadataItem: ObjectMetadataItem; @@ -32,11 +32,23 @@ export const computeOptimisticRecordFromInput = ({ }: ComputeOptimisticCacheRecordInputArgs) => { const unknownRecordInputFields = Object.keys(recordInput).filter( (recordKey) => { - const isUnknownMetadataItemField = - objectMetadataItem.fields.find((field) => field.name === recordKey) === - undefined; + const correspondingFieldMetadataItem = objectMetadataItem.fields.find( + (field) => field.name === recordKey, + ); + + const potentialJoinColumnNameFieldMetadataItem = + objectMetadataItem.fields.find( + (field) => + field.type === FieldMetadataType.RELATION && + field.settings?.joinColumnName === recordKey, + ); + + const isUnknownField = + !isDefined(correspondingFieldMetadataItem) && + !isDefined(potentialJoinColumnNameFieldMetadataItem); + const isTypenameField = recordKey === GRAPHQL_TYPENAME_KEY; - return isUnknownMetadataItemField && !isTypenameField; + return isUnknownField && !isTypenameField; }, ); if (unknownRecordInputFields.length > 0) { @@ -122,6 +134,7 @@ export const computeOptimisticRecordFromInput = ({ const relationFieldIdName = getForeignKeyNameFromRelationFieldName( fieldMetadataItem.name, ); + const recordInputFieldIdValue: string | null | undefined = recordInput[relationFieldIdName]; @@ -132,7 +145,11 @@ export const computeOptimisticRecordFromInput = ({ const relationIdFieldMetadataItem = objectMetadataItem.fields.find( (field) => field.name === relationFieldIdName, ); - if (!isDefined(relationIdFieldMetadataItem)) { + + if ( + !isDefined(relationIdFieldMetadataItem) && + !isDefined(fieldMetadataItem.settings?.joinColumnName) + ) { throw new Error( 'Should never occur, encountered unknown relationId within relations definitions', ); diff --git a/packages/twenty-front/src/modules/object-record/utils/sanitizeRecordInput.ts b/packages/twenty-front/src/modules/object-record/utils/sanitizeRecordInput.ts index 862af41f1..7201c1ebc 100644 --- a/packages/twenty-front/src/modules/object-record/utils/sanitizeRecordInput.ts +++ b/packages/twenty-front/src/modules/object-record/utils/sanitizeRecordInput.ts @@ -1,8 +1,8 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { isDefined } from 'twenty-shared/utils'; import { RelationDefinitionType } from '~/generated-metadata/graphql'; import { FieldMetadataType } from '~/generated/graphql'; -import { isDefined } from 'twenty-shared/utils'; export const sanitizeRecordInput = ({ objectMetadataItem, @@ -17,14 +17,26 @@ export const sanitizeRecordInput = ({ const fieldMetadataItem = objectMetadataItem.fields.find( (field) => field.name === fieldName, ); + const potentialJoinColumnNameFieldMetadataItem = + objectMetadataItem.fields.find( + (field) => + field.type === FieldMetadataType.RELATION && + field.settings?.joinColumnName === fieldName, + ); - if (!fieldMetadataItem) return undefined; + if ( + !isDefined(fieldMetadataItem) && + !isDefined(potentialJoinColumnNameFieldMetadataItem) + ) { + return undefined; + } - if (!fieldMetadataItem.isNullable && fieldValue == null) { + if (fieldMetadataItem?.isNullable === false && fieldValue == null) { return undefined; } if ( + isDefined(fieldMetadataItem) && fieldMetadataItem.type === FieldMetadataType.RELATION && fieldMetadataItem.relationDefinition?.direction === RelationDefinitionType.MANY_TO_ONE @@ -42,6 +54,7 @@ export const sanitizeRecordInput = ({ } if ( + isDefined(fieldMetadataItem) && fieldMetadataItem.type === FieldMetadataType.RELATION && fieldMetadataItem.relationDefinition?.direction === RelationDefinitionType.ONE_TO_MANY diff --git a/packages/twenty-front/src/pages/settings/accounts/SettingsAccounts.tsx b/packages/twenty-front/src/pages/settings/accounts/SettingsAccounts.tsx index 8789a96ca..1a0ff0583 100644 --- a/packages/twenty-front/src/pages/settings/accounts/SettingsAccounts.tsx +++ b/packages/twenty-front/src/pages/settings/accounts/SettingsAccounts.tsx @@ -14,9 +14,9 @@ import { SettingsPageContainer } from '@/settings/components/SettingsPageContain import { SettingsPath } from '@/types/SettingsPath'; import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { useLingui } from '@lingui/react/macro'; -import { getSettingsPath } from '~/utils/navigation/getSettingsPath'; import { H2Title } from 'twenty-ui/display'; import { Section } from 'twenty-ui/layout'; +import { getSettingsPath } from '~/utils/navigation/getSettingsPath'; export const SettingsAccounts = () => { const { t } = useLingui(); diff --git a/packages/twenty-server/.env.example b/packages/twenty-server/.env.example index ebc09d0ef..df66e2200 100644 --- a/packages/twenty-server/.env.example +++ b/packages/twenty-server/.env.example @@ -4,6 +4,7 @@ PG_DATABASE_URL=postgres://postgres:postgres@localhost:5432/default REDIS_URL=redis://localhost:6379 APP_SECRET=replace_me_with_a_random_string SIGN_IN_PREFILLED=true +SYNC_METADATA_INDEX_ENABLED=false FRONTEND_URL=http://localhost:3001 diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/0-51/0-51-migrate-relations-to-field-metadata.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/0-52/0-52-migrate-relations-to-field-metadata.command.ts similarity index 93% rename from packages/twenty-server/src/database/commands/upgrade-version-command/0-51/0-51-migrate-relations-to-field-metadata.command.ts rename to packages/twenty-server/src/database/commands/upgrade-version-command/0-52/0-52-migrate-relations-to-field-metadata.command.ts index 57499daad..24c66125a 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/0-51/0-51-migrate-relations-to-field-metadata.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/0-52/0-52-migrate-relations-to-field-metadata.command.ts @@ -18,10 +18,10 @@ import { RelationDirection, deduceRelationDirection, } from 'src/engine/utils/deduce-relation-direction.util'; -import { isFieldMetadataOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; +import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; @Command({ - name: 'upgrade:0-51:migrate-relations-to-field-metadata', + name: 'upgrade:0-52:migrate-relations-to-field-metadata', description: 'Migrate relations to field metadata', }) export class MigrateRelationsToFieldMetadataCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { @@ -64,13 +64,13 @@ export class MigrateRelationsToFieldMetadataCommand extends ActiveOrSuspendedWor const joinColumnFieldMetadataCollection = fieldMetadataCollection.filter( (fieldMetadata) => - isFieldMetadataOfType(fieldMetadata, FieldMetadataType.UUID), + isFieldMetadataEntityOfType(fieldMetadata, FieldMetadataType.UUID), // TODO: Fix this, it's working in other places but not here ) as FieldMetadataEntity[]; const fieldMetadataToUpdateCollection = fieldMetadataCollection .filter((fieldMetadata) => - isFieldMetadataOfType(fieldMetadata, FieldMetadataType.RELATION), + isFieldMetadataEntityOfType(fieldMetadata, FieldMetadataType.RELATION), ) .map((fieldMetadata) => this.updateRelationFieldMetadata( diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/0-52/0-52-upgrade-version-command.module.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/0-52/0-52-upgrade-version-command.module.ts index 5bcbecd33..a3b1ac452 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/0-52/0-52-upgrade-version-command.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/0-52/0-52-upgrade-version-command.module.ts @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { BackfillWorkflowNextStepIdsCommand } from 'src/database/commands/upgrade-version-command/0-52/0-52-backfill-workflow-next-step-ids.command'; +import { MigrateRelationsToFieldMetadataCommand } from 'src/database/commands/upgrade-version-command/0-52/0-52-migrate-relations-to-field-metadata.command'; import { UpgradeDateAndDateTimeFieldsSettingsJsonCommand } from 'src/database/commands/upgrade-version-command/0-52/0-52-upgrade-settings-field'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; @@ -20,10 +21,12 @@ import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/works providers: [ BackfillWorkflowNextStepIdsCommand, UpgradeDateAndDateTimeFieldsSettingsJsonCommand, + MigrateRelationsToFieldMetadataCommand, ], exports: [ BackfillWorkflowNextStepIdsCommand, UpgradeDateAndDateTimeFieldsSettingsJsonCommand, + MigrateRelationsToFieldMetadataCommand, ], }) export class V0_52_UpgradeVersionCommandModule {} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts index 37c20eba7..7e2da8ac6 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts @@ -17,6 +17,7 @@ import { UpdateDefaultViewRecordOpeningOnWorkflowObjectsCommand } from 'src/data import { InitializePermissionsCommand } from 'src/database/commands/upgrade-version-command/0-44/0-44-initialize-permissions.command'; import { UpdateViewAggregateOperationsCommand } from 'src/database/commands/upgrade-version-command/0-44/0-44-update-view-aggregate-operations.command'; import { UpgradeCreatedByEnumCommand } from 'src/database/commands/upgrade-version-command/0-51/0-51-update-workflow-trigger-type-enum.command'; +import { MigrateRelationsToFieldMetadataCommand } from 'src/database/commands/upgrade-version-command/0-52/0-52-migrate-relations-to-field-metadata.command'; import { UpgradeDateAndDateTimeFieldsSettingsJsonCommand } from 'src/database/commands/upgrade-version-command/0-52/0-52-upgrade-settings-field'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; @@ -58,6 +59,7 @@ export class UpgradeCommand extends UpgradeCommandRunner { // 0.52 Commands protected readonly upgradeDateAndDateTimeFieldsSettingsJsonCommand: UpgradeDateAndDateTimeFieldsSettingsJsonCommand, + protected readonly migrateRelationsToFieldMetadataCommand: MigrateRelationsToFieldMetadataCommand, ) { super( workspaceRepository, @@ -99,6 +101,7 @@ export class UpgradeCommand extends UpgradeCommandRunner { const _commands_052: VersionCommands = { beforeSyncMetadata: [ this.upgradeDateAndDateTimeFieldsSettingsJsonCommand, + this.migrateRelationsToFieldMetadataCommand, ], afterSyncMetadata: [], }; diff --git a/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts b/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts index bcec0e5c6..8f7cf4ac9 100644 --- a/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts +++ b/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts @@ -63,7 +63,7 @@ export const seedFeatureFlags = async ( { key: FeatureFlagKey.IsNewRelationEnabled, workspaceId: workspaceId, - value: false, + value: true, }, ]) .execute(); diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/__mocks__/mockPersonObjectMetadata.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/__mocks__/mockPersonObjectMetadata.ts index 0383c8978..f3e166f68 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/__mocks__/mockPersonObjectMetadata.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/__mocks__/mockPersonObjectMetadata.ts @@ -29,6 +29,7 @@ export const mockPersonObjectMetadata = ( fields: [], indexMetadatas: [], fieldsById: {}, + fieldsByJoinColumnName: {}, fieldsByName: { name: { id: '', diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser.ts index 36fa98e7e..db6a1c661 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser.ts @@ -14,15 +14,19 @@ import { GraphqlQueryFilterFieldParser } from './graphql-query-filter-field.pars export class GraphqlQueryFilterConditionParser { private fieldMetadataMapByName: FieldMetadataMap; + private fieldMetadataMapByJoinColumnName: FieldMetadataMap; private queryFilterFieldParser: GraphqlQueryFilterFieldParser; constructor( fieldMetadataMapByName: FieldMetadataMap, + fieldMetadataMapByJoinColumnName: FieldMetadataMap, featureFlagsMap: FeatureFlagMap, ) { this.fieldMetadataMapByName = fieldMetadataMapByName; + this.fieldMetadataMapByJoinColumnName = fieldMetadataMapByJoinColumnName; this.queryFilterFieldParser = new GraphqlQueryFilterFieldParser( this.fieldMetadataMapByName, + this.fieldMetadataMapByJoinColumnName, featureFlagsMap, ); } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-field.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-field.parser.ts index 5eb4b6940..35e224bd4 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-field.parser.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-field.parser.ts @@ -1,5 +1,5 @@ -import { WhereExpressionBuilder } from 'typeorm'; import { capitalize } from 'twenty-shared/utils'; +import { WhereExpressionBuilder } from 'typeorm'; import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface'; import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; @@ -18,13 +18,16 @@ const ARRAY_OPERATORS = ['in', 'contains', 'notContains']; export class GraphqlQueryFilterFieldParser { private fieldMetadataMapByName: FieldMetadataMap; + private fieldMetadataMapByJoinColumnName: FieldMetadataMap; private featureFlagsMap: FeatureFlagMap; constructor( fieldMetadataMapByName: FieldMetadataMap, + fieldMetadataMapByJoinColumnName: FieldMetadataMap, featureFlagsMap: FeatureFlagMap, ) { this.fieldMetadataMapByName = fieldMetadataMapByName; + this.fieldMetadataMapByJoinColumnName = fieldMetadataMapByJoinColumnName; this.featureFlagsMap = featureFlagsMap; } @@ -35,7 +38,9 @@ export class GraphqlQueryFilterFieldParser { filterValue: any, isFirst = false, ): void { - const fieldMetadata = this.fieldMetadataMapByName[`${key}`]; + const fieldMetadata = + this.fieldMetadataMapByName[`${key}`] || + this.fieldMetadataMapByJoinColumnName[`${key}`]; if (!fieldMetadata) { throw new Error(`Field metadata not found for field: ${key}`); diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts index f15acd743..b3769252e 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts @@ -24,6 +24,7 @@ import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modu export class GraphqlQueryParser { private fieldMetadataMapByName: FieldMetadataMap; + private fieldMetadataMapByJoinColumnName: FieldMetadataMap; private objectMetadataMaps: ObjectMetadataMaps; private filterConditionParser: GraphqlQueryFilterConditionParser; private orderFieldParser: GraphqlQueryOrderFieldParser; @@ -31,14 +32,17 @@ export class GraphqlQueryParser { constructor( fieldMetadataMapByName: FieldMetadataMap, + fieldMetadataMapByJoinColumnName: FieldMetadataMap, objectMetadataMaps: ObjectMetadataMaps, featureFlagsMap: FeatureFlagMap, ) { this.objectMetadataMaps = objectMetadataMaps; this.fieldMetadataMapByName = fieldMetadataMapByName; + this.fieldMetadataMapByJoinColumnName = fieldMetadataMapByJoinColumnName; this.featureFlagsMap = featureFlagsMap; this.filterConditionParser = new GraphqlQueryFilterConditionParser( this.fieldMetadataMapByName, + this.fieldMetadataMapByJoinColumnName, featureFlagsMap, ); this.orderFieldParser = new GraphqlQueryOrderFieldParser( diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper.ts index 7806f7437..81881fd26 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper.ts @@ -23,7 +23,7 @@ import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-met import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util'; import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; -import { isFieldMetadataOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; +import { isFieldMetadataInterfaceOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; @Injectable() export class ProcessNestedRelationsV2Helper { @@ -103,7 +103,10 @@ export class ProcessNestedRelationsV2Helper { parentObjectMetadataItem.fieldsByName[sourceFieldName]; if ( - !isFieldMetadataOfType(sourceFieldMetadata, FieldMetadataType.RELATION) + !isFieldMetadataInterfaceOfType( + sourceFieldMetadata, + FieldMetadataType.RELATION, + ) ) { // TODO: Maybe we should throw an error here ? return; @@ -149,7 +152,7 @@ export class ProcessNestedRelationsV2Helper { ? `"${targetRelationName}Id"` : 'id', ids: relationIds, - limit, + limit: limit * parentObjectRecords.length, objectMetadataMaps, targetObjectMetadata, aggregate, @@ -313,6 +316,7 @@ export class ProcessNestedRelationsV2Helper { result, targetObjectMetadata, objectMetadataMaps, + true, ); return { relationResults, relationAggregatedFieldsResult }; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper.ts index de0d6d49e..4fc89db28 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper.ts @@ -453,6 +453,7 @@ export class ProcessNestedRelationsHelper { result, referenceObjectMetadata, objectMetadataMaps, + false, ); return { relationResults, relationAggregatedFieldsResult }; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service.ts index d7e3d2453..fca01e495 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service.ts @@ -130,6 +130,7 @@ export abstract class GraphqlQueryBaseResolverService< const graphqlQueryParser = new GraphqlQueryParser( objectMetadataItemWithFieldMaps.fieldsByName, + objectMetadataItemWithFieldMaps.fieldsByJoinColumnName, options.objectMetadataMaps, featureFlagsMap, ); diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service.ts index 0f8f18a1c..83e5015b6 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service.ts @@ -44,6 +44,7 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol objectRecords, objectMetadataItemWithFieldMaps, objectMetadataMaps, + featureFlagsMap, ); this.apiEventEmitterService.emitCreateEvents( @@ -301,6 +302,7 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol objectRecords: InsertResult, objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, objectMetadataMaps: ObjectMetadataMaps, + featureFlagsMap: Record, ): Promise { const queryBuilder = executionArgs.repository.createQueryBuilder( objectMetadataItemWithFieldMaps.nameSingular, @@ -317,6 +319,7 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol nonFormattedUpsertedRecords, objectMetadataItemWithFieldMaps, objectMetadataMaps, + featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled], ); } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-one-resolver.service.ts index 2cece26eb..52476579e 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-one-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-one-resolver.service.ts @@ -53,6 +53,7 @@ export class GraphqlQueryCreateOneResolverService extends GraphqlQueryBaseResolv nonFormattedUpsertedRecords, objectMetadataItemWithFieldMaps, objectMetadataMaps, + featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled], ); this.apiEventEmitterService.emitCreateEvents( diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-many-resolver.service.ts index 51b1e67e1..e99a50f18 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-many-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-many-resolver.service.ts @@ -54,6 +54,7 @@ export class GraphqlQueryDeleteManyResolverService extends GraphqlQueryBaseResol nonFormattedDeletedObjectRecords.raw, objectMetadataItemWithFieldMaps, objectMetadataMaps, + featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled], ); this.apiEventEmitterService.emitDeletedEvents( diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-one-resolver.service.ts index a673ebbe4..992912742 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-one-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-one-resolver.service.ts @@ -47,6 +47,7 @@ export class GraphqlQueryDeleteOneResolverService extends GraphqlQueryBaseResolv nonFormattedDeletedObjectRecords.raw, objectMetadataItemWithFieldMaps, objectMetadataMaps, + featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled], ); this.apiEventEmitterService.emitDeletedEvents( diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service.ts index aab236a45..8919e68c1 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service.ts @@ -52,6 +52,7 @@ export class GraphqlQueryDestroyManyResolverService extends GraphqlQueryBaseReso nonFormattedDeletedObjectRecords.raw, objectMetadataItemWithFieldMaps, objectMetadataMaps, + featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled], ); this.apiEventEmitterService.emitDestroyEvents( diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service.ts index 7008ce93d..22575a62f 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service.ts @@ -52,6 +52,7 @@ export class GraphqlQueryDestroyOneResolverService extends GraphqlQueryBaseResol nonFormattedDeletedObjectRecords.raw, objectMetadataItemWithFieldMaps, objectMetadataMaps, + featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled], ); this.apiEventEmitterService.emitDestroyEvents( diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service.ts index d3189dacf..b0e1aae84 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service.ts @@ -64,6 +64,7 @@ export class GraphqlQueryFindDuplicatesResolverService extends GraphqlQueryBaseR const graphqlQueryParser = new GraphqlQueryParser( objectMetadataItemWithFieldsMaps?.fieldsByName, + objectMetadataItemWithFieldsMaps?.fieldsByJoinColumnName, objectMetadataMaps, featureFlagsMap, ); @@ -85,6 +86,7 @@ export class GraphqlQueryFindDuplicatesResolverService extends GraphqlQueryBaseR nonFormattedObjectRecords, objectMetadataItemWithFieldMaps, objectMetadataMaps, + featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled], ); } else if (executionArgs.args.data && !isEmpty(executionArgs.args.data)) { objectRecords = formatData( @@ -131,6 +133,7 @@ export class GraphqlQueryFindDuplicatesResolverService extends GraphqlQueryBaseR nonFormattedDuplicates, objectMetadataItemWithFieldMaps, objectMetadataMaps, + featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled], ); return typeORMObjectRecordsParser.createConnection({ diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service.ts index 4ccd0dab8..0c62f5ea9 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service.ts @@ -130,6 +130,7 @@ export class GraphqlQueryFindManyResolverService extends GraphqlQueryBaseResolve nonFormattedObjectRecords, objectMetadataItemWithFieldMaps, objectMetadataMaps, + featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled], ); const { hasNextPage, hasPreviousPage } = getPaginationInfo( diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service.ts index 3414235b8..b124e6d76 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service.ts @@ -59,6 +59,7 @@ export class GraphqlQueryFindOneResolverService extends GraphqlQueryBaseResolver nonFormattedObjectRecord, objectMetadataItemWithFieldMaps, objectMetadataMaps, + featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled], ); if (!objectRecord) { diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-many-resolver.service.ts index 5c7fd967e..df18d4e37 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-many-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-many-resolver.service.ts @@ -54,6 +54,7 @@ export class GraphqlQueryRestoreManyResolverService extends GraphqlQueryBaseReso nonFormattedRestoredObjectRecords.raw, objectMetadataItemWithFieldMaps, objectMetadataMaps, + featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled], ); this.apiEventEmitterService.emitRestoreEvents( diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-one-resolver.service.ts index 9c694dd7e..73c4d9002 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-one-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-one-resolver.service.ts @@ -47,6 +47,7 @@ export class GraphqlQueryRestoreOneResolverService extends GraphqlQueryBaseResol nonFormattedRestoredObjectRecords.raw, objectMetadataItemWithFieldMaps, objectMetadataMaps, + featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled], ); this.apiEventEmitterService.emitRestoreEvents( diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service.ts index 703c57305..37a302d88 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service.ts @@ -55,6 +55,7 @@ export class GraphqlQueryUpdateManyResolverService extends GraphqlQueryBaseResol existingRecords, objectMetadataItemWithFieldMaps, objectMetadataMaps, + featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled], ); if (isEmpty(formattedExistingRecords)) { @@ -89,6 +90,7 @@ export class GraphqlQueryUpdateManyResolverService extends GraphqlQueryBaseResol nonFormattedUpdatedObjectRecords.raw, objectMetadataItemWithFieldMaps, objectMetadataMaps, + featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled], ); this.apiEventEmitterService.emitUpdateEvents( diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service.ts index d6d803ae0..13fed1c5b 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service.ts @@ -55,6 +55,7 @@ export class GraphqlQueryUpdateOneResolverService extends GraphqlQueryBaseResolv existingRecords, objectMetadataItemWithFieldMaps, objectMetadataMaps, + featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled], ); if (isEmpty(formattedExistingRecords)) { @@ -74,6 +75,7 @@ export class GraphqlQueryUpdateOneResolverService extends GraphqlQueryBaseResolv nonFormattedUpdatedObjectRecords.raw, objectMetadataItemWithFieldMaps, objectMetadataMaps, + featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled], ); this.apiEventEmitterService.emitUpdateEvents( diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory.ts index b789427aa..95f2ae458 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory.ts @@ -22,7 +22,7 @@ import { CompositeInputTypeDefinitionFactory } from 'src/engine/api/graphql/work import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { FileService } from 'src/engine/core-modules/file/services/file.service'; import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; -import { isFieldMetadataOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; +import { isFieldMetadataInterfaceOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; // TODO: find a way to prevent conflict between handlers executing logic on object relations // And this factory that is also executing logic on object relations @@ -153,7 +153,10 @@ export class QueryResultGettersFactory { ) .filter(isDefined) .filter((fieldMetadata) => - isFieldMetadataOfType(fieldMetadata, FieldMetadataType.RELATION), + isFieldMetadataInterfaceOfType( + fieldMetadata, + FieldMetadataType.RELATION, + ), ); const relationFieldsProcessedMap = {} as Record< diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/extend-object-type-definition-v2.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/extend-object-type-definition-v2.factory.ts index cc310a15d..49ea91c95 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/extend-object-type-definition-v2.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/extend-object-type-definition-v2.factory.ts @@ -15,7 +15,7 @@ import { RelationTypeV2Factory } from 'src/engine/api/graphql/workspace-schema-b import { TypeDefinitionsStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/type-definitions.storage'; import { getResolverArgs } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util'; import { objectContainsRelationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/object-contains-relation-field'; -import { isFieldMetadataOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; +import { isFieldMetadataInterfaceOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; import { ArgsFactory } from './args.factory'; @@ -108,7 +108,12 @@ export class ExtendObjectTypeDefinitionV2Factory { for (const fieldMetadata of objectMetadata.fields) { // Ignore non-relation fields as they are already defined - if (!isFieldMetadataOfType(fieldMetadata, FieldMetadataType.RELATION)) { + if ( + !isFieldMetadataInterfaceOfType( + fieldMetadata, + FieldMetadataType.RELATION, + ) + ) { continue; } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory.ts index 67b3ee176..599106cbc 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory.ts @@ -36,6 +36,7 @@ export class InputTypeDefinitionFactory { objectMetadata: ObjectMetadataInterface, kind: InputTypeDefinitionKind, options: WorkspaceBuildSchemaOptions, + isNewRelationEnabled = false, ): InputTypeDefinition { const inputType = new GraphQLInputObjectType({ name: `${pascalCase(objectMetadata.nameSingular)}${kind.toString()}Input`, @@ -58,6 +59,7 @@ export class InputTypeDefinitionFactory { kind, options, this.inputTypeFactory, + isNewRelationEnabled, ), and: { type: andOrType, @@ -81,6 +83,7 @@ export class InputTypeDefinitionFactory { kind, options, this.inputTypeFactory, + isNewRelationEnabled, ); } }, diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/object-type-definition.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/object-type-definition.factory.ts index cf2efd3a7..ef13e0fc7 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/object-type-definition.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/object-type-definition.factory.ts @@ -30,6 +30,7 @@ export class ObjectTypeDefinitionFactory { objectMetadata: ObjectMetadataInterface, kind: ObjectTypeDefinitionKind, options: WorkspaceBuildSchemaOptions, + isNewRelationEnabled = false, ): ObjectTypeDefinition { return { target: objectMetadata.id, @@ -42,6 +43,7 @@ export class ObjectTypeDefinitionFactory { kind, options, this.outputTypeFactory, + isNewRelationEnabled, ), }), }; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service.ts index 235311cda..0dc6b4543 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service.ts @@ -58,7 +58,7 @@ export class TypeMapperService { settings?: FieldMetadataSettings, isIdField?: boolean, ): GraphQLScalarType | undefined { - if (isIdField || settings?.isForeignKey) { + if (isIdField || fieldMetadataType === FieldMetadataType.RELATION) { return GraphQLID; } const typeScalarMapping = new Map([ @@ -93,7 +93,7 @@ export class TypeMapperService { settings?: FieldMetadataSettings, isIdField?: boolean, ): GraphQLInputObjectType | GraphQLScalarType | undefined { - if (isIdField || settings?.isForeignKey) { + if (isIdField || fieldMetadataType === FieldMetadataType.RELATION) { return IDFilterType; } @@ -132,6 +132,7 @@ export class TypeMapperService { ): GraphQLInputType | undefined { const typeOrderByMapping = new Map([ [FieldMetadataType.UUID, OrderByDirectionType], + [FieldMetadataType.RELATION, OrderByDirectionType], [FieldMetadataType.TEXT, OrderByDirectionType], [FieldMetadataType.DATE_TIME, OrderByDirectionType], [FieldMetadataType.DATE, OrderByDirectionType], diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/type-definitions.generator.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/type-definitions.generator.ts index e298788dd..f12921e07 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/type-definitions.generator.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/type-definitions.generator.ts @@ -51,11 +51,16 @@ export class TypeDefinitionsGenerator { async generate( objectMetadataCollection: ObjectMetadataInterface[], options: WorkspaceBuildSchemaOptions, + isNewRelationEnabled: boolean, ) { // Generate composite type objects first because they can be used in dynamic objects await this.generateCompositeTypeDefs(options); // Generate metadata objects - await this.generateMetadataTypeDefs(objectMetadataCollection, options); + await this.generateMetadataTypeDefs( + objectMetadataCollection, + options, + isNewRelationEnabled, + ); } /** @@ -155,6 +160,7 @@ export class TypeDefinitionsGenerator { private async generateMetadataTypeDefs( dynamicObjectMetadataCollection: ObjectMetadataInterface[], options: WorkspaceBuildSchemaOptions, + isNewRelationEnabled: boolean, ) { this.logger.log( `Generating metadata objects: [${dynamicObjectMetadataCollection @@ -164,9 +170,17 @@ export class TypeDefinitionsGenerator { // Generate dynamic objects this.generateEnumTypeDefs(dynamicObjectMetadataCollection, options); - this.generateObjectTypeDefs(dynamicObjectMetadataCollection, options); + this.generateObjectTypeDefs( + dynamicObjectMetadataCollection, + options, + isNewRelationEnabled, + ); this.generatePaginationTypeDefs(dynamicObjectMetadataCollection, options); - this.generateInputTypeDefs(dynamicObjectMetadataCollection, options); + this.generateInputTypeDefs( + dynamicObjectMetadataCollection, + options, + isNewRelationEnabled, + ); await this.generateExtendedObjectTypeDefs( dynamicObjectMetadataCollection, options, @@ -176,12 +190,14 @@ export class TypeDefinitionsGenerator { private generateObjectTypeDefs( objectMetadataCollection: ObjectMetadataInterface[] | CompositeType[], options: WorkspaceBuildSchemaOptions, + isNewRelationEnabled: boolean, ) { const objectTypeDefs = objectMetadataCollection.map((objectMetadata) => this.objectTypeDefinitionFactory.create( objectMetadata, ObjectTypeDefinitionKind.Plain, options, + isNewRelationEnabled, ), ); @@ -209,6 +225,7 @@ export class TypeDefinitionsGenerator { private generateInputTypeDefs( objectMetadataCollection: ObjectMetadataInterface[], options: WorkspaceBuildSchemaOptions, + isNewRelationEnabled: boolean, ) { const inputTypeDefs = objectMetadataCollection .map((objectMetadata) => { @@ -226,24 +243,28 @@ export class TypeDefinitionsGenerator { objectMetadata, InputTypeDefinitionKind.Create, options, + isNewRelationEnabled, ), // Input type for update this.inputTypeDefinitionFactory.create( optionalExtendedObjectMetadata, InputTypeDefinitionKind.Update, options, + isNewRelationEnabled, ), // Filter input type this.inputTypeDefinitionFactory.create( optionalExtendedObjectMetadata, InputTypeDefinitionKind.Filter, options, + isNewRelationEnabled, ), // OrderBy input type this.inputTypeDefinitionFactory.create( optionalExtendedObjectMetadata, InputTypeDefinitionKind.OrderBy, options, + isNewRelationEnabled, ), ]; }) diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/generate-fields.utils.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/generate-fields.utils.ts index a03120947..dacafe04f 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/generate-fields.utils.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/generate-fields.utils.ts @@ -8,11 +8,12 @@ import { FieldMetadataType } from 'twenty-shared/types'; import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { InputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory'; import { ObjectTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/factories/object-type-definition.factory'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; +import { isFieldMetadataInterfaceOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; type TypeFactory = { @@ -40,13 +41,21 @@ export const generateFields = < kind: T, options: WorkspaceBuildSchemaOptions, typeFactory: TypeFactory, + isNewRelationEnabled = false, ): T extends InputTypeDefinitionKind ? GraphQLInputFieldConfigMap : GraphQLFieldConfigMap => { const fields = {}; for (const fieldMetadata of objectMetadata.fields) { - if (isRelationFieldMetadataType(fieldMetadata.type)) { + if ( + isFieldMetadataInterfaceOfType( + fieldMetadata, + FieldMetadataType.RELATION, + ) && + (fieldMetadata.settings?.relationType !== RelationType.MANY_TO_ONE || + !isNewRelationEnabled) + ) { continue; } @@ -80,6 +89,25 @@ export const generateFields = < typeFactoryOptions, ); + if ( + isFieldMetadataInterfaceOfType( + fieldMetadata, + FieldMetadataType.RELATION, + ) && + fieldMetadata.settings?.relationType === RelationType.MANY_TO_ONE + ) { + const joinColumnName = fieldMetadata.settings?.joinColumnName; + + if (!joinColumnName) { + throw new Error('Join column name is not defined'); + } + + fields[joinColumnName] = { + type, + description: fieldMetadata.description, + }; + } + fields[fieldMetadata.name] = { type, description: fieldMetadata.description, diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/workspace-graphql-schema.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/workspace-graphql-schema.factory.ts index be48bdb2b..09f81bcf9 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/workspace-graphql-schema.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/workspace-graphql-schema.factory.ts @@ -25,11 +25,13 @@ export class WorkspaceGraphQLSchemaFactory { objectMetadataCollection: ObjectMetadataInterface[], workspaceResolverBuilderMethods: WorkspaceResolverBuilderMethods, options: WorkspaceBuildSchemaOptions = {}, + isNewRelationEnabled = false, ): Promise { // Generate type definitions await this.typeDefinitionsGenerator.generate( objectMetadataCollection, options, + isNewRelationEnabled, ); // Generate schema diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema.factory.ts index 92d308e71..176b0af17 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema.factory.ts @@ -149,6 +149,8 @@ export class WorkspaceSchemaFactory { await this.workspaceGraphQLSchemaFactory.create( objectMetadataCollection, workspaceResolverBuilderMethodNames, + {}, + isNewRelationEnabled, ); usedScalarNames = diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/check-fields.utils.spec.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/check-fields.utils.spec.ts index 623f047ee..b907989b6 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/check-fields.utils.spec.ts +++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/check-fields.utils.spec.ts @@ -31,6 +31,7 @@ describe('checkFields', () => { ...objectMetadataItemMock, fieldsById, fieldsByName, + fieldsByJoinColumnName: {}, }; it('should check field types', () => { diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/get-field-type.utils.spec.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/get-field-type.utils.spec.ts index 07c9a2830..7370917e8 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/get-field-type.utils.spec.ts +++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/get-field-type.utils.spec.ts @@ -32,6 +32,7 @@ describe('getFieldType', () => { ...objectMetadataItemMock, fieldsById, fieldsByName, + fieldsByJoinColumnName: {}, }; it('should get field type', () => { diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/map-field-metadata-to-graphql-query.utils.spec.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/map-field-metadata-to-graphql-query.utils.spec.ts index 34ac20868..8660d23ee 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/map-field-metadata-to-graphql-query.utils.spec.ts +++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/map-field-metadata-to-graphql-query.utils.spec.ts @@ -61,6 +61,7 @@ describe('mapFieldMetadataToGraphqlQuery', () => { ...objectMetadataItemMock, fieldsById, fieldsByName, + fieldsByJoinColumnName: {}, }; const objectMetadataMapsMock: ObjectMetadataMaps = { diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/check-filter-enum-values.spec.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/check-filter-enum-values.spec.ts index d2af17ac3..2968d0387 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/check-filter-enum-values.spec.ts +++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/check-filter-enum-values.spec.ts @@ -33,6 +33,7 @@ describe('checkFilterEnumValues', () => { ...objectMetadataItemMock, fieldsById, fieldsByName, + fieldsByJoinColumnName: {}, }; it('should check properly', () => { diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/parse-filter.utils.spec.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/parse-filter.utils.spec.ts index 8376ab61b..74ff334e8 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/parse-filter.utils.spec.ts +++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/parse-filter.utils.spec.ts @@ -43,6 +43,7 @@ describe('parseFilter', () => { ...objectMetadataItemMock, fieldsById, fieldsByName, + fieldsByJoinColumnName: {}, }; it('should parse string filter test 1', () => { diff --git a/packages/twenty-server/src/engine/core-modules/auth/token/services/access-token.service.ts b/packages/twenty-server/src/engine/core-modules/auth/token/services/access-token.service.ts index 17df32f37..e0be07126 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/token/services/access-token.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/token/services/access-token.service.ts @@ -72,6 +72,7 @@ export class AccessTokenService { await this.twentyORMGlobalManager.getRepositoryForWorkspace( workspaceId, 'workspaceMember', + false, ); const workspaceMember = await workspaceMemberRepository.findOne({ diff --git a/packages/twenty-server/src/engine/core-modules/search/__mocks__/mockObjectMetadataItemsWithFieldMaps.ts b/packages/twenty-server/src/engine/core-modules/search/__mocks__/mockObjectMetadataItemsWithFieldMaps.ts index 986927eea..bb0b777f8 100644 --- a/packages/twenty-server/src/engine/core-modules/search/__mocks__/mockObjectMetadataItemsWithFieldMaps.ts +++ b/packages/twenty-server/src/engine/core-modules/search/__mocks__/mockObjectMetadataItemsWithFieldMaps.ts @@ -62,6 +62,7 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa workspaceId: '', }, }, + fieldsByJoinColumnName: {}, }, { id: '', @@ -140,6 +141,7 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa workspaceId: '', }, }, + fieldsByJoinColumnName: {}, }, { id: '', @@ -218,6 +220,7 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa workspaceId: '', }, }, + fieldsByJoinColumnName: {}, }, { id: '', @@ -243,5 +246,6 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa indexMetadatas: [], fieldsById: {}, fieldsByName: {}, + fieldsByJoinColumnName: {}, }, ]; diff --git a/packages/twenty-server/src/engine/core-modules/search/services/search.service.ts b/packages/twenty-server/src/engine/core-modules/search/services/search.service.ts index 0ebb5fcb2..9c696b1dd 100644 --- a/packages/twenty-server/src/engine/core-modules/search/services/search.service.ts +++ b/packages/twenty-server/src/engine/core-modules/search/services/search.service.ts @@ -74,6 +74,7 @@ export class SearchService { const queryParser = new GraphqlQueryParser( objectMetadataItem.fieldsByName, + objectMetadataItem.fieldsByJoinColumnName, generateObjectMetadataMaps([objectMetadataItem]), featureFlagMap, ); diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.resolver.ts index 44940636c..b868d2d62 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.resolver.ts @@ -25,7 +25,10 @@ import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { CreateOneFieldMetadataInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input'; import { DeleteOneFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/delete-field.input'; import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto'; -import { RelationDefinitionDTO } from 'src/engine/metadata-modules/field-metadata/dtos/relation-definition.dto'; +import { + RelationDefinitionDTO, + RelationDefinitionType, +} from 'src/engine/metadata-modules/field-metadata/dtos/relation-definition.dto'; import { RelationDTO } from 'src/engine/metadata-modules/field-metadata/dtos/relation.dto'; import { UpdateFieldInput, @@ -42,6 +45,7 @@ import { fieldMetadataGraphqlApiExceptionHandler } from 'src/engine/metadata-mod import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; +import { createDeterministicUuid } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/create-deterministic-uuid.util'; @UseGuards(WorkspaceAuthGuard) @Resolver(() => FieldMetadataDTO) @@ -180,6 +184,37 @@ export class FieldMetadataResolver { return null; } + const isNewRelationEnabled = await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IsNewRelationEnabled, + workspace.id, + ); + + // TODO: Remove this once we drop old relations or update the front-end to use the new relation + if (isNewRelationEnabled) { + const relation = await this.relation( + workspace, + fieldMetadata as FieldMetadataEntity, + context, + ); + + if (!relation) { + return null; + } + + return { + // Temporary fix as we don't have relationId in the new relation + relationId: createDeterministicUuid([ + relation.sourceFieldMetadata.id, + relation.targetFieldMetadata.id, + ]), + direction: relation.type as unknown as RelationDefinitionType, + sourceObjectMetadata: relation.sourceObjectMetadata, + targetObjectMetadata: relation.targetObjectMetadata, + sourceFieldMetadata: relation.sourceFieldMetadata, + targetFieldMetadata: relation.targetFieldMetadata, + }; + } + try { const relationMetadataItem = await context.loaders.relationMetadataLoader.load({ diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface.ts index ce6ba7cbd..65391e8a4 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface.ts @@ -42,7 +42,7 @@ export type FieldMetadataDateTimeSettings = { export type FieldMetadataRelationSettings = { relationType: RelationType; onDelete?: RelationOnDeleteAction; - joinColumnName?: string; + joinColumnName?: string | null; }; type FieldMetadataSettingsMapping = { diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts index 22108b574..c14cfbb9a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts @@ -19,6 +19,7 @@ import { IndexMetadataModule } from 'src/engine/metadata-modules/index-metadata/ import { BeforeUpdateOneObject } from 'src/engine/metadata-modules/object-metadata/hooks/before-update-one-object.hook'; import { ObjectMetadataGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/object-metadata/interceptors/object-metadata-graphql-api-exception.interceptor'; import { ObjectMetadataResolver } from 'src/engine/metadata-modules/object-metadata/object-metadata.resolver'; +import { ObjectMetadataFieldRelationService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-field-relation.service'; import { ObjectMetadataMigrationService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-migration.service'; import { ObjectMetadataRelatedRecordsService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-related-records.service'; import { ObjectMetadataRelationService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-relation.service'; @@ -63,6 +64,7 @@ import { UpdateObjectPayload } from './dtos/update-object.input'; ObjectMetadataService, ObjectMetadataMigrationService, ObjectMetadataRelationService, + ObjectMetadataFieldRelationService, ObjectMetadataRelatedRecordsService, ], resolvers: [ diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts index 50250c9a2..b0403b2d0 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts @@ -10,6 +10,8 @@ import { FindManyOptions, FindOneOptions, In, Not, Repository } from 'typeorm'; import { ObjectMetadataStandardIdToIdMap } from 'src/engine/metadata-modules/object-metadata/interfaces/object-metadata-standard-id-to-id-map'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; @@ -24,6 +26,7 @@ import { ObjectMetadataException, ObjectMetadataExceptionCode, } from 'src/engine/metadata-modules/object-metadata/object-metadata.exception'; +import { ObjectMetadataFieldRelationService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-field-relation.service'; import { ObjectMetadataMigrationService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-migration.service'; import { ObjectMetadataRelatedRecordsService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-related-records.service'; import { ObjectMetadataRelationService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-relation.service'; @@ -61,9 +64,11 @@ export class ObjectMetadataService extends TypeOrmQueryService { + const isNewRelationEnabled = await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IsNewRelationEnabled, + workspaceId, + ); + const objectMetadata = await this.objectMetadataRepository.findOne({ relations: [ + 'fields', + 'fields.object', + 'fields.relationTargetFieldMetadata', + 'fields.relationTargetFieldMetadata.object', 'fromRelations.fromFieldMetadata', 'fromRelations.toFieldMetadata', 'toRelations.fromFieldMetadata', @@ -375,6 +413,7 @@ export class ObjectMetadataService extends TypeOrmQueryService field.id); + const relationMetadataIds = objectMetadata.fields + .map((field) => field.relationTargetFieldMetadata?.id) + .filter(isDefined); + + await this.fieldMetadataRepository.delete({ + id: In(fieldMetadataIds.concat(relationMetadataIds)), + }); + await this.objectMetadataRepository.delete(objectMetadata.id); await this.workspaceMetadataVersionService.incrementMetadataVersion( @@ -478,6 +526,11 @@ export class ObjectMetadataService extends TypeOrmQueryService, + @InjectRepository(FieldMetadataEntity, 'metadata') + private readonly fieldMetadataRepository: Repository, + ) {} + + public async createRelationsAndForeignKeysMetadata( + workspaceId: string, + sourceObjectMetadata: ObjectMetadataEntity, + ) { + const relatedObjectMetadataCollection = await Promise.all( + DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS.map( + async (relationObjectMetadataStandardId) => + this.createRelationAndForeignKeyMetadata( + workspaceId, + sourceObjectMetadata, + relationObjectMetadataStandardId, + ), + ), + ); + + return relatedObjectMetadataCollection; + } + + private async createRelationAndForeignKeyMetadata( + workspaceId: string, + sourceObjectMetadata: ObjectMetadataEntity, + relationObjectMetadataStandardId: string, + ) { + const targetObjectMetadata = + await this.objectMetadataRepository.findOneByOrFail({ + standardId: relationObjectMetadataStandardId, + workspaceId: workspaceId, + isCustom: false, + }); + + await this.createFieldMetadataRelation( + workspaceId, + sourceObjectMetadata, + targetObjectMetadata, + ); + + return targetObjectMetadata; + } + + private async createFieldMetadataRelation( + workspaceId: string, + sourceObjectMetadata: ObjectMetadataEntity, + targetObjectMetadata: ObjectMetadataEntity, + ): Promise[]> { + const sourceFieldMetadata = this.createSourceFieldMetadata( + workspaceId, + sourceObjectMetadata, + targetObjectMetadata, + ); + + const targetFieldMetadata = this.createTargetFieldMetadata( + workspaceId, + sourceObjectMetadata, + targetObjectMetadata, + ); + + return this.fieldMetadataRepository.save([ + { + ...sourceFieldMetadata, + settings: { + relationType: RelationType.ONE_TO_MANY, + onDelete: RelationOnDeleteAction.CASCADE, + }, + relationTargetObjectMetadataId: targetObjectMetadata.id, + relationTargetFieldMetadataId: targetFieldMetadata.id, + } as Partial>, + { + ...targetFieldMetadata, + settings: { + relationType: RelationType.MANY_TO_ONE, + onDelete: RelationOnDeleteAction.CASCADE, + joinColumnName: `${sourceObjectMetadata.nameSingular}Id`, + }, + relationTargetObjectMetadataId: sourceObjectMetadata.id, + relationTargetFieldMetadataId: sourceFieldMetadata.id, + } as Partial>, + ]); + } + + public async updateRelationsAndForeignKeysMetadata( + workspaceId: string, + updatedObjectMetadata: ObjectMetadataEntity, + ): Promise< + { + targetObjectMetadata: ObjectMetadataEntity; + targetFieldMetadata: FieldMetadataEntity; + sourceFieldMetadata: FieldMetadataEntity; + }[] + > { + return await Promise.all( + DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS.map( + async (relationObjectMetadataStandardId) => + this.updateRelationAndForeignKeyMetadata( + workspaceId, + updatedObjectMetadata, + relationObjectMetadataStandardId, + ), + ), + ); + } + + private async updateRelationAndForeignKeyMetadata( + workspaceId: string, + sourceObjectMetadata: ObjectMetadataEntity, + targetObjectMetadataStandardId: string, + ) { + const targetObjectMetadata = + await this.objectMetadataRepository.findOneByOrFail({ + standardId: targetObjectMetadataStandardId, + workspaceId: workspaceId, + isCustom: false, + }); + + const targetFieldMetadataUpdateData = this.updateTargetFieldMetadata( + sourceObjectMetadata, + targetObjectMetadata, + ); + const targetFieldMetadataToUpdate = + await this.fieldMetadataRepository.findOneByOrFail({ + standardId: createRelationDeterministicUuid({ + objectId: sourceObjectMetadata.id, + standardId: + STANDARD_OBJECT_FIELD_IDS[targetObjectMetadata.nameSingular].custom, + }), + objectMetadataId: targetObjectMetadata.id, + workspaceId: workspaceId, + }); + const targetFieldMetadata = await this.fieldMetadataRepository.save({ + id: targetFieldMetadataToUpdate.id, + ...targetFieldMetadataUpdateData, + }); + + const sourceFieldMetadataUpdateData = this.updateSourceFieldMetadata( + sourceObjectMetadata, + targetObjectMetadata, + ); + const sourceFieldMetadataToUpdate = + await this.fieldMetadataRepository.findOneByOrFail({ + standardId: + CUSTOM_OBJECT_STANDARD_FIELD_IDS[targetObjectMetadata.namePlural], + objectMetadataId: sourceObjectMetadata.id, + workspaceId: workspaceId, + }); + const sourceFieldMetadata = await this.fieldMetadataRepository.save({ + id: sourceFieldMetadataToUpdate.id, + ...sourceFieldMetadataUpdateData, + }); + + return { + targetObjectMetadata, + targetFieldMetadata, + sourceFieldMetadata, + }; + } + + private createSourceFieldMetadata( + workspaceId: string, + sourceObjectMetadata: ObjectMetadataEntity, + targetObjectMetadata: ObjectMetadataEntity, + ): Partial> { + const relationObjectMetadataNamePlural = targetObjectMetadata.namePlural; + + const { description } = buildDescriptionForRelationFieldMetadataOnFromField( + { + relationObjectMetadataNamePlural, + targetObjectLabelSingular: sourceObjectMetadata.labelSingular, + }, + ); + + return { + id: uuidV4(), + standardId: + CUSTOM_OBJECT_STANDARD_FIELD_IDS[relationObjectMetadataNamePlural], + objectMetadataId: sourceObjectMetadata.id, + workspaceId: workspaceId, + isCustom: false, + isActive: true, + isSystem: true, + type: FieldMetadataType.RELATION, + name: targetObjectMetadata.namePlural, + label: capitalize(relationObjectMetadataNamePlural), + description, + icon: + STANDARD_OBJECT_ICONS[targetObjectMetadata.nameSingular] || + 'IconBuildingSkyscraper', + isNullable: true, + }; + } + + private updateSourceFieldMetadata( + sourceObjectMetadata: ObjectMetadataEntity, + targetObjectMetadata: ObjectMetadataEntity, + ) { + const relationObjectMetadataNamePlural = targetObjectMetadata.namePlural; + + const { description } = buildDescriptionForRelationFieldMetadataOnFromField( + { + relationObjectMetadataNamePlural, + targetObjectLabelSingular: sourceObjectMetadata.labelSingular, + }, + ); + + return { + description, + }; + } + + private createTargetFieldMetadata( + workspaceId: string, + sourceObjectMetadata: ObjectMetadataEntity, + targetObjectMetadata: ObjectMetadataEntity, + ): Partial> { + const customStandardFieldId = + STANDARD_OBJECT_FIELD_IDS[targetObjectMetadata.nameSingular].custom; + + if (!customStandardFieldId) { + throw new Error( + `Custom standard field ID not found for ${targetObjectMetadata.nameSingular}`, + ); + } + + const { description } = buildDescriptionForRelationFieldMetadataOnToField({ + relationObjectMetadataNamePlural: targetObjectMetadata.namePlural, + targetObjectLabelSingular: sourceObjectMetadata.labelSingular, + }); + + return { + id: uuidV4(), + name: sourceObjectMetadata.nameSingular, + label: sourceObjectMetadata.labelSingular, + description, + standardId: createRelationDeterministicUuid({ + objectId: sourceObjectMetadata.id, + standardId: customStandardFieldId, + }), + objectMetadataId: targetObjectMetadata.id, + workspaceId: workspaceId, + isCustom: false, + isActive: true, + isSystem: true, + type: FieldMetadataType.RELATION, + icon: 'IconBuildingSkyscraper', + isNullable: true, + }; + } + + private updateTargetFieldMetadata( + sourceObjectMetadata: ObjectMetadataEntity, + targetObjectMetadata: ObjectMetadataEntity, + ) { + const customStandardFieldId = + STANDARD_OBJECT_FIELD_IDS[targetObjectMetadata.nameSingular].custom; + + if (!customStandardFieldId) { + throw new Error( + `Custom standard field ID not found for ${targetObjectMetadata.nameSingular}`, + ); + } + + const { description } = buildDescriptionForRelationFieldMetadataOnToField({ + relationObjectMetadataNamePlural: targetObjectMetadata.namePlural, + targetObjectLabelSingular: sourceObjectMetadata.labelSingular, + }); + + return { + name: sourceObjectMetadata.nameSingular, + label: sourceObjectMetadata.labelSingular, + description, + }; + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/services/object-metadata-migration.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/services/object-metadata-migration.service.ts index 1a5858df5..396f25216 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/services/object-metadata-migration.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/services/object-metadata-migration.service.ts @@ -1,8 +1,10 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { In, Repository } from 'typeorm'; import { FieldMetadataType } from 'twenty-shared/types'; +import { In, Repository } from 'typeorm'; + +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; @@ -22,6 +24,7 @@ import { WorkspaceMigrationFactory } from 'src/engine/metadata-modules/workspace import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service'; import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; import { computeTableName } from 'src/engine/utils/compute-table-name.util'; +import { isFieldMetadataInterfaceOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; import { RELATION_MIGRATION_PRIORITY_PREFIX } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service'; @Injectable() @@ -117,6 +120,52 @@ export class ObjectMetadataMigrationService { ); } + public async updateRelationMigrations( + currentObjectMetadata: ObjectMetadataEntity, + alteredObjectMetadata: ObjectMetadataEntity, + relationMetadataCollection: { + targetObjectMetadata: ObjectMetadataEntity; + targetFieldMetadata: FieldMetadataEntity; + sourceFieldMetadata: FieldMetadataEntity; + }[], + workspaceId: string, + ) { + for (const { targetObjectMetadata } of relationMetadataCollection) { + const targetTableName = computeObjectTargetTable(targetObjectMetadata); + const columnName = `${currentObjectMetadata.nameSingular}Id`; + + await this.workspaceMigrationService.createCustomMigration( + generateMigrationName( + `rename-${currentObjectMetadata.nameSingular}-to-${alteredObjectMetadata.nameSingular}-in-${targetObjectMetadata.nameSingular}`, + ), + workspaceId, + [ + { + name: targetTableName, + action: WorkspaceMigrationTableActionType.ALTER, + columns: [ + { + action: WorkspaceMigrationColumnActionType.ALTER, + currentColumnDefinition: { + columnName, + columnType: 'uuid', + isNullable: true, + defaultValue: null, + }, + alteredColumnDefinition: { + columnName: `${alteredObjectMetadata.nameSingular}Id`, + columnType: 'uuid', + isNullable: true, + defaultValue: null, + }, + }, + ], + }, + ], + ); + } + } + public async createUpdateForeignKeysMigrations( existingObjectMetadata: ObjectMetadataEntity, updatedObjectMetadata: ObjectMetadataEntity, @@ -173,15 +222,16 @@ export class ObjectMetadataMigrationService { public async deleteAllRelationsAndDropTable( objectMetadata: ObjectMetadataEntity, workspaceId: string, + isNewRelationEnabled: boolean, ) { - const relationsToDelete: RelationToDelete[] = []; + const relationsMetadataToDelete: RelationToDelete[] = []; // TODO: Most of this logic should be moved to relation-metadata.service.ts for (const relation of [ ...objectMetadata.fromRelations, ...objectMetadata.toRelations, ]) { - relationsToDelete.push({ + relationsMetadataToDelete.push({ id: relation.id, fromFieldMetadataId: relation.fromFieldMetadata.id, toFieldMetadataId: relation.toFieldMetadata.id, @@ -201,13 +251,13 @@ export class ObjectMetadataMigrationService { }); } - if (relationsToDelete.length > 0) { + if (relationsMetadataToDelete.length > 0) { await this.relationMetadataRepository.delete( - relationsToDelete.map((relation) => relation.id), + relationsMetadataToDelete.map((relation) => relation.id), ); } - for (const relationToDelete of relationsToDelete) { + for (const relationToDelete of relationsMetadataToDelete) { const foreignKeyFieldsToDelete = await this.fieldMetadataRepository.find({ where: { name: `${relationToDelete.toFieldMetadataName}Id`, @@ -254,6 +304,61 @@ export class ObjectMetadataMigrationService { } } + if (isNewRelationEnabled) { + const manyToOneRelationFieldsToDelete = objectMetadata.fields.filter( + (field) => + isFieldMetadataInterfaceOfType(field, FieldMetadataType.RELATION) && + (field as FieldMetadataEntity).settings + ?.relationType === RelationType.MANY_TO_ONE, + ) as FieldMetadataEntity[]; + + const oneToManyRelationFieldsToDelete = objectMetadata.fields.filter( + (field) => + isFieldMetadataInterfaceOfType(field, FieldMetadataType.RELATION) && + (field as FieldMetadataEntity).settings + ?.relationType === RelationType.ONE_TO_MANY, + ); + + const relationFieldsToDelete = [ + ...manyToOneRelationFieldsToDelete, + ...(oneToManyRelationFieldsToDelete.map( + (field) => field.relationTargetFieldMetadata, + ) as FieldMetadataEntity[]), + ]; + + for (const relationFieldToDelete of relationFieldsToDelete) { + const joinColumnName = relationFieldToDelete.settings?.joinColumnName; + + if (!joinColumnName) { + throw new Error( + `Join column name is not set for relation field ${relationFieldToDelete.name}`, + ); + } + + await this.workspaceMigrationService.createCustomMigration( + generateMigrationName( + `delete-${RELATION_MIGRATION_PRIORITY_PREFIX}-${relationFieldToDelete.name}`, + ), + workspaceId, + [ + { + name: computeTableName( + relationFieldToDelete.object.nameSingular, + relationFieldToDelete.object.isCustom, + ), + action: WorkspaceMigrationTableActionType.ALTER, + columns: [ + { + action: WorkspaceMigrationColumnActionType.DROP, + columnName: joinColumnName, + } satisfies WorkspaceMigrationColumnDrop, + ], + }, + ], + ); + } + } + // DROP TABLE await this.workspaceMigrationService.createCustomMigration( generateMigrationName(`delete-${objectMetadata.nameSingular}`), diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-migrations-for-custom-object-relations.util.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-migrations-for-custom-object-relations.util.ts index 0695b66b6..e8f019500 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-migrations-for-custom-object-relations.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-migrations-for-custom-object-relations.util.ts @@ -23,6 +23,7 @@ export const buildMigrationsForCustomObjectRelations = ( columns: [ { action: WorkspaceMigrationColumnActionType.CREATE, + // TODO: When we get rid of this and use the sync metadata, columnName must be based on the joinColumnName from the field metadata settings columnName: computeColumnName(createdObjectMetadata.nameSingular, { isForeignKey: true, }), @@ -38,6 +39,7 @@ export const buildMigrationsForCustomObjectRelations = ( columns: [ { action: WorkspaceMigrationColumnActionType.CREATE_FOREIGN_KEY, + // TODO: When we get rid of this and use the sync metadata, columnName must be based on the joinColumnName from the field metadata settings columnName: computeColumnName(createdObjectMetadata.nameSingular, { isForeignKey: true, }), diff --git a/packages/twenty-server/src/engine/metadata-modules/relation-metadata/relation-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/relation-metadata/relation-metadata.service.ts index e9b4cdd4e..0a67c7ff9 100644 --- a/packages/twenty-server/src/engine/metadata-modules/relation-metadata/relation-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/relation-metadata/relation-metadata.service.ts @@ -3,13 +3,17 @@ import { InjectRepository } from '@nestjs/typeorm'; import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; import camelCase from 'lodash.camelcase'; -import { FindOneOptions, In, Repository } from 'typeorm'; -import { v4 as uuidV4 } from 'uuid'; import { FieldMetadataType } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; +import { FindOneOptions, In, Repository } from 'typeorm'; +import { v4 as uuidV4 } from 'uuid'; +import { FieldMetadataDefaultSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface'; import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/field-metadata.service'; import { IndexMetadataService } from 'src/engine/metadata-modules/index-metadata/index-metadata.service'; @@ -20,6 +24,7 @@ import { RelationMetadataException, RelationMetadataExceptionCode, } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.exception'; +import { InvalidMetadataNameException } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata-name.exception'; import { validateFieldNameAvailabilityOrThrow } from 'src/engine/metadata-modules/utils/validate-field-name-availability.utils'; import { validateMetadataNameOrThrow } from 'src/engine/metadata-modules/utils/validate-metadata-name.utils'; import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service'; @@ -34,7 +39,6 @@ import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service'; import { BASE_OBJECT_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; -import { InvalidMetadataNameException } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata-name.exception'; import { RelationMetadataEntity, @@ -56,6 +60,7 @@ export class RelationMetadataService extends TypeOrmQueryService fieldMetadata.id === fromId, + ); + + if (!fromFieldMetadata) { + throw new RelationMetadataException( + `From field metadata not found`, + RelationMetadataExceptionCode.RELATION_METADATA_NOT_FOUND, + ); + } + + const toFieldMetadata = createdRelationFieldsMetadata.find( + (fieldMetadata) => fieldMetadata.id === toId, + ); + + if (!toFieldMetadata) { + throw new RelationMetadataException( + `To field metadata not found`, + RelationMetadataExceptionCode.RELATION_METADATA_NOT_FOUND, + ); + } + + await this.fieldMetadataRepository.update(fromId, { + settings: { + relationType: RelationType.ONE_TO_MANY, + } as Partial, + relationTargetFieldMetadataId: toId, + relationTargetObjectMetadataId: relationMetadataInput.toObjectMetadataId, + }); + + await this.fieldMetadataRepository.update(toId, { + settings: { + relationType: RelationType.MANY_TO_ONE, + joinColumnName: `${relationMetadataInput.toName}Id`, + } as Partial, + relationTargetFieldMetadataId: fromId, + relationTargetObjectMetadataId: + relationMetadataInput.fromObjectMetadataId, + }); + await this.createWorkspaceCustomMigration( relationMetadataInput, objectMetadataMap, @@ -123,17 +183,6 @@ export class RelationMetadataService extends TypeOrmQueryService fieldMetadata.type === FieldMetadataType.UUID, - ); - - if (!foreignKeyFieldMetadata) { - throw new RelationMetadataException( - `ForeignKey field metadata not found`, - RelationMetadataExceptionCode.RELATION_METADATA_NOT_FOUND, - ); - } - const deletedAtFieldMetadata = toObjectMetadata.fields.find( (fieldMetadata) => fieldMetadata.standardId === BASE_OBJECT_STANDARD_FIELD_IDS.deletedAt, @@ -141,16 +190,29 @@ export class RelationMetadataService extends TypeOrmQueryService, - ], - false, - false, - ); + if (!isNewRelationEnabled) { + const foreignKeyFieldMetadata = createdRelationFieldsMetadata.find( + (fieldMetadata) => fieldMetadata.type === FieldMetadataType.UUID, + ); + + if (!foreignKeyFieldMetadata) { + throw new RelationMetadataException( + `ForeignKey field metadata not found`, + RelationMetadataExceptionCode.RELATION_METADATA_NOT_FOUND, + ); + } + + await this.indexMetadataService.createIndexMetadata( + relationMetadataInput.workspaceId, + toObjectMetadata, + [ + foreignKeyFieldMetadata, + deletedAtFieldMetadata as FieldMetadataEntity, + ], + false, + false, + ); + } await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations( relationMetadataInput.workspaceId, diff --git a/packages/twenty-server/src/engine/metadata-modules/types/object-metadata-item-with-field-maps.ts b/packages/twenty-server/src/engine/metadata-modules/types/object-metadata-item-with-field-maps.ts index b46cbe3cc..6460e3d37 100644 --- a/packages/twenty-server/src/engine/metadata-modules/types/object-metadata-item-with-field-maps.ts +++ b/packages/twenty-server/src/engine/metadata-modules/types/object-metadata-item-with-field-maps.ts @@ -5,4 +5,5 @@ import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metada export type ObjectMetadataItemWithFieldMaps = ObjectMetadataInterface & { fieldsById: FieldMetadataMap; fieldsByName: FieldMetadataMap; + fieldsByJoinColumnName: FieldMetadataMap; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/generate-object-metadata-maps.util.ts b/packages/twenty-server/src/engine/metadata-modules/utils/generate-object-metadata-maps.util.ts index e83f75cc3..464793fff 100644 --- a/packages/twenty-server/src/engine/metadata-modules/utils/generate-object-metadata-maps.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/utils/generate-object-metadata-maps.util.ts @@ -1,8 +1,11 @@ +import { FieldMetadataType } from 'twenty-shared/types'; + import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { isFieldMetadataInterfaceOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; export const generateObjectMetadataMaps = ( objectMetadataCollection: ObjectMetadataInterface[], @@ -15,8 +18,21 @@ export const generateObjectMetadataMaps = ( for (const objectMetadata of objectMetadataCollection) { const fieldsByIdMap: FieldMetadataMap = {}; const fieldsByNameMap: FieldMetadataMap = {}; + const fieldsByJoinColumnNameMap: FieldMetadataMap = {}; for (const fieldMetadata of objectMetadata.fields) { + if ( + isFieldMetadataInterfaceOfType( + fieldMetadata, + FieldMetadataType.RELATION, + ) + ) { + if (fieldMetadata.settings?.joinColumnName) { + fieldsByJoinColumnNameMap[fieldMetadata.settings.joinColumnName] = + fieldMetadata; + } + } + fieldsByNameMap[fieldMetadata.name] = fieldMetadata; fieldsByIdMap[fieldMetadata.id] = fieldMetadata; } @@ -25,6 +41,7 @@ export const generateObjectMetadataMaps = ( ...objectMetadata, fieldsById: fieldsByIdMap, fieldsByName: fieldsByNameMap, + fieldsByJoinColumnName: fieldsByJoinColumnNameMap, }; objectMetadataMaps.byId[objectMetadata.id] = processedObjectMetadata; diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/factories.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/factories.ts index 3dcbe1fac..f64f7b64f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/factories.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/factories.ts @@ -1,6 +1,7 @@ import { BasicColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/basic-column-action.factory'; import { CompositeColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory'; import { EnumColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/enum-column-action.factory'; +import { RelationColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/relation-column-action.factory'; import { TsVectorColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/ts-vector-column-action.factory'; export const workspaceColumnActionFactories = [ @@ -8,4 +9,5 @@ export const workspaceColumnActionFactories = [ BasicColumnActionFactory, EnumColumnActionFactory, CompositeColumnActionFactory, + RelationColumnActionFactory, ]; diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/relation-column-action.factory.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/relation-column-action.factory.ts new file mode 100644 index 000000000..1cd31d70f --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/relation-column-action.factory.ts @@ -0,0 +1,97 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import { FieldMetadataType } from 'twenty-shared/types'; + +import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; +import { WorkspaceColumnActionOptions } from 'src/engine/metadata-modules/workspace-migration/interfaces/workspace-column-action-options.interface'; + +import { ColumnActionAbstractFactory } from 'src/engine/metadata-modules/workspace-migration/factories/column-action-abstract.factory'; +import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util'; +import { + WorkspaceMigrationColumnActionType, + WorkspaceMigrationColumnAlter, + WorkspaceMigrationColumnCreate, +} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; +import { + WorkspaceMigrationException, + WorkspaceMigrationExceptionCode, +} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.exception'; + +@Injectable() +export class RelationColumnActionFactory extends ColumnActionAbstractFactory { + protected readonly logger = new Logger(RelationColumnActionFactory.name); + + protected handleCreateAction( + fieldMetadata: FieldMetadataInterface, + _options?: WorkspaceColumnActionOptions, + ): WorkspaceMigrationColumnCreate[] { + if (!fieldMetadata.settings || !fieldMetadata.settings.joinColumnName) { + return []; + } + + const joinColumnName = fieldMetadata.settings.joinColumnName; + + return [ + { + action: WorkspaceMigrationColumnActionType.CREATE, + columnName: joinColumnName, + columnType: fieldMetadataTypeToColumnType(FieldMetadataType.UUID), + isArray: false, + isNullable: fieldMetadata.isNullable ?? true, + isUnique: + fieldMetadata.settings.relationType === RelationType.ONE_TO_ONE, + defaultValue: null, + }, + ]; + } + + protected handleAlterAction( + currentFieldMetadata: FieldMetadataInterface, + alteredFieldMetadata: FieldMetadataInterface, + _options?: WorkspaceColumnActionOptions, + ): WorkspaceMigrationColumnAlter[] { + if (!currentFieldMetadata.settings || !alteredFieldMetadata.settings) { + return []; + } + + const currentJoinColumnName = currentFieldMetadata.settings.joinColumnName; + const alteredJoinColumnName = alteredFieldMetadata.settings.joinColumnName; + + if (!currentJoinColumnName || !alteredJoinColumnName) { + this.logger.error( + `Column name not found for current or altered field metadata, can be due to a missing or an invalid target column map. Current column name: ${currentJoinColumnName}, Altered column name: ${alteredJoinColumnName}.`, + ); + throw new WorkspaceMigrationException( + `Column name not found for current or altered field metadata`, + WorkspaceMigrationExceptionCode.INVALID_FIELD_METADATA, + ); + } + + return [ + { + action: WorkspaceMigrationColumnActionType.ALTER, + currentColumnDefinition: { + columnName: currentJoinColumnName, + columnType: fieldMetadataTypeToColumnType(FieldMetadataType.UUID), + isArray: false, + isNullable: currentFieldMetadata.isNullable ?? true, + isUnique: + currentFieldMetadata.settings.relationType === + RelationType.ONE_TO_ONE, + defaultValue: null, + }, + alteredColumnDefinition: { + columnName: alteredJoinColumnName, + columnType: fieldMetadataTypeToColumnType(FieldMetadataType.UUID), + isArray: false, + isNullable: alteredFieldMetadata.isNullable ?? true, + isUnique: + alteredFieldMetadata.settings.relationType === + RelationType.ONE_TO_ONE, + defaultValue: null, + }, + }, + ]; + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.factory.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.factory.ts index c55602075..525125eaa 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.factory.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.factory.ts @@ -9,6 +9,7 @@ import { WorkspaceColumnActionOptions } from 'src/engine/metadata-modules/worksp import { BasicColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/basic-column-action.factory'; import { CompositeColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory'; import { EnumColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/enum-column-action.factory'; +import { RelationColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/relation-column-action.factory'; import { TsVectorColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/ts-vector-column-action.factory'; import { WorkspaceMigrationColumnAction, @@ -35,6 +36,7 @@ export class WorkspaceMigrationFactory { private readonly tsVectorColumnActionFactory: TsVectorColumnActionFactory, private readonly enumColumnActionFactory: EnumColumnActionFactory, private readonly compositeColumnActionFactory: CompositeColumnActionFactory, + private readonly relationColumnActionFactory: RelationColumnActionFactory, ) { this.factoriesMap = new Map< FieldMetadataType, @@ -98,6 +100,10 @@ export class WorkspaceMigrationFactory { FieldMetadataType.RICH_TEXT_V2, { factory: this.compositeColumnActionFactory }, ], + [ + FieldMetadataType.RELATION, + { factory: this.relationColumnActionFactory }, + ], ]); } diff --git a/packages/twenty-server/src/engine/twenty-orm/custom.workspace-entity.ts b/packages/twenty-server/src/engine/twenty-orm/custom.workspace-entity.ts index aff305b2d..a3b227c59 100644 --- a/packages/twenty-server/src/engine/twenty-orm/custom.workspace-entity.ts +++ b/packages/twenty-server/src/engine/twenty-orm/custom.workspace-entity.ts @@ -1,14 +1,13 @@ import { msg } from '@lingui/core/macro'; import { FieldMetadataType } from 'twenty-shared/types'; +import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface'; +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; + import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants'; import { ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; import { DEFAULT_LABEL_IDENTIFIER_FIELD_NAME } from 'src/engine/metadata-modules/object-metadata/object-metadata.constants'; -import { - RelationMetadataType, - RelationOnDeleteAction, -} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceCustomEntity } from 'src/engine/twenty-orm/decorators/workspace-custom-entity.decorator'; import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator'; @@ -67,7 +66,7 @@ export class CustomWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.noteTargets, label: msg`Notes`, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, description: (objectMetadata) => { const label = objectMetadata.labelSingular; @@ -83,7 +82,7 @@ export class CustomWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.taskTargets, label: msg`Tasks`, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, description: (objectMetadata) => { const label = objectMetadata.labelSingular; @@ -99,7 +98,7 @@ export class CustomWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.favorites, label: msg`Favorites`, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, description: (objectMetadata) => { const label = objectMetadata.labelSingular; @@ -116,7 +115,7 @@ export class CustomWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.attachments, label: msg`Attachments`, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, description: (objectMetadata) => { const label = objectMetadata.labelSingular; @@ -132,7 +131,7 @@ export class CustomWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.timelineActivities, label: msg`Timeline Activities`, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, description: (objectMetadata) => { const label = objectMetadata.labelSingular; diff --git a/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-dynamic-relation.decorator.ts b/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-dynamic-relation.decorator.ts index ad36ef2b8..25a57f880 100644 --- a/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-dynamic-relation.decorator.ts +++ b/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-dynamic-relation.decorator.ts @@ -1,16 +1,14 @@ import { ObjectType } from 'typeorm'; +import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface'; +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { WorkspaceDynamicRelationMetadataArgsFactory } from 'src/engine/twenty-orm/interfaces/workspace-dynamic-relation-metadata-args.interface'; -import { - RelationMetadataType, - RelationOnDeleteAction, -} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage'; import { TypedReflect } from 'src/utils/typed-reflect'; interface WorkspaceBaseDynamicRelationOptions { - type: RelationMetadataType; + type: RelationType; argsFactory: WorkspaceDynamicRelationMetadataArgsFactory; inverseSideTarget: () => ObjectType; inverseSideFieldKey?: keyof TClass; diff --git a/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-field-index.decorator.ts b/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-field-index.decorator.ts index d4a748f91..d67f8089b 100644 --- a/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-field-index.decorator.ts +++ b/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-field-index.decorator.ts @@ -28,6 +28,15 @@ export function WorkspaceFieldIndex( ...additionalDefaultColumnsForIndex, ]; + // TODO: Remove this when we are handling properly indexes for new relation metadata + if ( + process.env.SYNC_METADATA_INDEX_ENABLED === 'false' || + process.env.SYNC_METADATA_INDEX_ENABLED === '' || + process.env.SYNC_METADATA_INDEX_ENABLED === undefined + ) { + return; + } + metadataArgsStorage.addIndexes({ name: `IDX_${generateDeterministicIndexName([ convertClassNameToObjectMetadataName(target.constructor.name), diff --git a/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-field.decorator.ts b/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-field.decorator.ts index 1edab6cb5..a1815f77d 100644 --- a/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-field.decorator.ts +++ b/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-field.decorator.ts @@ -11,7 +11,11 @@ import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args import { TypedReflect } from 'src/utils/typed-reflect'; export interface WorkspaceFieldOptions< - T extends FieldMetadataType = FieldMetadataType, + T extends FieldMetadataType = Exclude< + FieldMetadataType, + // Use @WorkspaceRelation or @WorkspaceDynamicRelation for relation fields + FieldMetadataType.RELATION + >, > { standardId: string; type: T; diff --git a/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-index.decorator.ts b/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-index.decorator.ts index 02690b201..aedb74158 100644 --- a/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-index.decorator.ts +++ b/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-index.decorator.ts @@ -18,6 +18,15 @@ export function WorkspaceIndex( throw new Error('Class level WorkspaceIndex should be used with columns'); } + // TODO: Remove this when we are handling properly indexes for new relation metadata + if ( + process.env.SYNC_METADATA_INDEX_ENABLED === 'false' || + process.env.SYNC_METADATA_INDEX_ENABLED === '' || + process.env.SYNC_METADATA_INDEX_ENABLED === undefined + ) { + return (_target: any) => {}; + } + return (target: any) => { const gate = TypedReflect.getMetadata( 'workspace:gate-metadata-args', diff --git a/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-is-unique.decorator.ts b/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-is-unique.decorator.ts index 179aa1390..051142145 100644 --- a/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-is-unique.decorator.ts +++ b/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-is-unique.decorator.ts @@ -17,17 +17,20 @@ export function WorkspaceIsUnique(): PropertyDecorator { const columns = [propertyKey.toString()]; - metadataArgsStorage.addIndexes({ - name: `IDX_UNIQUE_${generateDeterministicIndexName([ - convertClassNameToObjectMetadataName(target.constructor.name), - ...columns, - ])}`, - columns, - target: target.constructor, - gate, - isUnique: true, - whereClause: null, - }); + // TODO: Remove this when we are handling properly indexes for new relation metadata + if (process.env.SYNC_METADATA_INDEX_ENABLED === 'true') { + metadataArgsStorage.addIndexes({ + name: `IDX_UNIQUE_${generateDeterministicIndexName([ + convertClassNameToObjectMetadataName(target.constructor.name), + ...columns, + ])}`, + columns, + target: target.constructor, + gate, + isUnique: true, + whereClause: null, + }); + } return TypedReflect.defineMetadata( 'workspace:is-unique-metadata-args', diff --git a/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-relation.decorator.ts b/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-relation.decorator.ts index 0402a5b79..3c3e58f5e 100644 --- a/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-relation.decorator.ts +++ b/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-relation.decorator.ts @@ -1,15 +1,14 @@ import { MessageDescriptor } from '@lingui/core'; import { ObjectType } from 'typeorm'; +import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface'; +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; + import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { - RelationMetadataType, - RelationOnDeleteAction, -} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage'; import { TypedReflect } from 'src/utils/typed-reflect'; -interface WorkspaceRelationOptions { +interface WorkspaceRelationBaseOptions { standardId: string; label: | MessageDescriptor @@ -18,12 +17,26 @@ interface WorkspaceRelationOptions { | MessageDescriptor | ((objectMetadata: ObjectMetadataEntity) => MessageDescriptor); icon?: string; - type: RelationMetadataType; inverseSideTarget: () => ObjectType; inverseSideFieldKey?: keyof TClass; onDelete?: RelationOnDeleteAction; } +interface WorkspaceOtherRelationOptions + extends WorkspaceRelationBaseOptions { + type: RelationType.ONE_TO_MANY | RelationType.ONE_TO_ONE; +} + +interface WorkspaceManyToOneRelationOptions + extends WorkspaceRelationBaseOptions { + type: RelationType.MANY_TO_ONE; + inverseSideFieldKey: keyof TClass; +} + +type WorkspaceRelationOptions = + | WorkspaceOtherRelationOptions + | WorkspaceManyToOneRelationOptions; + export function WorkspaceRelation( options: WorkspaceRelationOptions, ): PropertyDecorator { diff --git a/packages/twenty-server/src/engine/twenty-orm/exceptions/twenty-orm.exception.ts b/packages/twenty-server/src/engine/twenty-orm/exceptions/twenty-orm.exception.ts index c71f1d395..f1412d894 100644 --- a/packages/twenty-server/src/engine/twenty-orm/exceptions/twenty-orm.exception.ts +++ b/packages/twenty-server/src/engine/twenty-orm/exceptions/twenty-orm.exception.ts @@ -14,4 +14,5 @@ export enum TwentyORMExceptionCode { ROLES_PERMISSIONS_VERSION_NOT_FOUND = 'ROLES_PERMISSIONS_VERSION_NOT_FOUND', FEATURE_FLAG_MAP_VERSION_NOT_FOUND = 'FEATURE_FLAG_MAP_VERSION_NOT_FOUND', USER_WORKSPACE_ROLE_MAP_VERSION_NOT_FOUND = 'USER_WORKSPACE_ROLE_MAP_VERSION_NOT_FOUND', + MALFORMED_METADATA = 'MALFORMED_METADATA', } diff --git a/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-column.factory.ts b/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-column.factory.ts index 4fac3daba..0bb934f6d 100644 --- a/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-column.factory.ts +++ b/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-column.factory.ts @@ -1,9 +1,11 @@ import { Injectable } from '@nestjs/common'; -import { ColumnType, EntitySchemaColumnOptions } from 'typeorm'; import { FieldMetadataType } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; +import { ColumnType, EntitySchemaColumnOptions } from 'typeorm'; import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; @@ -12,7 +14,11 @@ import { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metad import { serializeDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-default-value'; import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util'; -import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; +import { + TwentyORMException, + TwentyORMExceptionCode, +} from 'src/engine/twenty-orm/exceptions/twenty-orm.exception'; +import { isFieldMetadataInterfaceOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; type EntitySchemaColumnMap = { [key: string]: EntitySchemaColumnOptions; @@ -20,7 +26,10 @@ type EntitySchemaColumnMap = { @Injectable() export class EntitySchemaColumnFactory { - create(fieldMetadataMapByName: FieldMetadataMap): EntitySchemaColumnMap { + create( + fieldMetadataMapByName: FieldMetadataMap, + isNewRelationEnabled: boolean, + ): EntitySchemaColumnMap { let entitySchemaColumnMap: EntitySchemaColumnMap = {}; const fieldMetadataCollection = Object.values(fieldMetadataMapByName); @@ -28,33 +37,63 @@ export class EntitySchemaColumnFactory { for (const fieldMetadata of fieldMetadataCollection) { const key = fieldMetadata.name; - if (isRelationFieldMetadataType(fieldMetadata.type)) { - const relationMetadata = - fieldMetadata.fromRelationMetadata ?? - fieldMetadata.toRelationMetadata; - - if (!relationMetadata) { - throw new Error( - `Relation metadata is missing for field ${fieldMetadata.name}`, - ); - } - - const joinColumnKey = fieldMetadata.name + 'Id'; - const joinColumn = fieldMetadataCollection.find( - (field) => field.name === joinColumnKey, + if ( + isFieldMetadataInterfaceOfType( + fieldMetadata, + FieldMetadataType.RELATION, ) - ? joinColumnKey - : null; + ) { + if (!isNewRelationEnabled) { + const relationMetadata = + fieldMetadata.fromRelationMetadata ?? + fieldMetadata.toRelationMetadata; - if (joinColumn) { - entitySchemaColumnMap[joinColumn] = { - name: joinColumn, + if (!relationMetadata) { + throw new Error( + `Relation metadata is missing for field ${fieldMetadata.name}`, + ); + } + + const joinColumnKey = fieldMetadata.name + 'Id'; + const joinColumn = fieldMetadataCollection.find( + (field) => field.name === joinColumnKey, + ) + ? joinColumnKey + : null; + + if (joinColumn) { + entitySchemaColumnMap[joinColumn] = { + name: joinColumn, + type: 'uuid', + nullable: fieldMetadata.isNullable, + }; + } + + continue; + } else { + const isManyToOneRelation = + fieldMetadata.settings?.relationType === RelationType.MANY_TO_ONE; + const joinColumnName = fieldMetadata.settings?.joinColumnName; + + if (!isManyToOneRelation) { + continue; + } + + if (!isDefined(joinColumnName)) { + throw new TwentyORMException( + `Field ${fieldMetadata.id} of type ${fieldMetadata.type} is a many to one relation but does not have a join column name`, + TwentyORMExceptionCode.MALFORMED_METADATA, + ); + } + + entitySchemaColumnMap[joinColumnName] = { + name: joinColumnName, type: 'uuid', nullable: fieldMetadata.isNullable, }; - } - continue; + continue; + } } if (isCompositeFieldMetadataType(fieldMetadata.type)) { diff --git a/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-relation.factory.ts b/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-relation.factory.ts index 4d86fce6f..011089746 100644 --- a/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-relation.factory.ts +++ b/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-relation.factory.ts @@ -1,11 +1,13 @@ import { Injectable } from '@nestjs/common'; +import { FieldMetadataType } from 'twenty-shared/types'; import { EntitySchemaRelationOptions } from 'typeorm'; import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { determineRelationDetails } from 'src/engine/twenty-orm/utils/determine-relation-details.util'; -import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; +import { determineSchemaRelationDetails } from 'src/engine/twenty-orm/utils/determine-schema-relation-details.util'; +import { isFieldMetadataInterfaceOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; type EntitySchemaRelationMap = { [key: string]: EntitySchemaRelationOptions; @@ -18,37 +20,64 @@ export class EntitySchemaRelationFactory { async create( fieldMetadataMapByName: FieldMetadataMap, objectMetadataMaps: ObjectMetadataMaps, + isNewRelationEnabled: boolean, ): Promise { const entitySchemaRelationMap: EntitySchemaRelationMap = {}; const fieldMetadataCollection = Object.values(fieldMetadataMapByName); for (const fieldMetadata of fieldMetadataCollection) { - if (!isRelationFieldMetadataType(fieldMetadata.type)) { + if ( + !isFieldMetadataInterfaceOfType( + fieldMetadata, + FieldMetadataType.RELATION, + ) + ) { continue; } - const relationMetadata = - fieldMetadata.fromRelationMetadata ?? fieldMetadata.toRelationMetadata; + if (!isNewRelationEnabled) { + const relationMetadata = + fieldMetadata.fromRelationMetadata ?? + fieldMetadata.toRelationMetadata; - if (!relationMetadata) { - throw new Error( - `Relation metadata is missing for field ${fieldMetadata.name}`, + if (!relationMetadata) { + throw new Error( + `Relation metadata is missing for field ${fieldMetadata.name}`, + ); + } + + const relationDetails = await determineRelationDetails( + fieldMetadata, + relationMetadata, + objectMetadataMaps, ); + + entitySchemaRelationMap[fieldMetadata.name] = { + type: relationDetails.relationType, + target: relationDetails.target, + inverseSide: relationDetails.inverseSide, + joinColumn: relationDetails.joinColumn, + } satisfies EntitySchemaRelationOptions; + } else { + if (!fieldMetadata.settings) { + throw new Error( + `Field metadata settings are missing for field ${fieldMetadata.name}`, + ); + } + + const schemaRelationDetails = await determineSchemaRelationDetails( + fieldMetadata, + objectMetadataMaps, + ); + + entitySchemaRelationMap[fieldMetadata.name] = { + type: schemaRelationDetails.relationType, + target: schemaRelationDetails.target, + inverseSide: schemaRelationDetails.inverseSide, + joinColumn: schemaRelationDetails.joinColumn, + } satisfies EntitySchemaRelationOptions; } - - const relationDetails = await determineRelationDetails( - fieldMetadata, - relationMetadata, - objectMetadataMaps, - ); - - entitySchemaRelationMap[fieldMetadata.name] = { - type: relationDetails.relationType, - target: relationDetails.target, - inverseSide: relationDetails.inverseSide, - joinColumn: relationDetails.joinColumn, - } satisfies EntitySchemaRelationOptions; } return entitySchemaRelationMap; diff --git a/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema.factory.ts b/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema.factory.ts index 2d71c065e..635058db7 100644 --- a/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema.factory.ts +++ b/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema.factory.ts @@ -2,6 +2,8 @@ import { Injectable } from '@nestjs/common'; import { EntitySchema } from 'typeorm'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { EntitySchemaColumnFactory } from 'src/engine/twenty-orm/factories/entity-schema-column.factory'; @@ -14,6 +16,7 @@ export class EntitySchemaFactory { constructor( private readonly entitySchemaColumnFactory: EntitySchemaColumnFactory, private readonly entitySchemaRelationFactory: EntitySchemaRelationFactory, + private readonly featureFlagService: FeatureFlagService, ) {} async create( @@ -22,13 +25,20 @@ export class EntitySchemaFactory { objectMetadata: ObjectMetadataItemWithFieldMaps, objectMetadataMaps: ObjectMetadataMaps, ): Promise { + const isNewRelationEnabled = await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IsNewRelationEnabled, + workspaceId, + ); + const columns = this.entitySchemaColumnFactory.create( objectMetadata.fieldsByName, + isNewRelationEnabled, ); const relations = await this.entitySchemaRelationFactory.create( objectMetadata.fieldsByName, objectMetadataMaps, + isNewRelationEnabled, ); const entitySchema = new EntitySchema({ diff --git a/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts b/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts index 876a02d86..0a22d1efb 100644 --- a/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts +++ b/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts @@ -147,6 +147,7 @@ export class WorkspaceDatasourceFactory { { workspaceId, objectMetadataMaps: cachedObjectMetadataMaps, + featureFlagsMap: cachedFeatureFlagMap, }, { url: diff --git a/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-dynamic-relation-metadata-args.interface.ts b/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-dynamic-relation-metadata-args.interface.ts index 18533f00c..c9823dfc5 100644 --- a/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-dynamic-relation-metadata-args.interface.ts +++ b/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-dynamic-relation-metadata-args.interface.ts @@ -1,12 +1,10 @@ import { ObjectType } from 'typeorm'; +import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface'; +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { Gate } from 'src/engine/twenty-orm/interfaces/gate.interface'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { - RelationMetadataType, - RelationOnDeleteAction, -} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; export type WorkspaceDynamicRelationMetadataArgsFactory = ( oppositeObjectMetadata: ObjectMetadataEntity, @@ -57,7 +55,7 @@ export interface WorkspaceDynamicRelationMetadataArgs { /** * Relation type. */ - readonly type: RelationMetadataType; + readonly type: RelationType; /** * Relation inverse side target. diff --git a/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-internal-context.interface.ts b/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-internal-context.interface.ts index be7d9c712..55042285d 100644 --- a/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-internal-context.interface.ts +++ b/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-internal-context.interface.ts @@ -1,6 +1,8 @@ +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; export interface WorkspaceInternalContext { workspaceId: string; objectMetadataMaps: ObjectMetadataMaps; + featureFlagsMap: Record; } diff --git a/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-relation-metadata-args.interface.ts b/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-relation-metadata-args.interface.ts index 40de6f318..eb5a7c126 100644 --- a/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-relation-metadata-args.interface.ts +++ b/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-relation-metadata-args.interface.ts @@ -1,12 +1,10 @@ import { ObjectType } from 'typeorm'; +import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface'; +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { Gate } from 'src/engine/twenty-orm/interfaces/gate.interface'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { - RelationMetadataType, - RelationOnDeleteAction, -} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; export interface WorkspaceRelationMetadataArgs { /** @@ -33,7 +31,7 @@ export interface WorkspaceRelationMetadataArgs { /** * Relation type. */ - readonly type: RelationMetadataType; + readonly type: RelationType; /** * Relation description. diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts index 4b848a75d..dc45a7f95 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts @@ -715,7 +715,14 @@ export class WorkspaceRepository< objectMetadata ??= await this.getObjectMetadataFromTarget(); const objectMetadataMaps = this.internalContext.objectMetadataMaps; + const isNewRelationEnabled = + this.internalContext.featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled]; - return formatResult(data, objectMetadata, objectMetadataMaps) as T; + return formatResult( + data, + objectMetadata, + objectMetadataMaps, + isNewRelationEnabled, + ) as T; } } diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/convert-relation-type-to-typeorm-relation-type.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/convert-relation-type-to-typeorm-relation-type.util.ts new file mode 100644 index 000000000..deb671874 --- /dev/null +++ b/packages/twenty-server/src/engine/twenty-orm/utils/convert-relation-type-to-typeorm-relation-type.util.ts @@ -0,0 +1,14 @@ +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; + +export const converRelationTypeToTypeORMRelationType = (type: RelationType) => { + switch (type) { + case RelationType.ONE_TO_MANY: + return 'one-to-many'; + case RelationType.MANY_TO_ONE: + return 'many-to-one'; + case RelationType.ONE_TO_ONE: + return 'one-to-one'; + default: + throw new Error(`Invalid relation type: ${type}`); + } +}; diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/determine-schema-relation-details.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/determine-schema-relation-details.util.ts new file mode 100644 index 000000000..331c2a5b4 --- /dev/null +++ b/packages/twenty-server/src/engine/twenty-orm/utils/determine-schema-relation-details.util.ts @@ -0,0 +1,62 @@ +import { FieldMetadataType } from 'twenty-shared/types'; +import { RelationType } from 'typeorm/metadata/types/RelationTypes'; + +import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; + +import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { converRelationTypeToTypeORMRelationType } from 'src/engine/twenty-orm/utils/convert-relation-type-to-typeorm-relation-type.util'; + +interface RelationDetails { + relationType: RelationType; + target: string; + inverseSide: string; + joinColumn: { name: string } | undefined; +} + +export async function determineSchemaRelationDetails( + fieldMetadata: FieldMetadataInterface, + objectMetadataMaps: ObjectMetadataMaps, +): Promise { + if (!fieldMetadata.settings) { + throw new Error('Field metadata settings are missing'); + } + + const relationType = converRelationTypeToTypeORMRelationType( + fieldMetadata.settings.relationType, + ); + + if (!fieldMetadata.relationTargetObjectMetadataId) { + throw new Error('Relation target object metadata ID is missing'); + } + + const sourceObjectMetadata = + objectMetadataMaps.byId[fieldMetadata.objectMetadataId]; + const targetObjectMetadata = + objectMetadataMaps.byId[fieldMetadata.relationTargetObjectMetadataId]; + + if (!sourceObjectMetadata || !targetObjectMetadata) { + throw new Error('Object metadata not found'); + } + + if (!fieldMetadata.relationTargetFieldMetadataId) { + throw new Error('Relation target field metadata ID is missing'); + } + + const targetFieldMetadata = + targetObjectMetadata.fieldsById[ + fieldMetadata.relationTargetFieldMetadataId + ]; + + if (!targetFieldMetadata) { + throw new Error('Target field metadata not found'); + } + + return { + relationType, + target: targetObjectMetadata.nameSingular, + inverseSide: targetFieldMetadata.name, + joinColumn: fieldMetadata.settings.joinColumnName + ? { name: fieldMetadata.settings.joinColumnName } + : undefined, + }; +} diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/format-data.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/format-data.util.ts index f44a43542..ac9bb9805 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/format-data.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/format-data.util.ts @@ -7,6 +7,7 @@ import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-meta import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory'; +import { isFieldMetadataInterfaceOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; export function formatData( data: T, @@ -23,9 +24,28 @@ export function formatData( } const newData: Record = {}; + const fieldMetadataByJoinColumnName = + objectMetadataItemWithFieldMaps.fields.reduce((acc, fieldMetadata) => { + if ( + isFieldMetadataInterfaceOfType( + fieldMetadata, + FieldMetadataType.RELATION, + ) + ) { + const joinColumnName = fieldMetadata.settings?.joinColumnName; + + if (joinColumnName) { + acc.set(joinColumnName, fieldMetadata); + } + } + + return acc; + }, new Map()); for (const [key, value] of Object.entries(data)) { - const fieldMetadata = objectMetadataItemWithFieldMaps.fieldsByName[key]; + const fieldMetadata = + objectMetadataItemWithFieldMaps.fieldsByName[key] || + fieldMetadataByJoinColumnName.get(key); if (!fieldMetadata) { throw new Error( diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts index cc5931ce7..67294565f 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts @@ -13,7 +13,7 @@ import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/typ import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { computeRelationType } from 'src/engine/twenty-orm/utils/compute-relation-type.util'; import { getCompositeFieldMetadataCollection } from 'src/engine/twenty-orm/utils/get-composite-field-metadata-collection'; -import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; +import { isFieldMetadataInterfaceOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; import { isDate } from 'src/utils/date/isDate'; import { isValidDate } from 'src/utils/date/isValidDate'; @@ -21,6 +21,7 @@ export function formatResult( data: any, objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, objectMetadataMaps: ObjectMetadataMaps, + isNewRelationEnabled: boolean, ): T { if (!data) { return data; @@ -28,7 +29,12 @@ export function formatResult( if (Array.isArray(data)) { return data.map((item) => - formatResult(item, objectMetadataItemWithFieldMaps, objectMetadataMaps), + formatResult( + item, + objectMetadataItemWithFieldMaps, + objectMetadataMaps, + isNewRelationEnabled, + ), ) as T; } @@ -44,38 +50,57 @@ export function formatResult( objectMetadataItemWithFieldMaps, ); - const relationMetadataMap = new Map( - Object.values(objectMetadataItemWithFieldMaps.fieldsById) - .filter(({ type }) => isRelationFieldMetadataType(type)) - .map((fieldMetadata) => [ - fieldMetadata.name, - { - relationMetadata: - fieldMetadata.fromRelationMetadata ?? - fieldMetadata.toRelationMetadata, - relationType: computeRelationType( - fieldMetadata, - fieldMetadata.fromRelationMetadata ?? - (fieldMetadata.toRelationMetadata as RelationMetadataEntity), - ), - }, - ]), - ); + const relationMetadataMap: Map< + string, + { + relationMetadata: RelationMetadataEntity | undefined; + relationType: string; + } + > = isNewRelationEnabled + ? new Map() + : new Map( + Object.values(objectMetadataItemWithFieldMaps.fieldsById) + .filter((fieldMetadata) => + isFieldMetadataInterfaceOfType( + fieldMetadata, + FieldMetadataType.RELATION, + ), + ) + .map((fieldMetadata) => [ + fieldMetadata.name, + { + relationMetadata: + fieldMetadata.fromRelationMetadata ?? + fieldMetadata.toRelationMetadata, + relationType: computeRelationType( + fieldMetadata, + fieldMetadata.fromRelationMetadata ?? + (fieldMetadata.toRelationMetadata as RelationMetadataEntity), + ), + }, + ]), + ); const newData: object = {}; const objectMetadaItemFieldsByName = objectMetadataMaps.byId[objectMetadataItemWithFieldMaps.id]?.fieldsByName; for (const [key, value] of Object.entries(data)) { const compositePropertyArgs = compositeFieldMetadataMap.get(key); - const { relationMetadata, relationType } = - relationMetadataMap.get(key) ?? {}; + const fieldMetadata = objectMetadataItemWithFieldMaps.fieldsById[key]; + const isRelation = fieldMetadata + ? isFieldMetadataInterfaceOfType( + fieldMetadata, + FieldMetadataType.RELATION, + ) + : false; - if (!compositePropertyArgs && !relationMetadata) { + if (!compositePropertyArgs && !isRelation) { if (isPlainObject(value)) { newData[key] = formatResult( value, objectMetadataItemWithFieldMaps, objectMetadataMaps, + isNewRelationEnabled, ); } else if (objectMetadaItemFieldsByName[key]) { newData[key] = formatFieldMetadataValue( @@ -89,31 +114,63 @@ export function formatResult( continue; } - if (relationMetadata) { - const toObjectMetadata = - objectMetadataMaps.byId[relationMetadata.toObjectMetadataId]; + if (!isNewRelationEnabled) { + const { relationMetadata, relationType } = + relationMetadataMap.get(key) ?? {}; - const fromObjectMetadata = - objectMetadataMaps.byId[relationMetadata.fromObjectMetadataId]; + if (relationMetadata) { + const toObjectMetadata = + objectMetadataMaps.byId[relationMetadata.toObjectMetadataId]; - if (!toObjectMetadata) { - throw new Error( - `Object metadata for object metadataId "${relationMetadata.toObjectMetadataId}" is missing`, + const fromObjectMetadata = + objectMetadataMaps.byId[relationMetadata.fromObjectMetadataId]; + + if (!toObjectMetadata) { + throw new Error( + `Object metadata for object metadataId "${relationMetadata.toObjectMetadataId}" is missing`, + ); + } + + if (!fromObjectMetadata) { + throw new Error( + `Object metadata for object metadataId "${relationMetadata.fromObjectMetadataId}" is missing`, + ); + } + + newData[key] = formatResult( + value, + relationType === 'one-to-many' + ? toObjectMetadata + : fromObjectMetadata, + objectMetadataMaps, + isNewRelationEnabled, + ); + continue; + } + } else { + if (isRelation) { + if (!fieldMetadata.relationTargetObjectMetadataId) { + throw new Error( + `Relation target object metadata ID is missing for field "${key}"`, + ); + } + + const targetObjectMetadata = + objectMetadataMaps.byId[fieldMetadata.relationTargetObjectMetadataId]; + + if (!targetObjectMetadata) { + throw new Error( + `Object metadata for object metadataId "${fieldMetadata.relationTargetObjectMetadataId}" is missing`, + ); + } + + newData[key] = formatResult( + value, + targetObjectMetadata, + objectMetadataMaps, + isNewRelationEnabled, ); } - - if (!fromObjectMetadata) { - throw new Error( - `Object metadata for object metadataId "${relationMetadata.fromObjectMetadataId}" is missing`, - ); - } - - newData[key] = formatResult( - value, - relationType === 'one-to-many' ? toObjectMetadata : fromObjectMetadata, - objectMetadataMaps, - ); - continue; } if (!compositePropertyArgs) { diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/get-join-column.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/get-join-column.util.ts index a328b5a3f..e9a70d94a 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/get-join-column.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/get-join-column.util.ts @@ -1,7 +1,7 @@ +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { WorkspaceJoinColumnsMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-join-columns-metadata-args.interface'; import { WorkspaceRelationMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-relation-metadata-args.interface'; -import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage'; export const getJoinColumn = ( @@ -9,10 +9,7 @@ export const getJoinColumn = ( relationMetadataArgs: WorkspaceRelationMetadataArgs, opposite = false, ): string | null => { - if ( - relationMetadataArgs.type === RelationMetadataType.ONE_TO_MANY || - relationMetadataArgs.type === RelationMetadataType.MANY_TO_MANY - ) { + if (relationMetadataArgs.type === RelationType.ONE_TO_MANY) { return null; } @@ -41,7 +38,7 @@ export const getJoinColumn = ( // If we're in a ONE_TO_ONE relation and there are no join columns, we need to find the join column on the inverse side if ( - relationMetadataArgs.type === RelationMetadataType.ONE_TO_ONE && + relationMetadataArgs.type === RelationType.ONE_TO_ONE && filteredJoinColumnsMetadataArgsCollection.length === 0 && !opposite ) { diff --git a/packages/twenty-server/src/engine/utils/is-field-metadata-of-type.util.ts b/packages/twenty-server/src/engine/utils/is-field-metadata-of-type.util.ts index 0869461ac..0b06f1309 100644 --- a/packages/twenty-server/src/engine/utils/is-field-metadata-of-type.util.ts +++ b/packages/twenty-server/src/engine/utils/is-field-metadata-of-type.util.ts @@ -4,25 +4,22 @@ import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metada import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -export function isFieldMetadataOfType< +export function isFieldMetadataInterfaceOfType< Field extends FieldMetadataInterface, Type extends FieldMetadataType, >( fieldMetadata: Field, type: Type, -): fieldMetadata is Field & FieldMetadataInterface; -export function isFieldMetadataOfType< +): fieldMetadata is Field & FieldMetadataInterface { + return fieldMetadata.type === type; +} + +export function isFieldMetadataEntityOfType< Field extends FieldMetadataEntity, Type extends FieldMetadataType, >( fieldMetadata: Field, type: Type, -): fieldMetadata is Field & FieldMetadataEntity; -export function isFieldMetadataOfType< - Field extends - | FieldMetadataInterface - | FieldMetadataEntity, - Type extends FieldMetadataType, ->(fieldMetadata: Field, type: Type): boolean { +): fieldMetadata is Field & FieldMetadataEntity { return fieldMetadata.type === type; } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts index 83253b78c..f30efe033 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts @@ -93,9 +93,13 @@ export class WorkspaceManagerService { schemaName, ); + const featureFlags = + await this.featureFlagService.getWorkspaceFeatureFlagsMap(workspaceId); + await this.workspaceSyncMetadataService.synchronize({ workspaceId, dataSourceId: dataSourceMetadata.id, + featureFlags, }); const dataSourceMetadataCreationEnd = performance.now(); @@ -145,9 +149,13 @@ export class WorkspaceManagerService { schemaName, ); + const featureFlags = + await this.featureFlagService.getWorkspaceFeatureFlagsMap(workspaceId); + await this.workspaceSyncMetadataService.synchronize({ workspaceId, dataSourceId: dataSourceMetadata.id, + featureFlags, }); await this.prefillWorkspaceWithDemoObjects(dataSourceMetadata, workspaceId); @@ -165,9 +173,13 @@ export class WorkspaceManagerService { schemaName, ); + const featureFlags = + await this.featureFlagService.getWorkspaceFeatureFlagsMap(workspaceId); + await this.workspaceSyncMetadataService.synchronize({ workspaceId: workspaceId, dataSourceId: dataSourceMetadata.id, + featureFlags, }); await this.initPermissionsDev(workspaceId); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/index.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/index.ts index 5d5e66ac5..7714e1bd0 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/index.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/index.ts @@ -1,12 +1,14 @@ import { WorkspaceMigrationIndexFactory } from 'src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-index.factory'; -import { WorkspaceMigrationObjectFactory } from './workspace-migration-object.factory'; +import { WorkspaceMigrationFieldRelationFactory } from './workspace-migration-field-relation.factory'; import { WorkspaceMigrationFieldFactory } from './workspace-migration-field.factory'; +import { WorkspaceMigrationObjectFactory } from './workspace-migration-object.factory'; import { WorkspaceMigrationRelationFactory } from './workspace-migration-relation.factory'; export const workspaceMigrationBuilderFactories = [ WorkspaceMigrationObjectFactory, WorkspaceMigrationFieldFactory, + WorkspaceMigrationFieldRelationFactory, WorkspaceMigrationRelationFactory, WorkspaceMigrationIndexFactory, ]; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field-relation.factory.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field-relation.factory.ts new file mode 100644 index 000000000..04a9cbad6 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field-relation.factory.ts @@ -0,0 +1,388 @@ +import { Injectable } from '@nestjs/common'; + +import { FieldMetadataType } from 'twenty-shared/types'; + +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; +import { WorkspaceMigrationBuilderAction } from 'src/engine/workspace-manager/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface'; + +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util'; +import { + WorkspaceMigrationColumnActionType, + WorkspaceMigrationEntity, + WorkspaceMigrationTableAction, + WorkspaceMigrationTableActionType, +} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; +import { WorkspaceMigrationFactory } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.factory'; +import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; +import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; +import { FieldMetadataUpdate } from 'src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field.factory'; +import { camelCase } from 'src/utils/camel-case'; +@Injectable() +export class WorkspaceMigrationFieldRelationFactory { + constructor( + private readonly workspaceMigrationFactory: WorkspaceMigrationFactory, + ) {} + + async create( + originalObjectMetadataCollection: ObjectMetadataEntity[], + fieldMetadataCollection: FieldMetadataEntity[], + action: + | WorkspaceMigrationBuilderAction.CREATE + | WorkspaceMigrationBuilderAction.DELETE, + ): Promise[]>; + + async create( + originalObjectMetadataCollection: ObjectMetadataEntity[], + fieldMetadataUpdateCollection: FieldMetadataUpdate[], + action: WorkspaceMigrationBuilderAction.UPDATE, + ): Promise[]>; + + /** + * Deletion of the relation is handled by field deletion + */ + async create( + originalObjectMetadataCollection: ObjectMetadataEntity[], + fieldMetadataCollectionOrFieldMetadataUpdateCollection: + | FieldMetadataEntity[] + | FieldMetadataUpdate[], + action: WorkspaceMigrationBuilderAction, + ): Promise[]> { + const originalObjectMetadataMap = originalObjectMetadataCollection.reduce( + (result, currentObject) => { + result[currentObject.id] = currentObject; + + return result; + }, + {} as Record, + ); + + switch (action) { + case WorkspaceMigrationBuilderAction.CREATE: + return this.createFieldRelationMigration( + originalObjectMetadataMap, + fieldMetadataCollectionOrFieldMetadataUpdateCollection as FieldMetadataEntity[], + ); + case WorkspaceMigrationBuilderAction.UPDATE: + return this.updateFieldRelationMigration( + originalObjectMetadataMap, + fieldMetadataCollectionOrFieldMetadataUpdateCollection as FieldMetadataUpdate[], + ); + case WorkspaceMigrationBuilderAction.DELETE: + return this.deleteFieldRelationMigration( + originalObjectMetadataMap, + fieldMetadataCollectionOrFieldMetadataUpdateCollection as FieldMetadataEntity[], + ); + default: + return []; + } + } + + private async updateFieldRelationMigration( + originalObjectMetadataMap: Record, + fieldMetadataUpdateCollection: FieldMetadataUpdate[], + ): Promise[]> { + const workspaceMigrations: Partial[] = []; + + for (const { + altered: sourceFieldMetadata, + } of fieldMetadataUpdateCollection) { + const sourceObjectMetadata = + originalObjectMetadataMap[sourceFieldMetadata.objectMetadataId]; + const targetObjectMetadata = + originalObjectMetadataMap[ + sourceFieldMetadata.relationTargetObjectMetadataId + ]; + + if (!sourceObjectMetadata) { + throw new Error( + `ObjectMetadata with id ${sourceFieldMetadata.objectMetadataId} not found`, + ); + } + + if (!targetObjectMetadata) { + throw new Error( + `ObjectMetadata with id ${sourceFieldMetadata.relationTargetObjectMetadataId} not found`, + ); + } + + const targetFieldMetadata = targetObjectMetadata.fields.find( + (field) => + field.id === sourceFieldMetadata.relationTargetFieldMetadataId, + ); + + if (!targetFieldMetadata) { + throw new Error( + `FieldMetadata with id ${sourceFieldMetadata.relationTargetFieldMetadataId} not found`, + ); + } + + if ( + !isFieldMetadataEntityOfType( + targetFieldMetadata, + FieldMetadataType.RELATION, + ) + ) { + throw new Error( + `FieldMetadata with id ${sourceFieldMetadata.relationTargetFieldMetadataId} is not a relation`, + ); + } + + if (!targetFieldMetadata.settings) { + throw new Error( + `FieldMetadata for relation with id ${sourceFieldMetadata.id} has no settings`, + ); + } + + const migrations: WorkspaceMigrationTableAction[] = [ + { + name: computeObjectTargetTable(targetObjectMetadata), + action: WorkspaceMigrationTableActionType.ALTER, + columns: [ + { + action: WorkspaceMigrationColumnActionType.DROP_FOREIGN_KEY, + columnName: `${camelCase(targetFieldMetadata.name)}Id`, + }, + ], + }, + { + name: computeObjectTargetTable(targetObjectMetadata), + action: WorkspaceMigrationTableActionType.ALTER, + columns: [ + { + action: WorkspaceMigrationColumnActionType.CREATE_FOREIGN_KEY, + columnName: `${camelCase(targetFieldMetadata.name)}Id`, + referencedTableName: + computeObjectTargetTable(sourceObjectMetadata), + referencedTableColumnName: 'id', + isUnique: + targetFieldMetadata.settings.relationType === + RelationType.ONE_TO_ONE, + onDelete: targetFieldMetadata.settings.onDelete, + }, + ], + }, + ]; + + workspaceMigrations.push({ + workspaceId: sourceFieldMetadata.workspaceId, + name: generateMigrationName( + `update-relation-from-${sourceObjectMetadata.nameSingular}-to-${targetObjectMetadata.nameSingular}`, + ), + isCustom: false, + migrations, + }); + } + + return workspaceMigrations; + } + + private async createFieldRelationMigration( + originalObjectMetadataMap: Record, + fieldRelationMetadataCollection: FieldMetadataEntity[], + ): Promise[]> { + const workspaceMigrations: Partial[] = []; + + for (const sourceFieldMetadata of fieldRelationMetadataCollection) { + const sourceObjectMetadata = + originalObjectMetadataMap[sourceFieldMetadata.objectMetadataId]; + const targetObjectMetadata = + originalObjectMetadataMap[ + sourceFieldMetadata.relationTargetObjectMetadataId + ]; + + if (!sourceFieldMetadata.settings) { + throw new Error( + `FieldMetadata for relation with id ${sourceFieldMetadata.id} has no settings`, + ); + } + + // We're creating it from `ONE_TO_MANY` with the join column so we don't need to create a migration for `MANY_TO_ONE` + if ( + sourceFieldMetadata.settings.relationType === RelationType.MANY_TO_ONE + ) { + continue; + } + + if (!sourceObjectMetadata) { + throw new Error( + `ObjectMetadata with id ${sourceFieldMetadata.objectMetadataId} not found`, + ); + } + + if (!targetObjectMetadata) { + throw new Error( + `ObjectMetadata with id ${sourceFieldMetadata.relationTargetObjectMetadataId} not found`, + ); + } + + const targetFieldMetadata = targetObjectMetadata.fields.find( + (field) => + field.id === sourceFieldMetadata.relationTargetFieldMetadataId, + ); + + if (!targetFieldMetadata) { + throw new Error( + `FieldMetadata with id ${sourceFieldMetadata.relationTargetFieldMetadataId} not found`, + ); + } + + if ( + !isFieldMetadataEntityOfType( + targetFieldMetadata, + FieldMetadataType.RELATION, + ) + ) { + throw new Error( + `FieldMetadata with id ${sourceFieldMetadata.relationTargetFieldMetadataId} is not a relation`, + ); + } + + if (!targetFieldMetadata.settings) { + throw new Error( + `FieldMetadata for relation with id ${sourceFieldMetadata.id} has no settings`, + ); + } + + if (!targetFieldMetadata.settings.joinColumnName) { + continue; + } + + const migrations: WorkspaceMigrationTableAction[] = [ + { + name: computeObjectTargetTable(targetObjectMetadata), + action: WorkspaceMigrationTableActionType.ALTER, + columns: [ + ...this.workspaceMigrationFactory.createColumnActions( + WorkspaceMigrationColumnActionType.CREATE, + targetFieldMetadata, + ), + ], + }, + { + name: computeObjectTargetTable(targetObjectMetadata), + action: WorkspaceMigrationTableActionType.ALTER, + columns: [ + { + action: WorkspaceMigrationColumnActionType.CREATE_FOREIGN_KEY, + columnName: + targetFieldMetadata.settings.joinColumnName ?? + `${camelCase(targetFieldMetadata.name)}Id`, + referencedTableName: + computeObjectTargetTable(sourceObjectMetadata), + referencedTableColumnName: 'id', + isUnique: + targetFieldMetadata.settings.relationType === + RelationType.ONE_TO_ONE, + onDelete: targetFieldMetadata.settings.onDelete, + }, + ], + }, + ]; + + workspaceMigrations.push({ + workspaceId: sourceFieldMetadata.workspaceId, + name: generateMigrationName( + `create-relation-from-${sourceObjectMetadata.nameSingular}-to-${targetObjectMetadata.nameSingular}`, + ), + isCustom: false, + migrations, + }); + } + + return workspaceMigrations; + } + + private async deleteFieldRelationMigration( + originalObjectMetadataMap: Record, + fieldRelationMetadataCollection: FieldMetadataEntity[], + ): Promise[]> { + const workspaceMigrations: Partial[] = []; + + for (const sourceFieldMetadata of fieldRelationMetadataCollection) { + const sourceObjectMetadata = + originalObjectMetadataMap[sourceFieldMetadata.objectMetadataId]; + const targetObjectMetadata = + originalObjectMetadataMap[ + sourceFieldMetadata.relationTargetObjectMetadataId + ]; + + if (!sourceObjectMetadata) { + throw new Error( + `ObjectMetadata with id ${sourceFieldMetadata.objectMetadataId} not found`, + ); + } + + if (!targetObjectMetadata) { + throw new Error( + `ObjectMetadata with id ${sourceFieldMetadata.relationTargetObjectMetadataId} not found`, + ); + } + + const targetFieldMetadata = targetObjectMetadata.fields.find( + (field) => + field.id === sourceFieldMetadata.relationTargetFieldMetadataId, + ); + + if (!targetFieldMetadata) { + throw new Error( + `FieldMetadata with id ${sourceFieldMetadata.relationTargetFieldMetadataId} not found`, + ); + } + + if ( + !isFieldMetadataEntityOfType( + targetFieldMetadata, + FieldMetadataType.RELATION, + ) + ) { + throw new Error( + `FieldMetadata with id ${sourceFieldMetadata.relationTargetFieldMetadataId} is not a relation`, + ); + } + + if (!targetFieldMetadata.settings) { + throw new Error( + `FieldMetadata for relation with id ${sourceFieldMetadata.id} has no settings`, + ); + } + + const migrations: WorkspaceMigrationTableAction[] = [ + { + name: computeObjectTargetTable(targetObjectMetadata), + action: WorkspaceMigrationTableActionType.ALTER, + columns: [ + { + action: WorkspaceMigrationColumnActionType.DROP_FOREIGN_KEY, + columnName: `${camelCase(targetFieldMetadata.name)}Id`, + }, + ], + }, + { + name: computeObjectTargetTable(targetObjectMetadata), + action: WorkspaceMigrationTableActionType.ALTER, + columns: [ + { + action: WorkspaceMigrationColumnActionType.DROP, + columnName: + targetFieldMetadata.settings.joinColumnName ?? + `${camelCase(targetFieldMetadata.name)}Id`, + }, + ], + }, + ]; + + workspaceMigrations.push({ + workspaceId: sourceFieldMetadata.workspaceId, + name: generateMigrationName( + `update-relation-from-${sourceObjectMetadata.nameSingular}-to-${targetObjectMetadata.nameSingular}`, + ), + isCustom: false, + migrations, + }); + } + + return workspaceMigrations; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field.factory.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field.factory.ts index f2564c68b..5b687bb05 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field.factory.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field.factory.ts @@ -5,6 +5,8 @@ import { FieldMetadataType } from 'twenty-shared/types'; import { WorkspaceMigrationBuilderAction } from 'src/engine/workspace-manager/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util'; @@ -18,15 +20,18 @@ import { import { WorkspaceMigrationFactory } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.factory'; import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; -export interface FieldMetadataUpdate { - current: FieldMetadataEntity; - altered: FieldMetadataEntity; +export interface FieldMetadataUpdate< + Type extends FieldMetadataType = FieldMetadataType, +> { + current: FieldMetadataEntity; + altered: FieldMetadataEntity; } @Injectable() export class WorkspaceMigrationFieldFactory { constructor( private readonly workspaceMigrationFactory: WorkspaceMigrationFactory, + private readonly featureFlagService: FeatureFlagService, ) {} async create( @@ -86,6 +91,17 @@ export class WorkspaceMigrationFieldFactory { ): Promise[]> { const workspaceMigrations: Partial[] = []; + if (fieldMetadataCollection.length === 0) { + return []; + } + + const workspaceId = fieldMetadataCollection[0]?.workspaceId; + + const isNewRelationEnabled = await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IsNewRelationEnabled, + workspaceId, + ); + const fieldMetadataCollectionGroupByObjectMetadataId = fieldMetadataCollection.reduce( (result, currentFieldMetadata) => { @@ -110,7 +126,10 @@ export class WorkspaceMigrationFieldFactory { for (const fieldMetadata of fieldMetadataCollection) { // Relations are handled in workspace-migration-relation.factory.ts - if (fieldMetadata.type === FieldMetadataType.RELATION) { + if ( + !isNewRelationEnabled && + fieldMetadata.type === FieldMetadataType.RELATION + ) { continue; } @@ -127,7 +146,7 @@ export class WorkspaceMigrationFieldFactory { name: generateMigrationName( `create-${objectMetadata.nameSingular}-fields`, ), - isCustom: false, + isCustom: objectMetadata.isCustom, migrations: [ { name: computeObjectTargetTable( @@ -149,9 +168,23 @@ export class WorkspaceMigrationFieldFactory { ): Promise[]> { const workspaceMigrations: Partial[] = []; + if (fieldMetadataUpdateCollection.length === 0) { + return []; + } + + const workspaceId = fieldMetadataUpdateCollection[0]?.current.workspaceId; + + const isNewRelationEnabled = await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IsNewRelationEnabled, + workspaceId, + ); + for (const fieldMetadataUpdate of fieldMetadataUpdateCollection) { // Skip relations, because they're just representation and not real columns - if (fieldMetadataUpdate.altered.type === FieldMetadataType.RELATION) { + if ( + !isNewRelationEnabled && + fieldMetadataUpdate.altered.type === FieldMetadataType.RELATION + ) { continue; } @@ -211,7 +244,7 @@ export class WorkspaceMigrationFieldFactory { name: generateMigrationName( `update-${fieldMetadataUpdate.altered.name}`, ), - isCustom: false, + isCustom: fieldMetadataUpdate.altered.isCustom, migrations, }); } @@ -250,7 +283,7 @@ export class WorkspaceMigrationFieldFactory { workspaceMigrations.push({ workspaceId: fieldMetadata.workspaceId, name: generateMigrationName(`delete-${fieldMetadata.name}`), - isCustom: false, + isCustom: fieldMetadata.isCustom, migrations, }); } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-object.factory.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-object.factory.ts index a557d5f54..dca4a5e45 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-object.factory.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-object.factory.ts @@ -109,7 +109,7 @@ export class WorkspaceMigrationObjectFactory { workspaceMigrations.push({ workspaceId: objectMetadata.workspaceId, name: generateMigrationName(`create-${objectMetadata.nameSingular}`), - isCustom: false, + isCustom: objectMetadata.isCustom, migrations, }); } @@ -136,7 +136,7 @@ export class WorkspaceMigrationObjectFactory { name: generateMigrationName( `rename-${objectMetadataUpdate.current.nameSingular}`, ), - isCustom: false, + isCustom: objectMetadataUpdate.altered.isCustom, migrations: [ { name: oldTableName, @@ -167,7 +167,7 @@ export class WorkspaceMigrationObjectFactory { workspaceMigrations.push({ workspaceId: objectMetadata.workspaceId, name: generateMigrationName(`delete-${objectMetadata.nameSingular}`), - isCustom: false, + isCustom: objectMetadata.isCustom, migrations: [ ...(relationMetadataCollection ?? []).map( (relationMetadata) => diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/workspace-migration-builder.module.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/workspace-migration-builder.module.ts index cdf08d16d..0b1645cec 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/workspace-migration-builder.module.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/workspace-migration-builder.module.ts @@ -1,11 +1,12 @@ import { Module } from '@nestjs/common'; +import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module'; import { workspaceMigrationBuilderFactories } from './factories'; @Module({ - imports: [WorkspaceMigrationModule], + imports: [WorkspaceMigrationModule, FeatureFlagModule], providers: [...workspaceMigrationBuilderFactories], exports: [...workspaceMigrationBuilderFactories], }) diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-runner/utils/convert-on-delete-action-to-on-delete.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-runner/utils/convert-on-delete-action-to-on-delete.util.ts index 73e36ba67..345b68832 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-runner/utils/convert-on-delete-action-to-on-delete.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-runner/utils/convert-on-delete-action-to-on-delete.util.ts @@ -3,6 +3,10 @@ import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-met export const convertOnDeleteActionToOnDelete = ( onDeleteAction: RelationOnDeleteAction | undefined, ): 'CASCADE' | 'SET NULL' | 'RESTRICT' | 'NO ACTION' | undefined => { + if (!onDeleteAction) { + return undefined; + } + switch (onDeleteAction) { case 'CASCADE': return 'CASCADE'; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command.ts index 25191119b..6b306f076 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command.ts @@ -7,6 +7,7 @@ import { ActiveOrSuspendedWorkspacesMigrationCommandRunner, RunOnWorkspaceArgs, } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; @@ -25,6 +26,7 @@ export class SyncWorkspaceMetadataCommand extends ActiveOrSuspendedWorkspacesMig private readonly workspaceSyncMetadataService: WorkspaceSyncMetadataService, private readonly dataSourceService: DataSourceService, private readonly syncWorkspaceLoggerService: SyncWorkspaceLoggerService, + private readonly featureFlagService: FeatureFlagService, protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, ) { super(workspaceRepository, twentyORMGlobalManager); @@ -45,11 +47,15 @@ export class SyncWorkspaceMetadataCommand extends ActiveOrSuspendedWorkspacesMig workspaceId, ); + const featureFlags = + await this.featureFlagService.getWorkspaceFeatureFlagsMap(workspaceId); + const { storage, workspaceMigrations } = await this.workspaceSyncMetadataService.synchronize( { workspaceId, dataSourceId: dataSourceMetadata.id, + featureFlags, }, { applyChanges: !options.dryRun }, ); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/commands/workspace-sync-metadata-commands.module.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/commands/workspace-sync-metadata-commands.module.ts index 5a753043c..31e6ef53f 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/commands/workspace-sync-metadata-commands.module.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/commands/workspace-sync-metadata-commands.module.ts @@ -1,6 +1,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; @@ -19,6 +20,7 @@ import { SyncWorkspaceMetadataCommand } from './sync-workspace-metadata.command' WorkspaceModule, DataSourceModule, WorkspaceDataSourceModule, + FeatureFlagModule, TypeOrmModule.forFeature([Workspace], 'core'), SyncWorkspaceLoggerModule, ], diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/comparators/index.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/comparators/index.ts index 2f29fbf41..5b869691e 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/comparators/index.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/comparators/index.ts @@ -1,3 +1,4 @@ +import { WorkspaceFieldRelationComparator } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-field-relation.comparator'; import { WorkspaceIndexComparator } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-index.comparator'; import { WorkspaceFieldComparator } from './workspace-field.comparator'; @@ -6,6 +7,7 @@ import { WorkspaceRelationComparator } from './workspace-relation.comparator'; export const workspaceSyncMetadataComparators = [ WorkspaceFieldComparator, + WorkspaceFieldRelationComparator, WorkspaceObjectComparator, WorkspaceRelationComparator, WorkspaceIndexComparator, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-field-relation.comparator.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-field-relation.comparator.ts new file mode 100644 index 000000000..2277e6b08 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-field-relation.comparator.ts @@ -0,0 +1,250 @@ +import { Injectable } from '@nestjs/common'; + +import diff, { DifferenceChange, DifferenceRemove } from 'microdiff'; +import { FieldMetadataType } from 'twenty-shared/types'; + +import { FieldMetadataRelationSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface'; +import { + ComparatorAction, + FieldRelationComparatorResult, +} from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface'; + +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { transformMetadataForComparison } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/utils/transform-metadata-for-comparison.util'; + +const fieldPropertiesToCompare = [ + 'settings', + 'relationTargetObjectMetadataId', + 'relationTargetFieldMetadataId', +]; + +const fieldPropertiesToStringify = ['settings'] as const; + +@Injectable() +export class WorkspaceFieldRelationComparator { + constructor() {} + + public compare( + originalFieldMetadataCollection: FieldMetadataEntity[], + standardFieldMetadataCollection: FieldMetadataEntity[], + ): FieldRelationComparatorResult[] { + const result: FieldRelationComparatorResult[] = []; + const propertiesMap: Record< + string, + Partial> + > = {}; + + // Double security to only compare non-custom fields + const filteredOriginalFieldCollection = + originalFieldMetadataCollection.filter((field) => !field.isCustom); + const originalFieldMetadataMap = transformMetadataForComparison( + filteredOriginalFieldCollection, + { + shouldIgnoreProperty: (property) => { + if (fieldPropertiesToCompare.includes(property)) { + return false; + } + + return true; + }, + propertiesToStringify: fieldPropertiesToStringify, + keyFactory(datum) { + // Happen when the field is custom + return datum.standardId || datum.name; + }, + }, + ); + const standardFieldMetadataMap = transformMetadataForComparison( + standardFieldMetadataCollection, + { + shouldIgnoreProperty: (property) => { + if (fieldPropertiesToCompare.includes(property)) { + return false; + } + + return true; + }, + propertiesToStringify: fieldPropertiesToStringify, + keyFactory(datum) { + // Happen when the field is custom + return datum.standardId || datum.name; + }, + }, + ); + + // Compare fields + const fieldMetadataDifference = diff( + originalFieldMetadataMap, + standardFieldMetadataMap, + ); + + const fieldMetadataDifferenceMap = fieldMetadataDifference.reduce( + (acc, difference) => { + const fieldId = difference.path[0]; + + if (difference.type === 'CREATE') { + throw new Error( + `CREATE should never happen when comparing relations as they are created previously`, + ); + } + + if (!acc[fieldId]) { + acc[fieldId] = []; + } + + acc[fieldId].push(difference); + + return acc; + }, + {} as Record, + ); + + differenceLoop: for (const [fieldId, differences] of Object.entries( + fieldMetadataDifferenceMap, + )) { + const findField = ( + field: FieldMetadataEntity, + ) => { + return field.standardId === fieldId; + }; + // Object shouldn't have thousands of fields, so we can use find here + const standardFieldMetadata = + standardFieldMetadataCollection.find(findField); + const originalFieldMetadata = + originalFieldMetadataCollection.find(findField); + const allNewPropertiesAreNull = Object.values(differences).every( + (difference) => { + if (difference.type === 'REMOVE') { + return true; + } + + return difference.value === null; + }, + ); + const allOldPropertiesAreNull = Object.values(differences).every( + (difference) => difference.oldValue === null, + ); + let relationTypeChange = false; + + if (!originalFieldMetadata) { + throw new Error(`Field ${fieldId} not found in originalObjectMetadata`); + } + + if (!standardFieldMetadata) { + throw new Error(`Field ${fieldId} not found in standardObjectMetadata`); + } + + for (const difference of differences) { + const property = difference.path[difference.path.length - 1]; + + if (difference.type === 'REMOVE') { + // Whole relation is removed + if (property === fieldId) { + result.push({ + action: ComparatorAction.DELETE, + object: originalFieldMetadata, + }); + + continue differenceLoop; + } + + throw new Error( + `REMOVE partial part of relation should never happen, it should ben an update`, + ); + } + + // If the old value and the new value are both null, skip + // Database is storing null, and we can get undefined here + if ( + difference.oldValue === null && + (difference.value === null || difference.value === undefined) + ) { + break; + } + + if (typeof property !== 'string') { + break; + } + + if (!propertiesMap[fieldId]) { + propertiesMap[fieldId] = {}; + } + + // If the property is a stringified JSON, parse it + if ( + (fieldPropertiesToStringify as readonly string[]).includes(property) + ) { + const newValue = this.parseJSONOrString(difference.value); + + if (property === 'settings' && difference.oldValue) { + const newSettings = newValue as FieldMetadataRelationSettings; + const oldSettings = + difference.oldValue as FieldMetadataRelationSettings; + + // Check if the relation type has changed + if (oldSettings.relationType !== newSettings.relationType) { + relationTypeChange = true; + } + } + + propertiesMap[fieldId][property] = newValue; + } else { + propertiesMap[fieldId][property] = difference.value; + } + } + + if (relationTypeChange) { + result.push({ + action: ComparatorAction.DELETE, + object: originalFieldMetadata, + }); + + result.push({ + action: ComparatorAction.CREATE, + object: { + ...propertiesMap[fieldId], + id: originalFieldMetadata.id, + standardId: standardFieldMetadata.standardId ?? undefined, + }, + }); + } else if (allOldPropertiesAreNull) { + result.push({ + action: ComparatorAction.CREATE, + object: { + ...propertiesMap[fieldId], + id: originalFieldMetadata.id, + standardId: standardFieldMetadata.standardId ?? undefined, + }, + }); + } else if (allNewPropertiesAreNull) { + result.push({ + action: ComparatorAction.DELETE, + object: originalFieldMetadata, + }); + } else { + result.push({ + action: ComparatorAction.UPDATE, + object: { + ...propertiesMap[fieldId], + id: originalFieldMetadata.id, + standardId: standardFieldMetadata.standardId ?? undefined, + }, + }); + } + } + + return result; + } + + private parseJSONOrString(value: string | null): string | object | null { + if (value === null) { + return null; + } + + try { + return JSON.parse(value); + } catch { + return value; + } + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/index.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/index.ts index 8b11b063e..b4610d4a5 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/index.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/index.ts @@ -1,5 +1,6 @@ import { StandardIndexFactory } from 'src/engine/workspace-manager/workspace-sync-metadata/factories/standard-index.factory'; +import { StandardFieldRelationFactory } from './standard-field-relation.factory'; import { StandardFieldFactory } from './standard-field.factory'; import { StandardObjectFactory } from './standard-object.factory'; import { StandardRelationFactory } from './standard-relation.factory'; @@ -8,5 +9,6 @@ export const workspaceSyncMetadataFactories = [ StandardFieldFactory, StandardObjectFactory, StandardRelationFactory, + StandardFieldRelationFactory, StandardIndexFactory, ]; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-field-relation.factory.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-field-relation.factory.ts new file mode 100644 index 000000000..733f353bf --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-field-relation.factory.ts @@ -0,0 +1,186 @@ +import { Injectable } from '@nestjs/common'; + +import { FieldMetadataType } from 'twenty-shared/types'; + +import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface'; + +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; +import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage'; +import { getJoinColumn } from 'src/engine/twenty-orm/utils/get-join-column.util'; +import { convertClassNameToObjectMetadataName } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/convert-class-to-object-metadata-name.util'; +import { isGatedAndNotEnabled } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/is-gate-and-not-enabled.util'; +import { assert } from 'src/utils/assert'; + +interface CustomRelationFactory { + object: ObjectMetadataEntity; + metadata: typeof BaseWorkspaceEntity; +} + +@Injectable() +export class StandardFieldRelationFactory { + createFieldRelationForCustomObject( + customObjectFactories: CustomRelationFactory[], + context: WorkspaceSyncContext, + originalObjectMetadataMap: Record, + ): FieldMetadataEntity[] { + return customObjectFactories.flatMap((customObjectFactory) => + this.updateFieldRelationMetadata( + customObjectFactory, + context, + originalObjectMetadataMap, + ), + ); + } + + createFieldRelationForStandardObject( + standardObjectMetadataDefinitions: (typeof BaseWorkspaceEntity)[], + context: WorkspaceSyncContext, + originalObjectMetadataMap: Record, + ): Map[]> { + return standardObjectMetadataDefinitions.reduce( + (acc, standardObjectMetadata) => { + const workspaceEntityMetadataArgs = metadataArgsStorage.filterEntities( + standardObjectMetadata, + ); + + if (!workspaceEntityMetadataArgs) { + return acc; + } + + if ( + isGatedAndNotEnabled( + workspaceEntityMetadataArgs.gate, + context.featureFlags, + ) + ) { + return acc; + } + + acc.set( + workspaceEntityMetadataArgs.standardId, + this.updateFieldRelationMetadata( + standardObjectMetadata, + context, + originalObjectMetadataMap, + ), + ); + + return acc; + }, + new Map[]>(), + ); + } + + private updateFieldRelationMetadata( + workspaceEntityOrCustomRelationFactory: + | typeof BaseWorkspaceEntity + | CustomRelationFactory, + context: WorkspaceSyncContext, + originalObjectMetadataMap: Record, + ): FieldMetadataEntity[] { + const target = + 'metadata' in workspaceEntityOrCustomRelationFactory + ? workspaceEntityOrCustomRelationFactory.metadata + : workspaceEntityOrCustomRelationFactory; + const workspaceEntity = + 'metadata' in workspaceEntityOrCustomRelationFactory + ? metadataArgsStorage.filterExtendedEntities(target) + : metadataArgsStorage.filterEntities(target); + const workspaceRelationMetadataArgsCollection = + metadataArgsStorage.filterRelations(target); + + if (!workspaceEntity) { + throw new Error( + `Object metadata decorator not found, can't parse ${target.name}`, + ); + } + + if ( + !workspaceRelationMetadataArgsCollection || + isGatedAndNotEnabled(workspaceEntity?.gate, context.featureFlags) + ) { + return []; + } + + return workspaceRelationMetadataArgsCollection + .filter( + (workspaceRelationMetadataArgs) => + !isGatedAndNotEnabled( + workspaceRelationMetadataArgs.gate, + context.featureFlags, + ), + ) + .map((workspaceRelationMetadataArgs) => { + // Compute reflect relation metadata + const sourceObjectNameSingular = + 'object' in workspaceEntityOrCustomRelationFactory + ? workspaceEntityOrCustomRelationFactory.object.nameSingular + : convertClassNameToObjectMetadataName( + workspaceRelationMetadataArgs.target.name, + ); + const inverseSideTarget = + workspaceRelationMetadataArgs.inverseSideTarget(); + const targetObjectNameSingular = convertClassNameToObjectMetadataName( + inverseSideTarget.name, + ); + const sourceFieldMetadataName = workspaceRelationMetadataArgs.name; + const targetFieldMetadataName = + (workspaceRelationMetadataArgs.inverseSideFieldKey as + | string + | undefined) ?? sourceObjectNameSingular; + const sourceObjectMetadata = + originalObjectMetadataMap[sourceObjectNameSingular]; + const joinColumnsMetadataArgsCollection = + metadataArgsStorage.filterJoinColumns(target); + const joinColumnName = getJoinColumn( + joinColumnsMetadataArgsCollection, + workspaceRelationMetadataArgs, + ); + + assert( + sourceObjectMetadata, + `Source object ${sourceObjectNameSingular} not found in databse for relation ${workspaceRelationMetadataArgs.name} of type ${workspaceRelationMetadataArgs.type}`, + ); + + const targetObjectMetadata = + originalObjectMetadataMap[targetObjectNameSingular]; + + assert( + targetObjectMetadata, + `Target object ${targetObjectNameSingular} not found in databse for relation ${workspaceRelationMetadataArgs.name} of type ${workspaceRelationMetadataArgs.type}`, + ); + + const sourceFieldMetadata = sourceObjectMetadata?.fields.find( + (field) => field.name === sourceFieldMetadataName, + ) as FieldMetadataEntity; + + assert( + sourceFieldMetadata, + `Source field ${sourceFieldMetadataName} not found in object ${sourceObjectNameSingular} for relation ${workspaceRelationMetadataArgs.name} of type ${workspaceRelationMetadataArgs.type}`, + ); + + const targetFieldMetadata = targetObjectMetadata?.fields.find( + (field) => field.name === targetFieldMetadataName, + ) as FieldMetadataEntity; + + assert( + targetFieldMetadata, + `Target field ${targetFieldMetadataName} not found in object ${targetObjectNameSingular} for relation ${workspaceRelationMetadataArgs.name} of type ${workspaceRelationMetadataArgs.type}`, + ); + + return { + ...sourceFieldMetadata, + type: FieldMetadataType.RELATION, + settings: { + relationType: workspaceRelationMetadataArgs.type, + onDelete: workspaceRelationMetadataArgs.onDelete, + joinColumnName, + }, + relationTargetObjectMetadataId: targetObjectMetadata.id, + relationTargetFieldMetadataId: targetFieldMetadata.id, + } satisfies FieldMetadataEntity; + }); + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-field.factory.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-field.factory.ts index 66032006f..559248845 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-field.factory.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-field.factory.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { FieldMetadataType } from 'twenty-shared/types'; -import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface'; +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { WorkspaceDynamicRelationMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-dynamic-relation-metadata-args.interface'; import { WorkspaceEntityMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-entity-metadata-args.interface'; import { WorkspaceFieldMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-field-metadata-args.interface'; @@ -13,7 +13,7 @@ import { } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-field-metadata.interface'; import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface'; -import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage'; import { getJoinColumn } from 'src/engine/twenty-orm/utils/get-join-column.util'; @@ -25,13 +25,11 @@ export class StandardFieldFactory { create( target: typeof BaseWorkspaceEntity, context: WorkspaceSyncContext, - workspaceFeatureFlagsMap: FeatureFlagMap, ): (PartialFieldMetadata | PartialComputedFieldMetadata)[]; create( targets: (typeof BaseWorkspaceEntity)[], context: WorkspaceSyncContext, - workspaceFeatureFlagsMap: FeatureFlagMap, // Map of standardId to field metadata ): Map; create( @@ -39,7 +37,6 @@ export class StandardFieldFactory { | typeof BaseWorkspaceEntity | (typeof BaseWorkspaceEntity)[], context: WorkspaceSyncContext, - workspaceFeatureFlagsMap: FeatureFlagMap, ): | (PartialFieldMetadata | PartialComputedFieldMetadata)[] | Map { @@ -55,7 +52,7 @@ export class StandardFieldFactory { if ( isGatedAndNotEnabled( workspaceEntityMetadataArgs.gate, - workspaceFeatureFlagsMap, + context.featureFlags, ) ) { return acc; @@ -63,7 +60,7 @@ export class StandardFieldFactory { acc.set( workspaceEntityMetadataArgs.standardId, - this.create(target, context, workspaceFeatureFlagsMap), + this.create(target, context), ); return acc; @@ -79,21 +76,18 @@ export class StandardFieldFactory { workspaceEntityMetadataArgs, metadataCollections.fields, context, - workspaceFeatureFlagsMap, this.createFieldMetadata, ), ...this.processMetadata( workspaceEntityMetadataArgs, metadataCollections.relations, context, - workspaceFeatureFlagsMap, this.createFieldRelationMetadata, ), ...this.processMetadata( workspaceEntityMetadataArgs, metadataCollections.dynamicRelations, context, - workspaceFeatureFlagsMap, this.createComputedFieldRelationMetadata, ), ]; @@ -114,22 +108,15 @@ export class StandardFieldFactory { workspaceEntityMetadataArgs: WorkspaceEntityMetadataArgs | undefined, metadataArgs: T[], context: WorkspaceSyncContext, - featureFlagsMap: FeatureFlagMap, createMetadata: ( workspaceEntityMetadataArgs: WorkspaceEntityMetadataArgs | undefined, args: T, context: WorkspaceSyncContext, - featureFlagsMap: FeatureFlagMap, ) => U[], ): U[] { return metadataArgs .flatMap((args) => - createMetadata( - workspaceEntityMetadataArgs, - args, - context, - featureFlagsMap, - ), + createMetadata(workspaceEntityMetadataArgs, args, context), ) .filter(Boolean) as U[]; } @@ -141,12 +128,11 @@ export class StandardFieldFactory { workspaceEntityMetadataArgs: WorkspaceEntityMetadataArgs | undefined, workspaceFieldMetadataArgs: WorkspaceFieldMetadataArgs, context: WorkspaceSyncContext, - workspaceFeatureFlagsMap: FeatureFlagMap, ): PartialFieldMetadata[] { if ( isGatedAndNotEnabled( workspaceFieldMetadataArgs.gate, - workspaceFeatureFlagsMap, + context.featureFlags, ) ) { return []; @@ -182,8 +168,10 @@ export class StandardFieldFactory { workspaceEntityMetadataArgs: WorkspaceEntityMetadataArgs | undefined, workspaceRelationMetadataArgs: WorkspaceRelationMetadataArgs, context: WorkspaceSyncContext, - workspaceFeatureFlagsMap: FeatureFlagMap, ): PartialFieldMetadata[] { + const isNewRelationEnabled = + context.featureFlags[FeatureFlagKey.IsNewRelationEnabled]; + const fieldMetadataCollection: PartialFieldMetadata[] = []; const foreignKeyStandardId = createDeterministicUuid( workspaceRelationMetadataArgs.standardId, @@ -200,13 +188,14 @@ export class StandardFieldFactory { if ( isGatedAndNotEnabled( workspaceRelationMetadataArgs.gate, - workspaceFeatureFlagsMap, + context.featureFlags, ) ) { return []; } - if (joinColumn) { + // We don't want to create the join column field metadata for new relation + if (!isNewRelationEnabled && joinColumn) { fieldMetadataCollection.push({ type: FieldMetadataType.UUID, standardId: foreignKeyStandardId, @@ -222,8 +211,7 @@ export class StandardFieldFactory { isSystem: true, isNullable: workspaceRelationMetadataArgs.isNullable, isUnique: - workspaceRelationMetadataArgs.type === - RelationMetadataType.ONE_TO_ONE, + workspaceRelationMetadataArgs.type === RelationType.ONE_TO_ONE, isActive: workspaceRelationMetadataArgs.isActive ?? true, }); } @@ -235,15 +223,13 @@ export class StandardFieldFactory { label: workspaceRelationMetadataArgs.label, description: workspaceRelationMetadataArgs.description, icon: workspaceRelationMetadataArgs.icon, - defaultValue: null, workspaceId: context.workspaceId, isCustom: false, isSystem: workspaceEntityMetadataArgs?.isSystem || workspaceRelationMetadataArgs.isSystem, isNullable: true, - isUnique: - workspaceRelationMetadataArgs.type === RelationMetadataType.ONE_TO_ONE, + isUnique: workspaceRelationMetadataArgs.type === RelationType.ONE_TO_ONE, isActive: workspaceRelationMetadataArgs.isActive ?? true, }); @@ -259,13 +245,12 @@ export class StandardFieldFactory { | WorkspaceDynamicRelationMetadataArgs | undefined, context: WorkspaceSyncContext, - workspaceFeatureFlagsMap: FeatureFlagMap, ): PartialComputedFieldMetadata[] { if ( !workspaceDynamicRelationMetadataArgs || isGatedAndNotEnabled( workspaceDynamicRelationMetadataArgs.gate, - workspaceFeatureFlagsMap, + context.featureFlags, ) ) { return []; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-index.factory.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-index.factory.ts index 5d6412fef..e1b67d4cb 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-index.factory.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-index.factory.ts @@ -1,6 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface'; import { PartialIndexMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-index-metadata.interface'; import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface'; @@ -20,7 +19,6 @@ export class StandardIndexFactory { context: WorkspaceSyncContext, originalStandardObjectMetadataMap: Record, originalCustomObjectMetadataMap: Record, - workspaceFeatureFlagsMap: FeatureFlagMap, ): Partial[] { const standardIndexOnStandardObjects = standardObjectMetadataDefinitions.flatMap((standardObjectMetadata) => @@ -28,7 +26,6 @@ export class StandardIndexFactory { standardObjectMetadata, context, originalStandardObjectMetadataMap, - workspaceFeatureFlagsMap, ), ); @@ -36,7 +33,6 @@ export class StandardIndexFactory { this.createStandardIndexMetadataForCustomObject( context, originalCustomObjectMetadataMap, - workspaceFeatureFlagsMap, ); return [ @@ -49,7 +45,6 @@ export class StandardIndexFactory { target: typeof BaseWorkspaceEntity, context: WorkspaceSyncContext, originalStandardObjectMetadataMap: Record, - workspaceFeatureFlagsMap: FeatureFlagMap, ): Partial[] { const workspaceEntity = metadataArgsStorage.filterEntities(target); @@ -59,7 +54,7 @@ export class StandardIndexFactory { ); } - if (isGatedAndNotEnabled(workspaceEntity?.gate, workspaceFeatureFlagsMap)) { + if (isGatedAndNotEnabled(workspaceEntity?.gate, context.featureFlags)) { return []; } @@ -68,7 +63,7 @@ export class StandardIndexFactory { .filter((workspaceIndexMetadataArgs) => { return !isGatedAndNotEnabled( workspaceIndexMetadataArgs.gate, - workspaceFeatureFlagsMap, + context.featureFlags, ); }); @@ -102,7 +97,6 @@ export class StandardIndexFactory { private createStandardIndexMetadataForCustomObject( context: WorkspaceSyncContext, originalCustomObjectMetadataMap: Record, - workspaceFeatureFlagsMap: FeatureFlagMap, ): Partial[] { const target = CustomWorkspaceEntity; const workspaceEntity = metadataArgsStorage.filterExtendedEntities(target); @@ -118,7 +112,7 @@ export class StandardIndexFactory { .filter((workspaceIndexMetadataArgs) => { return !isGatedAndNotEnabled( workspaceIndexMetadataArgs.gate, - workspaceFeatureFlagsMap, + context.featureFlags, ); }); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-object.factory.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-object.factory.ts index c45e3b9ae..e572777cd 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-object.factory.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-object.factory.ts @@ -1,6 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface'; import { PartialWorkspaceEntity } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-object-metadata.interface'; import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface'; @@ -13,19 +12,15 @@ export class StandardObjectFactory { create( standardObjectMetadataDefinitions: (typeof BaseWorkspaceEntity)[], context: WorkspaceSyncContext, - workspaceFeatureFlagsMap: FeatureFlagMap, ): Omit[] { return standardObjectMetadataDefinitions - .map((metadata) => - this.createObjectMetadata(metadata, context, workspaceFeatureFlagsMap), - ) + .map((metadata) => this.createObjectMetadata(metadata, context)) .filter((metadata): metadata is PartialWorkspaceEntity => !!metadata); } private createObjectMetadata( target: typeof BaseWorkspaceEntity, context: WorkspaceSyncContext, - workspaceFeatureFlagsMap: FeatureFlagMap, ): Omit | undefined { const workspaceEntityMetadataArgs = metadataArgsStorage.filterEntities(target); @@ -39,7 +34,7 @@ export class StandardObjectFactory { if ( isGatedAndNotEnabled( workspaceEntityMetadataArgs.gate, - workspaceFeatureFlagsMap, + context.featureFlags, ) ) { return undefined; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-relation.factory.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-relation.factory.ts index f77d74f59..bb7b6d374 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-relation.factory.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-relation.factory.ts @@ -1,18 +1,18 @@ import { Injectable } from '@nestjs/common'; +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface'; -import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface'; -import { isGatedAndNotEnabled } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/is-gate-and-not-enabled.util'; -import { assert } from 'src/utils/assert'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { RelationMetadataEntity, RelationMetadataType, } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { convertClassNameToObjectMetadataName } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/convert-class-to-object-metadata-name.util'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage'; +import { convertClassNameToObjectMetadataName } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/convert-class-to-object-metadata-name.util'; +import { isGatedAndNotEnabled } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/is-gate-and-not-enabled.util'; +import { assert } from 'src/utils/assert'; interface CustomRelationFactory { object: ObjectMetadataEntity; @@ -25,14 +25,12 @@ export class StandardRelationFactory { customObjectFactories: CustomRelationFactory[], context: WorkspaceSyncContext, originalObjectMetadataMap: Record, - workspaceFeatureFlagsMap: FeatureFlagMap, ): Partial[]; create( standardObjectMetadataDefinitions: (typeof BaseWorkspaceEntity)[], context: WorkspaceSyncContext, originalObjectMetadataMap: Record, - workspaceFeatureFlagsMap: FeatureFlagMap, ): Partial[]; create( @@ -44,7 +42,6 @@ export class StandardRelationFactory { }[], context: WorkspaceSyncContext, originalObjectMetadataMap: Record, - workspaceFeatureFlagsMap: FeatureFlagMap, ): Partial[] { return standardObjectMetadataDefinitionsOrCustomObjectFactories.flatMap( ( @@ -56,7 +53,6 @@ export class StandardRelationFactory { standardObjectMetadata, context, originalObjectMetadataMap, - workspaceFeatureFlagsMap, ), ); } @@ -67,7 +63,6 @@ export class StandardRelationFactory { | CustomRelationFactory, context: WorkspaceSyncContext, originalObjectMetadataMap: Record, - workspaceFeatureFlagsMap: FeatureFlagMap, ): Partial[] { const target = 'metadata' in workspaceEntityOrCustomRelationFactory @@ -88,7 +83,7 @@ export class StandardRelationFactory { if ( !workspaceRelationMetadataArgsCollection || - isGatedAndNotEnabled(workspaceEntity?.gate, workspaceFeatureFlagsMap) + isGatedAndNotEnabled(workspaceEntity?.gate, context.featureFlags) ) { return []; } @@ -96,16 +91,13 @@ export class StandardRelationFactory { return workspaceRelationMetadataArgsCollection .filter((workspaceRelationMetadataArgs) => { // We're not storing many-to-one relations in the DB for the moment - if ( - workspaceRelationMetadataArgs.type === - RelationMetadataType.MANY_TO_ONE - ) { + if (workspaceRelationMetadataArgs.type === RelationType.MANY_TO_ONE) { return false; } return !isGatedAndNotEnabled( workspaceRelationMetadataArgs.gate, - workspaceFeatureFlagsMap, + context.featureFlags, ); }) .map((workspaceRelationMetadataArgs) => { @@ -163,7 +155,9 @@ export class StandardRelationFactory { ); return { - relationType: workspaceRelationMetadataArgs.type, + // TODO: Will be removed when we drop RelationMetadata + relationType: + workspaceRelationMetadataArgs.type as unknown as RelationMetadataType, fromObjectMetadataId: fromObjectMetadata?.id, toObjectMetadataId: toObjectMetadata?.id, fromFieldMetadataId: fromFieldMetadata?.id, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface.ts index 23c746a5f..7d081d1a8 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface.ts @@ -1,3 +1,5 @@ +import { FieldMetadataType } from 'twenty-shared/types'; + import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; @@ -52,6 +54,20 @@ export type FieldComparatorResult = > | ComparatorDeleteResult; +export type FieldRelationComparatorResult = + | ComparatorSkipResult + | ComparatorCreateResult< + Partial> & { + id: string; + } + > + | ComparatorUpdateResult< + Partial> & { + id: string; + } + > + | ComparatorDeleteResult>; + export type RelationComparatorResult = | ComparatorCreateResult> | ComparatorDeleteResult diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-field-metadata.interface.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-field-metadata.interface.ts index ec67535b0..f39bc0abd 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-field-metadata.interface.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-field-metadata.interface.ts @@ -5,8 +5,10 @@ import { WorkspaceDynamicRelationMetadataArgsFactory } from 'src/engine/twenty-o import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -export type PartialFieldMetadata = Omit< - FieldMetadataInterface, +export type PartialFieldMetadata< + T extends FieldMetadataType = FieldMetadataType, +> = Omit< + FieldMetadataInterface, 'id' | 'label' | 'description' | 'objectMetadataId' > & { standardId: string; @@ -31,6 +33,10 @@ export type PartialComputedFieldMetadata = { objectMetadataId?: string; }; -export type ComputedPartialFieldMetadata = { - [K in keyof PartialFieldMetadata]: ExcludeFunctions; +export type ComputedPartialFieldMetadata< + T extends FieldMetadataType = FieldMetadataType, +> = { + [K in keyof PartialFieldMetadata]: ExcludeFunctions< + PartialFieldMetadata[K] + >; }; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/interfaces/updater-action.type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/interfaces/updater-action.type.ts new file mode 100644 index 000000000..98dc44a0a --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/interfaces/updater-action.type.ts @@ -0,0 +1 @@ +export type UpdaterAction = 'create' | 'update' | 'delete'; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/interfaces/updater-options.interface.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/interfaces/updater-options.interface.ts new file mode 100644 index 000000000..476345541 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/interfaces/updater-options.interface.ts @@ -0,0 +1,5 @@ +import { UpdaterAction } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/updater-action.type'; + +export interface UpdaterOptions { + actions: UpdaterAction[]; +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface.ts index fc0c7b5c5..386a8c88f 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface.ts @@ -1,4 +1,7 @@ +import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface'; + export interface WorkspaceSyncContext { workspaceId: string; dataSourceId: string; + featureFlags: FeatureFlagMap; } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-metadata-updater.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-metadata-updater.service.ts index 2fb5a9210..990b7e5c0 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-metadata-updater.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-metadata-updater.service.ts @@ -1,5 +1,7 @@ import { Injectable } from '@nestjs/common'; +import { FieldMetadataType } from 'twenty-shared/types'; +import { capitalize } from 'twenty-shared/utils'; import { EntityManager, EntityTarget, @@ -10,12 +12,13 @@ import { } from 'typeorm'; import { DeepPartial } from 'typeorm/common/DeepPartial'; import { v4 as uuidV4 } from 'uuid'; -import { capitalize } from 'twenty-shared/utils'; -import { FieldMetadataType } from 'twenty-shared/types'; import { PartialFieldMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-field-metadata.interface'; import { PartialIndexMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-index-metadata.interface'; +import { UpdaterOptions } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/updater-options.interface'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; import { FieldMetadataComplexOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; @@ -25,63 +28,77 @@ import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory'; +import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; import { FieldMetadataUpdate } from 'src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field.factory'; import { ObjectMetadataUpdate } from 'src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-object.factory'; import { WorkspaceSyncStorage } from 'src/engine/workspace-manager/workspace-sync-metadata/storage/workspace-sync.storage'; @Injectable() export class WorkspaceMetadataUpdaterService { + constructor(private readonly featureFlagService: FeatureFlagService) {} + async updateObjectMetadata( manager: EntityManager, storage: WorkspaceSyncStorage, + options?: UpdaterOptions, ): Promise<{ createdObjectMetadataCollection: ObjectMetadataEntity[]; updatedObjectMetadataCollection: ObjectMetadataUpdate[]; }> { const objectMetadataRepository = manager.getRepository(ObjectMetadataEntity); + let createdObjectMetadataCollection: ObjectMetadataEntity[] = []; + let updatedObjectMetadataCollection: ObjectMetadataUpdate[] = []; /** * Create object metadata */ - const createdPartialObjectMetadataCollection = - await objectMetadataRepository.save( - storage.objectMetadataCreateCollection.map((objectMetadata) => ({ - ...objectMetadata, - isActive: true, - })) as DeepPartial[], + if (!options || options.actions.includes('create')) { + const createdPartialObjectMetadataCollection = + await objectMetadataRepository.save( + storage.objectMetadataCreateCollection.map((objectMetadata) => ({ + ...objectMetadata, + isActive: true, + })) as DeepPartial[], + ); + const identifiers = createdPartialObjectMetadataCollection.map( + (object) => object.id, ); - const identifiers = createdPartialObjectMetadataCollection.map( - (object) => object.id, - ); - const createdObjectMetadataCollection = await manager.find( - ObjectMetadataEntity, - { - where: { id: In(identifiers) }, - relations: ['dataSource', 'fields'], - }, - ); + + createdObjectMetadataCollection = await manager.find( + ObjectMetadataEntity, + { + where: { id: In(identifiers) }, + relations: ['dataSource', 'fields'], + }, + ); + } /** * Update object metadata */ - const updatedObjectMetadataCollection = await this.updateEntities( - manager, - ObjectMetadataEntity, - storage.objectMetadataUpdateCollection, - [ - 'fields', - 'dataSourceId', - 'workspaceId', - 'labelIdentifierFieldMetadataId', - 'imageIdentifierFieldMetadataId', - ], - ); + if (!options || options.actions.includes('update')) { + updatedObjectMetadataCollection = await this.updateEntities( + manager, + ObjectMetadataEntity, + storage.objectMetadataUpdateCollection, + [ + 'fields', + 'dataSourceId', + 'workspaceId', + 'labelIdentifierFieldMetadataId', + 'imageIdentifierFieldMetadataId', + ], + ); + } /** * Delete object metadata */ - if (storage.objectMetadataDeleteCollection.length > 0) { + if ( + storage.objectMetadataDeleteCollection.length > 0 && + (!options || options.actions.includes('delete')) + ) { await objectMetadataRepository.delete( storage.objectMetadataDeleteCollection.map((object) => object.id), ); @@ -121,6 +138,7 @@ export class WorkspaceMetadataUpdaterService { async updateFieldMetadata( manager: EntityManager, storage: WorkspaceSyncStorage, + options?: UpdaterOptions, ): Promise<{ createdFieldMetadataCollection: FieldMetadataEntity[]; updatedFieldMetadataCollection: FieldMetadataUpdate[]; @@ -130,26 +148,32 @@ export class WorkspaceMetadataUpdaterService { IndexFieldMetadataEntity, ); const indexMetadataRepository = manager.getRepository(IndexMetadataEntity); + let createdFieldMetadataCollection: FieldMetadataEntity[] = []; + let updatedFieldMetadataCollection: FieldMetadataUpdate[] = []; /** * Update field metadata */ - const updatedFieldMetadataCollection = - await this.updateEntities( - manager, - FieldMetadataEntity, - storage.fieldMetadataUpdateCollection, - ['objectMetadataId', 'workspaceId'], - ); + if (!options || options.actions.includes('update')) { + updatedFieldMetadataCollection = + await this.updateEntities( + manager, + FieldMetadataEntity, + storage.fieldMetadataUpdateCollection, + ['objectMetadataId', 'workspaceId'], + ); + } /** * Create field metadata */ - const createdFieldMetadataCollection = await fieldMetadataRepository.save( - storage.fieldMetadataCreateCollection.map((field) => - this.prepareFieldMetadataForCreation(field), - ) as DeepPartial[], - ); + if (!options || options.actions.includes('create')) { + createdFieldMetadataCollection = await fieldMetadataRepository.save( + storage.fieldMetadataCreateCollection.map((field) => + this.prepareFieldMetadataForCreation(field), + ) as DeepPartial[], + ); + } /** * Delete field metadata @@ -160,7 +184,10 @@ export class WorkspaceMetadataUpdaterService { (field) => field.type !== FieldMetadataType.RELATION, ); - if (fieldMetadataDeleteCollectionWithoutRelationType.length > 0) { + if ( + fieldMetadataDeleteCollectionWithoutRelationType.length > 0 && + (!options || options.actions.includes('delete')) + ) { await this.deleteIndexFieldMetadata( fieldMetadataDeleteCollectionWithoutRelationType, indexFieldMetadataRepository, @@ -181,6 +208,68 @@ export class WorkspaceMetadataUpdaterService { }; } + async updateFieldRelationMetadata( + manager: EntityManager, + storage: WorkspaceSyncStorage, + options?: UpdaterOptions, + ): Promise<{ + createdFieldRelationMetadataCollection: FieldMetadataUpdate[]; + updatedFieldRelationMetadataCollection: FieldMetadataUpdate[]; + deletedFieldRelationMetadataCollection: FieldMetadataUpdate[]; + }> { + let createdFieldRelationMetadataCollection: FieldMetadataUpdate[] = + []; + let updatedFieldRelationMetadataCollection: FieldMetadataUpdate[] = + []; + let deletedFieldRelationMetadataCollection: FieldMetadataUpdate[] = + []; + + /** + * Create field relation metadata + */ + if (!options || options.actions.includes('create')) { + createdFieldRelationMetadataCollection = await this.updateEntities< + FieldMetadataEntity + >( + manager, + FieldMetadataEntity, + storage.fieldRelationMetadataCreateCollection, + ['objectMetadataId', 'workspaceId'], + ); + } + + /** + * Update field relation metadata + */ + if (!options || options.actions.includes('update')) { + updatedFieldRelationMetadataCollection = await this.updateEntities< + FieldMetadataEntity + >( + manager, + FieldMetadataEntity, + storage.fieldRelationMetadataUpdateCollection, + ['objectMetadataId', 'workspaceId'], + ); + } + + if (!options || options.actions.includes('delete')) { + deletedFieldRelationMetadataCollection = await this.updateEntities< + FieldMetadataEntity + >( + manager, + FieldMetadataEntity, + storage.fieldRelationMetadataDeleteCollection, + ['objectMetadataId', 'workspaceId'], + ); + } + + return { + createdFieldRelationMetadataCollection, + updatedFieldRelationMetadataCollection, + deletedFieldRelationMetadataCollection, + }; + } + async deleteIndexFieldMetadata( fieldMetadataDeleteCollectionWithoutRelationType: Partial[], indexFieldMetadataRepository: Repository, @@ -276,6 +365,15 @@ export class WorkspaceMetadataUpdaterService { createdIndexMetadataCollection: IndexMetadataEntity[]; }> { const indexMetadataRepository = manager.getRepository(IndexMetadataEntity); + const workspaceId = originalObjectMetadataCollection?.[0]?.workspaceId; + let isNewRelationEnabled = false; + + if (workspaceId) { + isNewRelationEnabled = await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IsNewRelationEnabled, + workspaceId, + ); + } const convertIndexMetadataForSaving = ( indexMetadata: PartialIndexMetadata, @@ -288,6 +386,15 @@ export class WorkspaceMetadataUpdaterService { const fieldMetadata = originalObjectMetadataCollection .find((object) => object.id === indexMetadata.objectMetadataId) ?.fields.find((field) => { + if ( + isNewRelationEnabled && + isFieldMetadataEntityOfType(field, FieldMetadataType.RELATION) + ) { + if (field.settings?.joinColumnName === column) { + return true; + } + } + if (field.name === column) { return true; } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-field-metadata-relation.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-field-metadata-relation.service.ts new file mode 100644 index 000000000..b440fc625 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-field-metadata-relation.service.ts @@ -0,0 +1,267 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import { FieldMetadataType } from 'twenty-shared/types'; +import { EntityManager } from 'typeorm'; + +import { WorkspaceMigrationBuilderAction } from 'src/engine/workspace-manager/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface'; +import { + ComparatorAction, + FieldRelationComparatorResult, +} from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface'; +import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface'; + +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; +import { CustomWorkspaceEntity } from 'src/engine/twenty-orm/custom.workspace-entity'; +import { WorkspaceMigrationFieldRelationFactory } from 'src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field-relation.factory'; +import { + FieldMetadataUpdate, + WorkspaceMigrationFieldFactory, +} from 'src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field.factory'; +import { WorkspaceFieldRelationComparator } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-field-relation.comparator'; +import { StandardFieldRelationFactory } from 'src/engine/workspace-manager/workspace-sync-metadata/factories/standard-field-relation.factory'; +import { WorkspaceMetadataUpdaterService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-metadata-updater.service'; +import { standardObjectMetadataDefinitions } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects'; +import { WorkspaceSyncStorage } from 'src/engine/workspace-manager/workspace-sync-metadata/storage/workspace-sync.storage'; +import { mapObjectMetadataByUniqueIdentifier } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/sync-metadata.util'; + +@Injectable() +export class WorkspaceSyncFieldMetadataRelationService { + private readonly logger = new Logger( + WorkspaceSyncFieldMetadataRelationService.name, + ); + + constructor( + private readonly standardFieldRelationFactory: StandardFieldRelationFactory, + private readonly workspaceFieldRelationComparator: WorkspaceFieldRelationComparator, + private readonly workspaceMetadataUpdaterService: WorkspaceMetadataUpdaterService, + private readonly workspaceMigrationFieldFactory: WorkspaceMigrationFieldFactory, + private readonly workspaceMigrationFieldRelationFactory: WorkspaceMigrationFieldRelationFactory, + ) {} + + async synchronize( + context: WorkspaceSyncContext, + manager: EntityManager, + storage: WorkspaceSyncStorage, + ): Promise[]> { + let originalObjectMetadataCollection = + await this.getOriginalObjectMetadataCollection(context, manager); + + const customObjectMetadataCollection = + originalObjectMetadataCollection.filter( + (objectMetadata) => objectMetadata.isCustom, + ); + + // Create map of object metadata & field metadata by unique identifier + const originalObjectMetadataMapByName = mapObjectMetadataByUniqueIdentifier( + originalObjectMetadataCollection, + // Relation are based on the singular name + (objectMetadata) => objectMetadata.nameSingular, + ); + + await this.synchronizeStandardObjectRelationFields( + context, + originalObjectMetadataCollection, + originalObjectMetadataMapByName, + storage, + ); + + await this.synchronizeCustomObjectRelationFields( + context, + customObjectMetadataCollection, + originalObjectMetadataMapByName, + storage, + ); + + this.logger.log('Updating workspace metadata'); + + // Save field metadata to DB + const metadataFieldUpdaterResult = + await this.workspaceMetadataUpdaterService.updateFieldRelationMetadata( + manager, + storage, + ); + + this.logger.log('Generating migrations'); + + const updateFieldWorkspaceMigrations = + await this.workspaceMigrationFieldFactory.create( + originalObjectMetadataCollection, + [ + // Both of them are update as field where created in WorkspaceSyncFieldMetadataService + ...metadataFieldUpdaterResult.createdFieldRelationMetadataCollection, + ...metadataFieldUpdaterResult.updatedFieldRelationMetadataCollection, + ...metadataFieldUpdaterResult.deletedFieldRelationMetadataCollection, + ] as FieldMetadataUpdate[], + WorkspaceMigrationBuilderAction.UPDATE, + ); + + // Resync updated object metadata + originalObjectMetadataCollection = + await this.getOriginalObjectMetadataCollection(context, manager); + + const deletedFieldRelationMetadataCollection = + metadataFieldUpdaterResult.deletedFieldRelationMetadataCollection.map( + (field) => field.altered, + ); + + const deleteFieldRelationWorkspaceMigrations = + await this.workspaceMigrationFieldRelationFactory.create( + originalObjectMetadataCollection, + deletedFieldRelationMetadataCollection, + WorkspaceMigrationBuilderAction.DELETE, + ); + + const createdFieldRelationMetadataCollection = + metadataFieldUpdaterResult.createdFieldRelationMetadataCollection.map( + (field) => field.altered, + ); + + const createFieldRelationWorkspaceMigrations = + await this.workspaceMigrationFieldRelationFactory.create( + originalObjectMetadataCollection, + createdFieldRelationMetadataCollection, + WorkspaceMigrationBuilderAction.CREATE, + ); + + const updateFieldRelationWorkspaceMigrations = + await this.workspaceMigrationFieldRelationFactory.create( + originalObjectMetadataCollection, + metadataFieldUpdaterResult.updatedFieldRelationMetadataCollection, + WorkspaceMigrationBuilderAction.UPDATE, + ); + + // TODO: Should we handle deletion of relation here? + + this.logger.log('Saving migrations'); + + return [ + ...updateFieldWorkspaceMigrations, + ...deleteFieldRelationWorkspaceMigrations, + ...createFieldRelationWorkspaceMigrations, + ...updateFieldRelationWorkspaceMigrations, + ]; + } + + private async synchronizeStandardObjectRelationFields( + context: WorkspaceSyncContext, + originalObjectMetadataCollection: ObjectMetadataEntity[], + originalObjectMetadataMapByName: Record, + storage: WorkspaceSyncStorage, + ): Promise { + // Create standard field metadata map + const standardFieldMetadataRelationCollection = + this.standardFieldRelationFactory.createFieldRelationForStandardObject( + standardObjectMetadataDefinitions, + context, + originalObjectMetadataMapByName, + ); + + // Create map of original and standard object metadata by standard ids + const originalObjectMetadataMap = mapObjectMetadataByUniqueIdentifier( + originalObjectMetadataCollection, + ); + + // Loop over all standard objects and compare them with the objects in DB + for (const [ + standardObjectId, + standardFieldMetadataCollection, + ] of standardFieldMetadataRelationCollection) { + const originalObjectMetadata = + originalObjectMetadataMap[standardObjectId]; + + const originalFieldRelationMetadataCollection = + (originalObjectMetadata?.fields.filter( + (field) => field.type === FieldMetadataType.RELATION, + ) ?? []) as FieldMetadataEntity[]; + + if (originalFieldRelationMetadataCollection.length === 0) { + continue; + } + + const fieldComparatorResults = + this.workspaceFieldRelationComparator.compare( + originalFieldRelationMetadataCollection, + standardFieldMetadataCollection, + ); + + this.storeComparatorResults(fieldComparatorResults, storage); + } + } + + private async synchronizeCustomObjectRelationFields( + context: WorkspaceSyncContext, + customObjectMetadataCollection: ObjectMetadataEntity[], + originalObjectMetadataMapByName: Record, + storage: WorkspaceSyncStorage, + ): Promise { + // Create standard field metadata collection + const customFieldMetadataRelationCollection = + this.standardFieldRelationFactory.createFieldRelationForCustomObject( + customObjectMetadataCollection.map((objectMetadata) => ({ + object: objectMetadata, + metadata: CustomWorkspaceEntity, + })), + context, + originalObjectMetadataMapByName, + ); + + // Loop over all custom objects from the DB and compare their fields with standard fields + for (const customObjectMetadata of customObjectMetadataCollection) { + /** + * COMPARE FIELD METADATA + */ + const fieldComparatorResults = + this.workspaceFieldRelationComparator.compare( + customObjectMetadata.fields as FieldMetadataEntity[], + customFieldMetadataRelationCollection, + ); + + this.storeComparatorResults(fieldComparatorResults, storage); + } + } + + private async getOriginalObjectMetadataCollection( + context: WorkspaceSyncContext, + manager: EntityManager, + ): Promise { + const objectMetadataRepository = + manager.getRepository(ObjectMetadataEntity); + + const originalObjectMetadataCollection = + await objectMetadataRepository.find({ + where: { + workspaceId: context.workspaceId, + fields: { + type: FieldMetadataType.RELATION, + }, + }, + relations: ['dataSource', 'fields'], + }); + + return originalObjectMetadataCollection; + } + + private storeComparatorResults( + fieldComparatorResults: FieldRelationComparatorResult[], + storage: WorkspaceSyncStorage, + ): void { + for (const fieldComparatorResult of fieldComparatorResults) { + switch (fieldComparatorResult.action) { + case ComparatorAction.CREATE: { + storage.addCreateFieldRelationMetadata(fieldComparatorResult.object); + break; + } + case ComparatorAction.UPDATE: { + storage.addUpdateFieldRelationMetadata(fieldComparatorResult.object); + break; + } + case ComparatorAction.DELETE: { + storage.addDeleteFieldRelationMetadata(fieldComparatorResult.object); + break; + } + } + } + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-field-metadata.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-field-metadata.service.ts index 00115e2fb..00ca52d58 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-field-metadata.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-field-metadata.service.ts @@ -2,7 +2,6 @@ import { Injectable, Logger } from '@nestjs/common'; import { EntityManager } from 'typeorm'; -import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface'; import { WorkspaceMigrationBuilderAction } from 'src/engine/workspace-manager/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface'; import { ComparatorAction, @@ -37,7 +36,6 @@ export class WorkspaceSyncFieldMetadataService { context: WorkspaceSyncContext, manager: EntityManager, storage: WorkspaceSyncStorage, - workspaceFeatureFlagsMap: FeatureFlagMap, ): Promise[]> { const objectMetadataRepository = manager.getRepository(ObjectMetadataEntity); @@ -61,14 +59,12 @@ export class WorkspaceSyncFieldMetadataService { originalObjectMetadataCollection, customObjectMetadataCollection, storage, - workspaceFeatureFlagsMap, ); await this.synchronizeCustomObjectFields( context, customObjectMetadataCollection, storage, - workspaceFeatureFlagsMap, ); this.logger.log('Updating workspace metadata'); @@ -121,14 +117,12 @@ export class WorkspaceSyncFieldMetadataService { originalObjectMetadataCollection: ObjectMetadataEntity[], customObjectMetadataCollection: ObjectMetadataEntity[], storage: WorkspaceSyncStorage, - workspaceFeatureFlagsMap: FeatureFlagMap, ): Promise { // Create standard field metadata map const standardObjectStandardFieldMetadataMap = this.standardFieldFactory.create( standardObjectMetadataDefinitions, context, - workspaceFeatureFlagsMap, ); // Create map of original and standard object metadata by standard ids @@ -145,6 +139,7 @@ export class WorkspaceSyncFieldMetadataService { originalObjectMetadataMap[standardObjectId]; const computedStandardFieldMetadataCollection = computeStandardFields( + context, standardFieldMetadataCollection, originalObjectMetadata, // We need to provide this for generated relations with custom objects @@ -161,24 +156,46 @@ export class WorkspaceSyncFieldMetadataService { } } + synchronizeCustomObject( + context: WorkspaceSyncContext, + customObjectMetadata: ObjectMetadataEntity, + ): FieldComparatorResult[] { + // Create standard field metadata collection + const customObjectStandardFieldMetadataCollection = + this.standardFieldFactory.create(CustomWorkspaceEntity, context); + + const standardFieldMetadataCollection = computeStandardFields( + context, + customObjectStandardFieldMetadataCollection, + customObjectMetadata, + ); + + /** + * COMPARE FIELD METADATA + */ + const fieldComparatorResults = this.workspaceFieldComparator.compare( + customObjectMetadata.id, + customObjectMetadata.fields, + standardFieldMetadataCollection, + ); + + return fieldComparatorResults; + } + private async synchronizeCustomObjectFields( context: WorkspaceSyncContext, customObjectMetadataCollection: ObjectMetadataEntity[], storage: WorkspaceSyncStorage, - workspaceFeatureFlagsMap: FeatureFlagMap, ): Promise { // Create standard field metadata collection const customObjectStandardFieldMetadataCollection = - this.standardFieldFactory.create( - CustomWorkspaceEntity, - context, - workspaceFeatureFlagsMap, - ); + this.standardFieldFactory.create(CustomWorkspaceEntity, context); // Loop over all custom objects from the DB and compare their fields with standard fields for (const customObjectMetadata of customObjectMetadataCollection) { // Also, maybe it's better to refactor a bit and move generation part into a separate module ? const standardFieldMetadataCollection = computeStandardFields( + context, customObjectStandardFieldMetadataCollection, customObjectMetadata, ); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-index-metadata.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-index-metadata.service.ts index 0173d171e..b0022b3b2 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-index-metadata.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-index-metadata.service.ts @@ -2,7 +2,6 @@ import { Injectable, Logger } from '@nestjs/common'; import { Any, EntityManager } from 'typeorm'; -import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface'; import { WorkspaceMigrationBuilderAction } from 'src/engine/workspace-manager/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface'; import { ComparatorAction } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface'; import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface'; @@ -33,7 +32,6 @@ export class WorkspaceSyncIndexMetadataService { context: WorkspaceSyncContext, manager: EntityManager, storage: WorkspaceSyncStorage, - workspaceFeatureFlagsMap: FeatureFlagMap, ): Promise[]> { this.logger.log('Syncing index metadata'); @@ -89,7 +87,6 @@ export class WorkspaceSyncIndexMetadataService { context, originalStandardObjectMetadataMap, originalCustomObjectMetadataMap, - workspaceFeatureFlagsMap, ); const indexComparatorResults = this.workspaceIndexComparator.compare( diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-object-metadata-identifiers.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-object-metadata-identifiers.service.ts index 2649d7f4a..cc68bcd8c 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-object-metadata-identifiers.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-object-metadata-identifiers.service.ts @@ -3,7 +3,6 @@ import { Injectable } from '@nestjs/common'; import { EntityManager, Repository } from 'typeorm'; import { FieldMetadataType } from 'twenty-shared/types'; -import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface'; import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; @@ -21,7 +20,6 @@ export class WorkspaceSyncObjectMetadataIdentifiersService { context: WorkspaceSyncContext, manager: EntityManager, _storage: WorkspaceSyncStorage, - workspaceFeatureFlagsMap: FeatureFlagMap, ): Promise { const objectMetadataRepository = manager.getRepository(ObjectMetadataEntity); @@ -32,10 +30,8 @@ export class WorkspaceSyncObjectMetadataIdentifiersService { objectMetadataRepository, ); - const standardObjectMetadataMap = this.createStandardObjectMetadataMap( - context, - workspaceFeatureFlagsMap, - ); + const standardObjectMetadataMap = + this.createStandardObjectMetadataMap(context); await this.processObjectMetadataCollection( originalObjectMetadataCollection, @@ -56,12 +52,10 @@ export class WorkspaceSyncObjectMetadataIdentifiersService { private createStandardObjectMetadataMap( context: WorkspaceSyncContext, - workspaceFeatureFlagsMap: FeatureFlagMap, ): Record { const standardObjectMetadataCollection = this.standardObjectFactory.create( standardObjectMetadataDefinitions, context, - workspaceFeatureFlagsMap, ); return mapObjectMetadataByUniqueIdentifier( diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-object-metadata.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-object-metadata.service.ts index 1c056e5d1..ed4fcaf2b 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-object-metadata.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-object-metadata.service.ts @@ -2,7 +2,6 @@ import { Injectable, Logger } from '@nestjs/common'; import { EntityManager } from 'typeorm'; -import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface'; import { WorkspaceMigrationBuilderAction } from 'src/engine/workspace-manager/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface'; import { ComparatorAction } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface'; import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface'; @@ -33,7 +32,6 @@ export class WorkspaceSyncObjectMetadataService { context: WorkspaceSyncContext, manager: EntityManager, storage: WorkspaceSyncStorage, - workspaceFeatureFlagsMap: FeatureFlagMap, ): Promise[]> { const objectMetadataRepository = manager.getRepository(ObjectMetadataEntity); @@ -83,7 +81,6 @@ export class WorkspaceSyncObjectMetadataService { const standardObjectMetadataCollection = this.standardObjectFactory.create( standardObjectMetadataDefinitions, context, - workspaceFeatureFlagsMap, ); // Create map of original and standard object metadata by standard ids diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-relation-metadata.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-relation-metadata.service.ts index 8a19b3f78..76fc12f93 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-relation-metadata.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-relation-metadata.service.ts @@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common'; import { EntityManager } from 'typeorm'; -import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface'; import { WorkspaceMigrationBuilderAction } from 'src/engine/workspace-manager/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface'; import { ComparatorAction } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface'; import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface'; @@ -32,7 +31,6 @@ export class WorkspaceSyncRelationMetadataService { context: WorkspaceSyncContext, manager: EntityManager, storage: WorkspaceSyncStorage, - workspaceFeatureFlagsMap: FeatureFlagMap, ): Promise[]> { const objectMetadataRepository = manager.getRepository(ObjectMetadataEntity); @@ -76,7 +74,6 @@ export class WorkspaceSyncRelationMetadataService { standardObjectMetadataDefinitions, context, originalObjectMetadataMap, - workspaceFeatureFlagsMap, ); const customRelationMetadataCollection = @@ -87,7 +84,6 @@ export class WorkspaceSyncRelationMetadataService { })), context, originalObjectMetadataMap, - workspaceFeatureFlagsMap, ); const relationComparatorResults = this.workspaceRelationComparator.compare( diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/storage/workspace-sync.storage.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/storage/workspace-sync.storage.ts index 8d3aea6b3..2c15457bb 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/storage/workspace-sync.storage.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/storage/workspace-sync.storage.ts @@ -1,3 +1,5 @@ +import { FieldMetadataType } from 'twenty-shared/types'; + import { ComputedPartialFieldMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-field-metadata.interface'; import { ComputedPartialWorkspaceEntity } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-object-metadata.interface'; @@ -27,6 +29,20 @@ export class WorkspaceSyncStorage { })[] = []; private readonly _fieldMetadataDeleteCollection: FieldMetadataEntity[] = []; + // Field relation metadata + private readonly _fieldRelationMetadataCreateCollection: (Partial< + ComputedPartialFieldMetadata + > & { + id: string; + })[] = []; + private readonly _fieldRelationMetadataUpdateCollection: (Partial< + ComputedPartialFieldMetadata + > & { + id: string; + })[] = []; + private readonly _fieldRelationMetadataDeleteCollection: FieldMetadataEntity[] = + []; + // Relation metadata private readonly _relationMetadataCreateCollection: Partial[] = []; @@ -68,6 +84,18 @@ export class WorkspaceSyncStorage { return this._fieldMetadataDeleteCollection; } + get fieldRelationMetadataCreateCollection() { + return this._fieldRelationMetadataCreateCollection; + } + + get fieldRelationMetadataUpdateCollection() { + return this._fieldRelationMetadataUpdateCollection; + } + + get fieldRelationMetadataDeleteCollection() { + return this._fieldRelationMetadataDeleteCollection; + } + get relationMetadataCreateCollection() { return this._relationMetadataCreateCollection; } @@ -118,6 +146,28 @@ export class WorkspaceSyncStorage { this._fieldMetadataDeleteCollection.push(field); } + addCreateFieldRelationMetadata( + field: Partial> & { + id: string; + }, + ) { + this._fieldRelationMetadataCreateCollection.push(field); + } + + addUpdateFieldRelationMetadata( + field: Partial> & { + id: string; + }, + ) { + this._fieldRelationMetadataUpdateCollection.push(field); + } + + addDeleteFieldRelationMetadata( + field: FieldMetadataEntity, + ) { + this._fieldRelationMetadataDeleteCollection.push(field); + } + addCreateRelationMetadata(relation: Partial) { this._relationMetadataCreateCollection.push(relation); } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/compute-standard-fields.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/compute-standard-fields.util.ts index ac3e3fff0..9a10bae8c 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/compute-standard-fields.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/compute-standard-fields.util.ts @@ -5,7 +5,9 @@ import { PartialComputedFieldMetadata, PartialFieldMetadata, } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-field-metadata.interface'; +import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { createForeignKeyDeterministicUuid, @@ -13,6 +15,7 @@ import { } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/create-deterministic-uuid.util'; export const computeStandardFields = ( + context: WorkspaceSyncContext, standardFieldMetadataCollection: ( | PartialFieldMetadata | PartialComputedFieldMetadata @@ -22,6 +25,9 @@ export const computeStandardFields = ( ): ComputedPartialFieldMetadata[] => { const fields: ComputedPartialFieldMetadata[] = []; + const isNewRelationEnabled = + context.featureFlags[FeatureFlagKey.IsNewRelationEnabled]; + for (const partialFieldMetadata of standardFieldMetadataCollection) { // Relation from standard object to custom object if ('argsFactory' in partialFieldMetadata) { @@ -52,18 +58,22 @@ export const computeStandardFields = ( defaultValue: null, }); - // Foreign key - fields.push({ - ...rest, - standardId: foreignKeyStandardId, - name: joinColumn, - type: FieldMetadataType.UUID, - label: `${data.label} ID (foreign key)`, - description: `${data.description} id foreign key`, - defaultValue: null, - icon: undefined, - isSystem: true, - }); + // Only add foreign key if new relation is disabled + // As new relation will no longer create the field metadata related to foreign key + if (!isNewRelationEnabled) { + // Foreign key + fields.push({ + ...rest, + standardId: foreignKeyStandardId, + name: joinColumn, + type: FieldMetadataType.UUID, + label: `${data.label} ID (foreign key)`, + description: `${data.description} id foreign key`, + defaultValue: null, + icon: undefined, + isSystem: true, + }); + } } } else { // Relation from standard object to standard object diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.module.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.module.ts index c7946f630..238acd9ad 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.module.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.module.ts @@ -17,6 +17,7 @@ import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/works import { workspaceSyncMetadataComparators } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators'; import { workspaceSyncMetadataFactories } from 'src/engine/workspace-manager/workspace-sync-metadata/factories'; import { WorkspaceMetadataUpdaterService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-metadata-updater.service'; +import { WorkspaceSyncFieldMetadataRelationService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-field-metadata-relation.service'; import { WorkspaceSyncFieldMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-field-metadata.service'; import { WorkspaceSyncIndexMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-index-metadata.service'; import { WorkspaceSyncObjectMetadataIdentifiersService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-object-metadata-identifiers.service'; @@ -50,6 +51,7 @@ import { WorkspaceSyncMetadataService } from 'src/engine/workspace-manager/works WorkspaceSyncObjectMetadataIdentifiersService, WorkspaceSyncRelationMetadataService, WorkspaceSyncFieldMetadataService, + WorkspaceSyncFieldMetadataRelationService, WorkspaceSyncMetadataService, WorkspaceSyncIndexMetadataService, SyncWorkspaceLoggerService, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service.ts index 1aee8779d..a3965f241 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service.ts @@ -5,6 +5,7 @@ import { DataSource, QueryFailedError } from 'typeorm'; import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service'; import { @@ -12,6 +13,7 @@ import { WorkspaceMigrationTableActionType, } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service'; +import { WorkspaceSyncFieldMetadataRelationService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-field-metadata-relation.service'; import { WorkspaceSyncFieldMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-field-metadata.service'; import { WorkspaceSyncIndexMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-index-metadata.service'; import { WorkspaceSyncObjectMetadataIdentifiersService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-object-metadata-identifiers.service'; @@ -30,14 +32,15 @@ export class WorkspaceSyncMetadataService { constructor( @InjectDataSource('metadata') private readonly metadataDataSource: DataSource, - private readonly featureFlagService: FeatureFlagService, private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService, private readonly workspaceSyncObjectMetadataService: WorkspaceSyncObjectMetadataService, private readonly workspaceSyncRelationMetadataService: WorkspaceSyncRelationMetadataService, private readonly workspaceSyncFieldMetadataService: WorkspaceSyncFieldMetadataService, + private readonly workspaceSyncFieldMetadataRelationService: WorkspaceSyncFieldMetadataRelationService, private readonly workspaceSyncIndexMetadataService: WorkspaceSyncIndexMetadataService, private readonly workspaceSyncObjectMetadataIdentifiersService: WorkspaceSyncObjectMetadataIdentifiersService, private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService, + private readonly featureFlagService: FeatureFlagService, ) {} /** @@ -67,16 +70,16 @@ export class WorkspaceSyncMetadataService { const manager = queryRunner.manager; try { + const isNewRelationEnabled = + await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IsNewRelationEnabled, + context.workspaceId, + ); + const workspaceMigrationRepository = manager.getRepository( WorkspaceMigrationEntity, ); - // Retrieve feature flags - const workspaceFeatureFlagsMap = - await this.featureFlagService.getWorkspaceFeatureFlagsMap( - context.workspaceId, - ); - this.logger.log('Syncing standard objects and fields metadata'); // 1 - Sync standard objects @@ -87,7 +90,6 @@ export class WorkspaceSyncMetadataService { context, manager, storage, - workspaceFeatureFlagsMap, ); const workspaceObjectMigrationsEnd = performance.now(); @@ -103,7 +105,6 @@ export class WorkspaceSyncMetadataService { context, manager, storage, - workspaceFeatureFlagsMap, ); const workspaceFieldMigrationsEnd = performance.now(); @@ -123,13 +124,24 @@ export class WorkspaceSyncMetadataService { // 3 - Sync standard relations on standard and custom objects const workspaceRelationMigrationsStart = performance.now(); - const workspaceRelationMigrations = - await this.workspaceSyncRelationMetadataService.synchronize( - context, - manager, - storage, - workspaceFeatureFlagsMap, - ); + + let workspaceRelationMigrations: Partial[] = []; + + if (isNewRelationEnabled) { + workspaceRelationMigrations = + await this.workspaceSyncFieldMetadataRelationService.synchronize( + context, + manager, + storage, + ); + } else { + workspaceRelationMigrations = + await this.workspaceSyncRelationMetadataService.synchronize( + context, + manager, + storage, + ); + } const workspaceRelationMigrationsEnd = performance.now(); @@ -144,7 +156,6 @@ export class WorkspaceSyncMetadataService { context, manager, storage, - workspaceFeatureFlagsMap, ); const workspaceIndexMigrationsEnd = performance.now(); @@ -160,7 +171,6 @@ export class WorkspaceSyncMetadataService { context, manager, storage, - workspaceFeatureFlagsMap, ); const workspaceObjectMetadataIdentifiersEnd = performance.now(); diff --git a/packages/twenty-server/src/modules/attachment/standard-objects/attachment.workspace-entity.ts b/packages/twenty-server/src/modules/attachment/standard-objects/attachment.workspace-entity.ts index 6f59e225a..f8d311aad 100644 --- a/packages/twenty-server/src/modules/attachment/standard-objects/attachment.workspace-entity.ts +++ b/packages/twenty-server/src/modules/attachment/standard-objects/attachment.workspace-entity.ts @@ -1,9 +1,9 @@ import { msg } from '@lingui/core/macro'; import { FieldMetadataType } from 'twenty-shared/types'; +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; -import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { CustomWorkspaceEntity } from 'src/engine/twenty-orm/custom.workspace-entity'; import { WorkspaceDynamicRelation } from 'src/engine/twenty-orm/decorators/workspace-dynamic-relation.decorator'; @@ -63,7 +63,7 @@ export class AttachmentWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: ATTACHMENT_STANDARD_FIELD_IDS.author, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Author`, description: msg`Attachment author`, icon: 'IconCircleUser', @@ -78,7 +78,7 @@ export class AttachmentWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: ATTACHMENT_STANDARD_FIELD_IDS.task, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Task`, description: msg`Attachment task`, icon: 'IconNotes', @@ -93,7 +93,7 @@ export class AttachmentWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: ATTACHMENT_STANDARD_FIELD_IDS.note, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Note`, description: msg`Attachment note`, icon: 'IconNotes', @@ -108,7 +108,7 @@ export class AttachmentWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: ATTACHMENT_STANDARD_FIELD_IDS.person, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Person`, description: msg`Attachment person`, icon: 'IconUser', @@ -123,7 +123,7 @@ export class AttachmentWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: ATTACHMENT_STANDARD_FIELD_IDS.company, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Company`, description: msg`Attachment company`, icon: 'IconBuildingSkyscraper', @@ -138,7 +138,7 @@ export class AttachmentWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: ATTACHMENT_STANDARD_FIELD_IDS.opportunity, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Opportunity`, description: msg`Attachment opportunity`, icon: 'IconBuildingSkyscraper', @@ -152,7 +152,7 @@ export class AttachmentWorkspaceEntity extends BaseWorkspaceEntity { opportunityId: string | null; @WorkspaceDynamicRelation({ - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, argsFactory: (oppositeObjectMetadata) => ({ standardId: ATTACHMENT_STANDARD_FIELD_IDS.custom, name: oppositeObjectMetadata.nameSingular, diff --git a/packages/twenty-server/src/modules/blocklist/standard-objects/blocklist.workspace-entity.ts b/packages/twenty-server/src/modules/blocklist/standard-objects/blocklist.workspace-entity.ts index 68b89bce7..380f1cded 100644 --- a/packages/twenty-server/src/modules/blocklist/standard-objects/blocklist.workspace-entity.ts +++ b/packages/twenty-server/src/modules/blocklist/standard-objects/blocklist.workspace-entity.ts @@ -1,9 +1,9 @@ import { msg } from '@lingui/core/macro'; import { FieldMetadataType } from 'twenty-shared/types'; +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; -import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator'; @@ -39,7 +39,7 @@ export class BlocklistWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: BLOCKLIST_STANDARD_FIELD_IDS.workspaceMember, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`WorkspaceMember`, description: msg`WorkspaceMember`, icon: 'IconCircleUser', diff --git a/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity.ts b/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity.ts index d05fcf855..978fd3c15 100644 --- a/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity.ts +++ b/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity.ts @@ -1,9 +1,9 @@ import { msg } from '@lingui/core/macro'; import { FieldMetadataType } from 'twenty-shared/types'; +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; -import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator'; @@ -51,7 +51,7 @@ export class CalendarChannelEventAssociationWorkspaceEntity extends BaseWorkspac @WorkspaceRelation({ standardId: CALENDAR_CHANNEL_EVENT_ASSOCIATION_STANDARD_FIELD_IDS.calendarChannel, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Channel ID`, description: msg`Channel ID`, icon: 'IconCalendar', @@ -66,7 +66,7 @@ export class CalendarChannelEventAssociationWorkspaceEntity extends BaseWorkspac @WorkspaceRelation({ standardId: CALENDAR_CHANNEL_EVENT_ASSOCIATION_STANDARD_FIELD_IDS.calendarEvent, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Event ID`, description: msg`Event ID`, icon: 'IconCalendar', diff --git a/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity.ts b/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity.ts index ad06c4bcc..ed8999130 100644 --- a/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity.ts +++ b/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity.ts @@ -3,12 +3,10 @@ import { registerEnumType } from '@nestjs/graphql'; import { msg } from '@lingui/core/macro'; import { FieldMetadataType } from 'twenty-shared/types'; +import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface'; +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; -import { - RelationMetadataType, - RelationOnDeleteAction, -} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator'; @@ -303,7 +301,7 @@ export class CalendarChannelWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: CALENDAR_CHANNEL_STANDARD_FIELD_IDS.connectedAccount, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Connected Account`, description: msg`Connected Account`, icon: 'IconUserCircle', @@ -318,7 +316,7 @@ export class CalendarChannelWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: CALENDAR_CHANNEL_STANDARD_FIELD_IDS.calendarChannelEventAssociations, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Calendar Channel Event Associations`, description: msg`Calendar Channel Event Associations`, icon: 'IconCalendar', diff --git a/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity.ts b/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity.ts index b19f17a82..9a289fbfa 100644 --- a/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity.ts +++ b/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity.ts @@ -1,9 +1,9 @@ import { msg } from '@lingui/core/macro'; import { FieldMetadataType } from 'twenty-shared/types'; +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; -import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator'; @@ -105,7 +105,7 @@ export class CalendarEventParticipantWorkspaceEntity extends BaseWorkspaceEntity @WorkspaceRelation({ standardId: CALENDAR_EVENT_PARTICIPANT_STANDARD_FIELD_IDS.calendarEvent, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Event ID`, description: msg`Event ID`, icon: 'IconCalendar', @@ -119,7 +119,7 @@ export class CalendarEventParticipantWorkspaceEntity extends BaseWorkspaceEntity @WorkspaceRelation({ standardId: CALENDAR_EVENT_PARTICIPANT_STANDARD_FIELD_IDS.person, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Person`, description: msg`Person`, icon: 'IconUser', @@ -134,7 +134,7 @@ export class CalendarEventParticipantWorkspaceEntity extends BaseWorkspaceEntity @WorkspaceRelation({ standardId: CALENDAR_EVENT_PARTICIPANT_STANDARD_FIELD_IDS.workspaceMember, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Workspace Member`, description: msg`Workspace Member`, icon: 'IconUser', diff --git a/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-event.workspace-entity.ts b/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-event.workspace-entity.ts index ba09d3bad..e33fea473 100644 --- a/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-event.workspace-entity.ts +++ b/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-event.workspace-entity.ts @@ -1,13 +1,11 @@ import { msg } from '@lingui/core/macro'; import { FieldMetadataType } from 'twenty-shared/types'; +import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface'; +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; import { LinksMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/links.composite-type'; -import { - RelationMetadataType, - RelationOnDeleteAction, -} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator'; @@ -151,7 +149,7 @@ export class CalendarEventWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: CALENDAR_EVENT_STANDARD_FIELD_IDS.calendarChannelEventAssociations, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Calendar Channel Event Associations`, description: msg`Calendar Channel Event Associations`, icon: 'IconCalendar', @@ -164,7 +162,7 @@ export class CalendarEventWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: CALENDAR_EVENT_STANDARD_FIELD_IDS.calendarEventParticipants, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Event Participants`, description: msg`Event Participants`, icon: 'IconUserCircle', diff --git a/packages/twenty-server/src/modules/company/standard-objects/company.workspace-entity.ts b/packages/twenty-server/src/modules/company/standard-objects/company.workspace-entity.ts index f2ebcfe7d..509e7b7e6 100644 --- a/packages/twenty-server/src/modules/company/standard-objects/company.workspace-entity.ts +++ b/packages/twenty-server/src/modules/company/standard-objects/company.workspace-entity.ts @@ -1,6 +1,8 @@ import { msg } from '@lingui/core/macro'; import { FieldMetadataType } from 'twenty-shared/types'; +import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface'; +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants'; @@ -9,10 +11,6 @@ import { AddressMetadata } from 'src/engine/metadata-modules/field-metadata/comp import { CurrencyMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/currency.composite-type'; import { LinksMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/links.composite-type'; import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; -import { - RelationMetadataType, - RelationOnDeleteAction, -} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceDuplicateCriteria } from 'src/engine/twenty-orm/decorators/workspace-duplicate-criteria.decorator'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; @@ -164,7 +162,7 @@ export class CompanyWorkspaceEntity extends BaseWorkspaceEntity { // Relations @WorkspaceRelation({ standardId: COMPANY_STANDARD_FIELD_IDS.people, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`People`, description: msg`People linked to the company.`, icon: 'IconUsers', @@ -176,7 +174,7 @@ export class CompanyWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: COMPANY_STANDARD_FIELD_IDS.accountOwner, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Account Owner`, description: msg`Your team member responsible for managing the company account`, icon: 'IconUserCircle', @@ -192,7 +190,7 @@ export class CompanyWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: COMPANY_STANDARD_FIELD_IDS.taskTargets, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Tasks`, description: msg`Tasks tied to the company`, icon: 'IconCheckbox', @@ -203,7 +201,7 @@ export class CompanyWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: COMPANY_STANDARD_FIELD_IDS.noteTargets, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Notes`, description: msg`Notes tied to the company`, icon: 'IconNotes', @@ -214,7 +212,7 @@ export class CompanyWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: COMPANY_STANDARD_FIELD_IDS.opportunities, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Opportunities`, description: msg`Opportunities linked to the company.`, icon: 'IconTargetArrow', @@ -226,7 +224,7 @@ export class CompanyWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: COMPANY_STANDARD_FIELD_IDS.favorites, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Favorites`, description: msg`Favorites linked to the company`, icon: 'IconHeart', @@ -239,7 +237,7 @@ export class CompanyWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: COMPANY_STANDARD_FIELD_IDS.attachments, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Attachments`, description: msg`Attachments linked to the company`, icon: 'IconFileImport', @@ -251,7 +249,7 @@ export class CompanyWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: COMPANY_STANDARD_FIELD_IDS.timelineActivities, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Timeline Activities`, description: msg`Timeline Activities linked to the company`, icon: 'IconIconTimelineEvent', diff --git a/packages/twenty-server/src/modules/connected-account/standard-objects/connected-account.workspace-entity.ts b/packages/twenty-server/src/modules/connected-account/standard-objects/connected-account.workspace-entity.ts index 47443a434..a629d66aa 100644 --- a/packages/twenty-server/src/modules/connected-account/standard-objects/connected-account.workspace-entity.ts +++ b/packages/twenty-server/src/modules/connected-account/standard-objects/connected-account.workspace-entity.ts @@ -4,12 +4,10 @@ import { FieldMetadataType, } from 'twenty-shared/types'; +import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface'; +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; -import { - RelationMetadataType, - RelationOnDeleteAction, -} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator'; @@ -113,7 +111,7 @@ export class ConnectedAccountWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: CONNECTED_ACCOUNT_STANDARD_FIELD_IDS.accountOwner, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Account Owner`, description: msg`Account Owner`, icon: 'IconUserCircle', @@ -127,7 +125,7 @@ export class ConnectedAccountWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: CONNECTED_ACCOUNT_STANDARD_FIELD_IDS.messageChannels, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Message Channels`, description: msg`Message Channels`, icon: 'IconMessage', @@ -138,7 +136,7 @@ export class ConnectedAccountWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: CONNECTED_ACCOUNT_STANDARD_FIELD_IDS.calendarChannels, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Calendar Channels`, description: msg`Calendar Channels`, icon: 'IconCalendar', diff --git a/packages/twenty-server/src/modules/favorite-folder/standard-objects/favorite-folder.workspace-entity.ts b/packages/twenty-server/src/modules/favorite-folder/standard-objects/favorite-folder.workspace-entity.ts index fbd689d4b..fcc50c9d2 100644 --- a/packages/twenty-server/src/modules/favorite-folder/standard-objects/favorite-folder.workspace-entity.ts +++ b/packages/twenty-server/src/modules/favorite-folder/standard-objects/favorite-folder.workspace-entity.ts @@ -1,12 +1,10 @@ import { msg } from '@lingui/core/macro'; import { FieldMetadataType } from 'twenty-shared/types'; +import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface'; +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; -import { - RelationMetadataType, - RelationOnDeleteAction, -} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator'; @@ -48,7 +46,7 @@ export class FavoriteFolderWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: FAVORITE_FOLDER_STANDARD_FIELD_IDS.favorites, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Favorites`, description: msg`Favorites in this folder`, icon: 'IconHeart', diff --git a/packages/twenty-server/src/modules/favorite/standard-objects/favorite.workspace-entity.ts b/packages/twenty-server/src/modules/favorite/standard-objects/favorite.workspace-entity.ts index bd4f2dbea..5b952f35e 100644 --- a/packages/twenty-server/src/modules/favorite/standard-objects/favorite.workspace-entity.ts +++ b/packages/twenty-server/src/modules/favorite/standard-objects/favorite.workspace-entity.ts @@ -1,9 +1,9 @@ import { msg } from '@lingui/core/macro'; import { FieldMetadataType } from 'twenty-shared/types'; +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; -import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { CustomWorkspaceEntity } from 'src/engine/twenty-orm/custom.workspace-entity'; import { WorkspaceDynamicRelation } from 'src/engine/twenty-orm/decorators/workspace-dynamic-relation.decorator'; @@ -28,6 +28,7 @@ import { WorkflowRunWorkspaceEntity } from 'src/modules/workflow/common/standard import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity'; import { WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; +import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; @WorkspaceEntity({ standardId: STANDARD_OBJECT_IDS.favorite, @@ -54,7 +55,7 @@ export class FavoriteWorkspaceEntity extends BaseWorkspaceEntity { // Relations @WorkspaceRelation({ standardId: FAVORITE_STANDARD_FIELD_IDS.forWorkspaceMember, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Workspace Member`, description: msg`Favorite workspace member`, icon: 'IconCircleUser', @@ -69,7 +70,7 @@ export class FavoriteWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: FAVORITE_STANDARD_FIELD_IDS.person, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Person`, description: msg`Favorite person`, icon: 'IconUser', @@ -84,7 +85,7 @@ export class FavoriteWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: FAVORITE_STANDARD_FIELD_IDS.company, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Company`, description: msg`Favorite company`, icon: 'IconBuildingSkyscraper', @@ -99,7 +100,7 @@ export class FavoriteWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: FAVORITE_STANDARD_FIELD_IDS.favoriteFolder, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Favorite Folder`, description: msg`The folder this favorite belongs to`, icon: 'IconFolder', @@ -114,7 +115,7 @@ export class FavoriteWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: FAVORITE_STANDARD_FIELD_IDS.opportunity, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Opportunity`, description: msg`Favorite opportunity`, icon: 'IconTargetArrow', @@ -129,7 +130,7 @@ export class FavoriteWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: FAVORITE_STANDARD_FIELD_IDS.workflow, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Workflow`, description: msg`Favorite workflow`, icon: 'IconSettingsAutomation', @@ -144,7 +145,7 @@ export class FavoriteWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: FAVORITE_STANDARD_FIELD_IDS.workflowVersion, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Workflow`, description: msg`Favorite workflow version`, icon: 'IconSettingsAutomation', @@ -159,7 +160,7 @@ export class FavoriteWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: FAVORITE_STANDARD_FIELD_IDS.workflowRun, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Workflow`, description: msg`Favorite workflow run`, icon: 'IconSettingsAutomation', @@ -174,7 +175,7 @@ export class FavoriteWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: FAVORITE_STANDARD_FIELD_IDS.task, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Task`, description: msg`Favorite task`, icon: 'IconCheckbox', @@ -189,7 +190,7 @@ export class FavoriteWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: FAVORITE_STANDARD_FIELD_IDS.note, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Note`, description: msg`Favorite note`, icon: 'IconNotes', @@ -204,12 +205,13 @@ export class FavoriteWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: FAVORITE_STANDARD_FIELD_IDS.view, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`View`, description: msg`Favorite view`, icon: 'IconLayoutCollage', inverseSideTarget: () => ViewWorkspaceEntity, inverseSideFieldKey: 'favorites', + onDelete: RelationOnDeleteAction.CASCADE, }) @WorkspaceIsNullable() view: Relation | null; @@ -218,7 +220,7 @@ export class FavoriteWorkspaceEntity extends BaseWorkspaceEntity { viewId: string; @WorkspaceDynamicRelation({ - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, argsFactory: (oppositeObjectMetadata) => ({ standardId: FAVORITE_STANDARD_FIELD_IDS.custom, name: oppositeObjectMetadata.nameSingular, diff --git a/packages/twenty-server/src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity.ts b/packages/twenty-server/src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity.ts index daaa70a0c..9c0a275f8 100644 --- a/packages/twenty-server/src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity.ts +++ b/packages/twenty-server/src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity.ts @@ -1,9 +1,9 @@ import { msg } from '@lingui/core/macro'; import { FieldMetadataType } from 'twenty-shared/types'; +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; -import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator'; @@ -87,7 +87,7 @@ export class MessageChannelMessageAssociationWorkspaceEntity extends BaseWorkspa @WorkspaceRelation({ standardId: MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_STANDARD_FIELD_IDS.messageChannel, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Message Channel Id`, description: msg`Message Channel Id`, icon: 'IconHash', @@ -102,7 +102,7 @@ export class MessageChannelMessageAssociationWorkspaceEntity extends BaseWorkspa @WorkspaceRelation({ standardId: MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_STANDARD_FIELD_IDS.message, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Message Id`, description: msg`Message Id`, icon: 'IconHash', diff --git a/packages/twenty-server/src/modules/messaging/common/standard-objects/message-channel.workspace-entity.ts b/packages/twenty-server/src/modules/messaging/common/standard-objects/message-channel.workspace-entity.ts index bdd23c688..c1ad6eb19 100644 --- a/packages/twenty-server/src/modules/messaging/common/standard-objects/message-channel.workspace-entity.ts +++ b/packages/twenty-server/src/modules/messaging/common/standard-objects/message-channel.workspace-entity.ts @@ -3,12 +3,10 @@ import { registerEnumType } from '@nestjs/graphql'; import { msg } from '@lingui/core/macro'; import { FieldMetadataType } from 'twenty-shared/types'; +import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface'; +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; -import { - RelationMetadataType, - RelationOnDeleteAction, -} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator'; @@ -355,7 +353,7 @@ export class MessageChannelWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: MESSAGE_CHANNEL_STANDARD_FIELD_IDS.connectedAccount, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Connected Account`, description: msg`Connected Account`, icon: 'IconUserCircle', @@ -370,7 +368,7 @@ export class MessageChannelWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: MESSAGE_CHANNEL_STANDARD_FIELD_IDS.messageChannelMessageAssociations, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Message Channel Association`, description: msg`Messages from the channel.`, icon: 'IconMessage', @@ -384,7 +382,7 @@ export class MessageChannelWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: MESSAGE_CHANNEL_STANDARD_FIELD_IDS.messageFolders, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Message Folders`, description: msg`Message Folders`, icon: 'IconFolder', diff --git a/packages/twenty-server/src/modules/messaging/common/standard-objects/message-folder.workspace-entity.ts b/packages/twenty-server/src/modules/messaging/common/standard-objects/message-folder.workspace-entity.ts index 9ab5cd5d5..ba4442c6f 100644 --- a/packages/twenty-server/src/modules/messaging/common/standard-objects/message-folder.workspace-entity.ts +++ b/packages/twenty-server/src/modules/messaging/common/standard-objects/message-folder.workspace-entity.ts @@ -2,7 +2,8 @@ import { msg } from '@lingui/core/macro'; import { Relation } from 'typeorm'; import { FieldMetadataType } from 'twenty-shared/types'; -import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; + import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator'; @@ -37,11 +38,12 @@ export class MessageFolderWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: MESSAGE_FOLDER_STANDARD_FIELD_IDS.messageChannel, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Message Channel`, description: msg`Message Channel`, icon: 'IconMessage', inverseSideTarget: () => MessageChannelWorkspaceEntity, + inverseSideFieldKey: 'messageFolders', }) messageChannel: Relation; diff --git a/packages/twenty-server/src/modules/messaging/common/standard-objects/message-participant.workspace-entity.ts b/packages/twenty-server/src/modules/messaging/common/standard-objects/message-participant.workspace-entity.ts index 9a7ab12bf..cec06c5a1 100644 --- a/packages/twenty-server/src/modules/messaging/common/standard-objects/message-participant.workspace-entity.ts +++ b/packages/twenty-server/src/modules/messaging/common/standard-objects/message-participant.workspace-entity.ts @@ -1,9 +1,9 @@ import { msg } from '@lingui/core/macro'; import { FieldMetadataType } from 'twenty-shared/types'; +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; -import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator'; @@ -67,7 +67,7 @@ export class MessageParticipantWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: MESSAGE_PARTICIPANT_STANDARD_FIELD_IDS.message, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Message`, description: msg`Message`, icon: 'IconMessage', @@ -81,7 +81,7 @@ export class MessageParticipantWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: MESSAGE_PARTICIPANT_STANDARD_FIELD_IDS.person, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Person`, description: msg`Person`, icon: 'IconUser', @@ -96,7 +96,7 @@ export class MessageParticipantWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: MESSAGE_PARTICIPANT_STANDARD_FIELD_IDS.workspaceMember, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Workspace Member`, description: msg`Workspace member`, icon: 'IconCircleUser', diff --git a/packages/twenty-server/src/modules/messaging/common/standard-objects/message-thread.workspace-entity.ts b/packages/twenty-server/src/modules/messaging/common/standard-objects/message-thread.workspace-entity.ts index ed5fb7e8c..f4f596ebe 100644 --- a/packages/twenty-server/src/modules/messaging/common/standard-objects/message-thread.workspace-entity.ts +++ b/packages/twenty-server/src/modules/messaging/common/standard-objects/message-thread.workspace-entity.ts @@ -1,11 +1,9 @@ import { msg } from '@lingui/core/macro'; +import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface'; +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; -import { - RelationMetadataType, - RelationOnDeleteAction, -} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator'; @@ -30,7 +28,7 @@ import { MessageWorkspaceEntity } from 'src/modules/messaging/common/standard-ob export class MessageThreadWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: MESSAGE_THREAD_STANDARD_FIELD_IDS.messages, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Messages`, description: msg`Messages from the thread.`, icon: 'IconMessage', diff --git a/packages/twenty-server/src/modules/messaging/common/standard-objects/message.workspace-entity.ts b/packages/twenty-server/src/modules/messaging/common/standard-objects/message.workspace-entity.ts index 4aa4967de..dafb9a7bc 100644 --- a/packages/twenty-server/src/modules/messaging/common/standard-objects/message.workspace-entity.ts +++ b/packages/twenty-server/src/modules/messaging/common/standard-objects/message.workspace-entity.ts @@ -1,12 +1,10 @@ import { msg } from '@lingui/core/macro'; import { FieldMetadataType } from 'twenty-shared/types'; +import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface'; +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; -import { - RelationMetadataType, - RelationOnDeleteAction, -} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator'; @@ -73,7 +71,7 @@ export class MessageWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: MESSAGE_STANDARD_FIELD_IDS.messageThread, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Message Thread Id`, description: msg`Message Thread Id`, icon: 'IconHash', @@ -89,7 +87,7 @@ export class MessageWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: MESSAGE_STANDARD_FIELD_IDS.messageParticipants, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Message Participants`, description: msg`Message Participants`, icon: 'IconUserCircle', @@ -101,7 +99,7 @@ export class MessageWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: MESSAGE_STANDARD_FIELD_IDS.messageChannelMessageAssociations, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Message Channel Association`, description: msg`Messages from the channel.`, icon: 'IconMessage', diff --git a/packages/twenty-server/src/modules/note/standard-objects/note-target.workspace-entity.ts b/packages/twenty-server/src/modules/note/standard-objects/note-target.workspace-entity.ts index 232e8edae..21cc84bde 100644 --- a/packages/twenty-server/src/modules/note/standard-objects/note-target.workspace-entity.ts +++ b/packages/twenty-server/src/modules/note/standard-objects/note-target.workspace-entity.ts @@ -1,8 +1,8 @@ import { msg } from '@lingui/core/macro'; +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; -import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { CustomWorkspaceEntity } from 'src/engine/twenty-orm/custom.workspace-entity'; import { WorkspaceDynamicRelation } from 'src/engine/twenty-orm/decorators/workspace-dynamic-relation.decorator'; @@ -31,7 +31,7 @@ import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/perso export class NoteTargetWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: NOTE_TARGET_STANDARD_FIELD_IDS.note, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Note`, description: msg`NoteTarget note`, icon: 'IconNotes', @@ -46,7 +46,7 @@ export class NoteTargetWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: NOTE_TARGET_STANDARD_FIELD_IDS.person, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Person`, description: msg`NoteTarget person`, icon: 'IconUser', @@ -61,7 +61,7 @@ export class NoteTargetWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: NOTE_TARGET_STANDARD_FIELD_IDS.company, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Company`, description: msg`NoteTarget company`, icon: 'IconBuildingSkyscraper', @@ -76,7 +76,7 @@ export class NoteTargetWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: NOTE_TARGET_STANDARD_FIELD_IDS.opportunity, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Opportunity`, description: msg`NoteTarget opportunity`, icon: 'IconTargetArrow', @@ -90,7 +90,7 @@ export class NoteTargetWorkspaceEntity extends BaseWorkspaceEntity { opportunityId: string | null; @WorkspaceDynamicRelation({ - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, argsFactory: (oppositeObjectMetadata) => ({ standardId: NOTE_TARGET_STANDARD_FIELD_IDS.custom, name: oppositeObjectMetadata.nameSingular, diff --git a/packages/twenty-server/src/modules/note/standard-objects/note.workspace-entity.ts b/packages/twenty-server/src/modules/note/standard-objects/note.workspace-entity.ts index 83cfcb285..f721f0753 100644 --- a/packages/twenty-server/src/modules/note/standard-objects/note.workspace-entity.ts +++ b/packages/twenty-server/src/modules/note/standard-objects/note.workspace-entity.ts @@ -1,16 +1,14 @@ import { msg } from '@lingui/core/macro'; import { FieldMetadataType } from 'twenty-shared/types'; +import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface'; +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants'; import { ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; import { RichTextV2Metadata } from 'src/engine/metadata-modules/field-metadata/composite-types/rich-text-v2.composite-type'; import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; -import { - RelationMetadataType, - RelationOnDeleteAction, -} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator'; @@ -105,7 +103,7 @@ export class NoteWorkspaceEntity extends BaseWorkspaceEntity { label: msg`Relations`, description: msg`Note targets`, icon: 'IconArrowUpRight', - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, inverseSideTarget: () => NoteTargetWorkspaceEntity, onDelete: RelationOnDeleteAction.SET_NULL, }) @@ -117,7 +115,7 @@ export class NoteWorkspaceEntity extends BaseWorkspaceEntity { label: msg`Attachments`, description: msg`Note attachments`, icon: 'IconFileImport', - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, inverseSideTarget: () => AttachmentWorkspaceEntity, onDelete: RelationOnDeleteAction.SET_NULL, }) @@ -126,7 +124,7 @@ export class NoteWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: NOTE_STANDARD_FIELD_IDS.timelineActivities, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Timeline Activities`, description: msg`Timeline Activities linked to the note.`, icon: 'IconTimelineEvent', @@ -138,7 +136,7 @@ export class NoteWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: NOTE_STANDARD_FIELD_IDS.favorites, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Favorites`, description: msg`Favorites linked to the note`, icon: 'IconHeart', diff --git a/packages/twenty-server/src/modules/opportunity/standard-objects/opportunity.workspace-entity.ts b/packages/twenty-server/src/modules/opportunity/standard-objects/opportunity.workspace-entity.ts index a05511cd0..ebc751ba2 100644 --- a/packages/twenty-server/src/modules/opportunity/standard-objects/opportunity.workspace-entity.ts +++ b/packages/twenty-server/src/modules/opportunity/standard-objects/opportunity.workspace-entity.ts @@ -1,16 +1,14 @@ import { msg } from '@lingui/core/macro'; import { FieldMetadataType } from 'twenty-shared/types'; +import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface'; +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants'; import { ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; import { CurrencyMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/currency.composite-type'; import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; -import { - RelationMetadataType, - RelationOnDeleteAction, -} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator'; @@ -128,7 +126,7 @@ export class OpportunityWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: OPPORTUNITY_STANDARD_FIELD_IDS.pointOfContact, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Point of Contact`, description: msg`Opportunity point of contact`, icon: 'IconUser', @@ -144,7 +142,7 @@ export class OpportunityWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: OPPORTUNITY_STANDARD_FIELD_IDS.company, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Company`, description: msg`Opportunity company`, icon: 'IconBuildingSkyscraper', @@ -160,7 +158,7 @@ export class OpportunityWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: OPPORTUNITY_STANDARD_FIELD_IDS.favorites, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Favorites`, description: msg`Favorites linked to the opportunity`, icon: 'IconHeart', @@ -173,7 +171,7 @@ export class OpportunityWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: OPPORTUNITY_STANDARD_FIELD_IDS.taskTargets, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Tasks`, description: msg`Tasks tied to the opportunity`, icon: 'IconCheckbox', @@ -184,7 +182,7 @@ export class OpportunityWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: OPPORTUNITY_STANDARD_FIELD_IDS.noteTargets, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Notes`, description: msg`Notes tied to the opportunity`, icon: 'IconNotes', @@ -195,7 +193,7 @@ export class OpportunityWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: OPPORTUNITY_STANDARD_FIELD_IDS.attachments, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Attachments`, description: msg`Attachments linked to the opportunity`, icon: 'IconFileImport', @@ -207,7 +205,7 @@ export class OpportunityWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: OPPORTUNITY_STANDARD_FIELD_IDS.timelineActivities, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Timeline Activities`, description: msg`Timeline Activities linked to the opportunity.`, icon: 'IconTimelineEvent', diff --git a/packages/twenty-server/src/modules/person/standard-objects/person.workspace-entity.ts b/packages/twenty-server/src/modules/person/standard-objects/person.workspace-entity.ts index 74778bc1d..29fd39a60 100644 --- a/packages/twenty-server/src/modules/person/standard-objects/person.workspace-entity.ts +++ b/packages/twenty-server/src/modules/person/standard-objects/person.workspace-entity.ts @@ -1,6 +1,8 @@ import { msg } from '@lingui/core/macro'; import { FieldMetadataType } from 'twenty-shared/types'; +import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface'; +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants'; @@ -10,10 +12,6 @@ import { FullNameMetadata } from 'src/engine/metadata-modules/field-metadata/com import { LinksMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/links.composite-type'; import { PhonesMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/phones.composite-type'; import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; -import { - RelationMetadataType, - RelationOnDeleteAction, -} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceDuplicateCriteria } from 'src/engine/twenty-orm/decorators/workspace-duplicate-criteria.decorator'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; @@ -181,7 +179,7 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity { // Relations @WorkspaceRelation({ standardId: PERSON_STANDARD_FIELD_IDS.company, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Company`, description: msg`Contact’s company`, icon: 'IconBuildingSkyscraper', @@ -196,7 +194,7 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: PERSON_STANDARD_FIELD_IDS.pointOfContactForOpportunities, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Linked Opportunities`, description: msg`List of opportunities for which that person is the point of contact`, icon: 'IconTargetArrow', @@ -208,7 +206,7 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: PERSON_STANDARD_FIELD_IDS.taskTargets, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Tasks`, description: msg`Tasks tied to the contact`, icon: 'IconCheckbox', @@ -219,7 +217,7 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: PERSON_STANDARD_FIELD_IDS.noteTargets, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Notes`, description: msg`Notes tied to the contact`, icon: 'IconNotes', @@ -230,7 +228,7 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: PERSON_STANDARD_FIELD_IDS.favorites, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Favorites`, description: msg`Favorites linked to the contact`, icon: 'IconHeart', @@ -242,7 +240,7 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: PERSON_STANDARD_FIELD_IDS.attachments, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Attachments`, description: msg`Attachments linked to the contact.`, icon: 'IconFileImport', @@ -253,7 +251,7 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: PERSON_STANDARD_FIELD_IDS.messageParticipants, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Message Participants`, description: msg`Message Participants`, icon: 'IconUserCircle', @@ -266,7 +264,7 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: PERSON_STANDARD_FIELD_IDS.calendarEventParticipants, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Calendar Event Participants`, description: msg`Calendar Event Participants`, icon: 'IconCalendar', @@ -280,7 +278,7 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: PERSON_STANDARD_FIELD_IDS.timelineActivities, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Events`, description: msg`Events linked to the person`, icon: 'IconTimelineEvent', diff --git a/packages/twenty-server/src/modules/task/standard-objects/task-target.workspace-entity.ts b/packages/twenty-server/src/modules/task/standard-objects/task-target.workspace-entity.ts index 57e23f182..603c3988e 100644 --- a/packages/twenty-server/src/modules/task/standard-objects/task-target.workspace-entity.ts +++ b/packages/twenty-server/src/modules/task/standard-objects/task-target.workspace-entity.ts @@ -1,8 +1,8 @@ import { msg } from '@lingui/core/macro'; +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; -import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { CustomWorkspaceEntity } from 'src/engine/twenty-orm/custom.workspace-entity'; import { WorkspaceDynamicRelation } from 'src/engine/twenty-orm/decorators/workspace-dynamic-relation.decorator'; @@ -31,7 +31,7 @@ import { TaskWorkspaceEntity } from 'src/modules/task/standard-objects/task.work export class TaskTargetWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: TASK_TARGET_STANDARD_FIELD_IDS.task, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Task`, description: msg`TaskTarget task`, icon: 'IconCheckbox', @@ -46,7 +46,7 @@ export class TaskTargetWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: TASK_TARGET_STANDARD_FIELD_IDS.person, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Person`, description: msg`TaskTarget person`, icon: 'IconUser', @@ -61,7 +61,7 @@ export class TaskTargetWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: TASK_TARGET_STANDARD_FIELD_IDS.company, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Company`, description: msg`TaskTarget company`, icon: 'IconBuildingSkyscraper', @@ -76,7 +76,7 @@ export class TaskTargetWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: TASK_TARGET_STANDARD_FIELD_IDS.opportunity, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Opportunity`, description: msg`TaskTarget opportunity`, icon: 'IconTargetArrow', @@ -90,7 +90,7 @@ export class TaskTargetWorkspaceEntity extends BaseWorkspaceEntity { opportunityId: string | null; @WorkspaceDynamicRelation({ - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, argsFactory: (oppositeObjectMetadata) => ({ standardId: TASK_TARGET_STANDARD_FIELD_IDS.custom, name: oppositeObjectMetadata.nameSingular, diff --git a/packages/twenty-server/src/modules/task/standard-objects/task.workspace-entity.ts b/packages/twenty-server/src/modules/task/standard-objects/task.workspace-entity.ts index f48c8e636..00d198632 100644 --- a/packages/twenty-server/src/modules/task/standard-objects/task.workspace-entity.ts +++ b/packages/twenty-server/src/modules/task/standard-objects/task.workspace-entity.ts @@ -1,16 +1,14 @@ import { msg } from '@lingui/core/macro'; import { FieldMetadataType } from 'twenty-shared/types'; +import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface'; +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants'; import { ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; import { RichTextV2Metadata } from 'src/engine/metadata-modules/field-metadata/composite-types/rich-text-v2.composite-type'; import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; -import { - RelationMetadataType, - RelationOnDeleteAction, -} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator'; @@ -144,7 +142,7 @@ export class TaskWorkspaceEntity extends BaseWorkspaceEntity { label: msg`Relations`, description: msg`Task targets`, icon: 'IconArrowUpRight', - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, inverseSideTarget: () => TaskTargetWorkspaceEntity, onDelete: RelationOnDeleteAction.SET_NULL, }) @@ -156,7 +154,7 @@ export class TaskWorkspaceEntity extends BaseWorkspaceEntity { label: msg`Attachments`, description: msg`Task attachments`, icon: 'IconFileImport', - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, inverseSideTarget: () => AttachmentWorkspaceEntity, onDelete: RelationOnDeleteAction.SET_NULL, }) @@ -168,7 +166,7 @@ export class TaskWorkspaceEntity extends BaseWorkspaceEntity { label: msg`Assignee`, description: msg`Task assignee`, icon: 'IconUserCircle', - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, inverseSideTarget: () => WorkspaceMemberWorkspaceEntity, inverseSideFieldKey: 'assignedTasks', onDelete: RelationOnDeleteAction.SET_NULL, @@ -181,7 +179,7 @@ export class TaskWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: TASK_STANDARD_FIELD_IDS.timelineActivities, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Timeline Activities`, description: msg`Timeline Activities linked to the task.`, icon: 'IconTimelineEvent', @@ -193,7 +191,7 @@ export class TaskWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: TASK_STANDARD_FIELD_IDS.favorites, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Favorites`, description: msg`Favorites linked to the task`, icon: 'IconHeart', diff --git a/packages/twenty-server/src/modules/timeline/standard-objects/audit-log.workspace-entity.ts b/packages/twenty-server/src/modules/timeline/standard-objects/audit-log.workspace-entity.ts index ead1b6358..440a4771f 100644 --- a/packages/twenty-server/src/modules/timeline/standard-objects/audit-log.workspace-entity.ts +++ b/packages/twenty-server/src/modules/timeline/standard-objects/audit-log.workspace-entity.ts @@ -1,9 +1,9 @@ import { msg } from '@lingui/core/macro'; import { FieldMetadataType } from 'twenty-shared/types'; +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; -import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator'; @@ -86,7 +86,7 @@ export class AuditLogWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: AUDIT_LOGS_STANDARD_FIELD_IDS.workspaceMember, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Workspace Member`, description: msg`Event workspace member`, icon: 'IconCircleUser', diff --git a/packages/twenty-server/src/modules/timeline/standard-objects/timeline-activity.workspace-entity.ts b/packages/twenty-server/src/modules/timeline/standard-objects/timeline-activity.workspace-entity.ts index 38f288c7d..8a2eefa5b 100644 --- a/packages/twenty-server/src/modules/timeline/standard-objects/timeline-activity.workspace-entity.ts +++ b/packages/twenty-server/src/modules/timeline/standard-objects/timeline-activity.workspace-entity.ts @@ -1,9 +1,9 @@ import { msg } from '@lingui/core/macro'; import { FieldMetadataType } from 'twenty-shared/types'; +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; -import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { CustomWorkspaceEntity } from 'src/engine/twenty-orm/custom.workspace-entity'; import { WorkspaceDynamicRelation } from 'src/engine/twenty-orm/decorators/workspace-dynamic-relation.decorator'; @@ -100,7 +100,7 @@ export class TimelineActivityWorkspaceEntity extends BaseWorkspaceEntity { // Who made the action @WorkspaceRelation({ standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.workspaceMember, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Workspace Member`, description: msg`Event workspace member`, icon: 'IconCircleUser', @@ -115,7 +115,7 @@ export class TimelineActivityWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.person, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Person`, description: msg`Event person`, icon: 'IconUser', @@ -130,7 +130,7 @@ export class TimelineActivityWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.company, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Company`, description: msg`Event company`, icon: 'IconBuildingSkyscraper', @@ -145,7 +145,7 @@ export class TimelineActivityWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.opportunity, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Opportunity`, description: msg`Event opportunity`, icon: 'IconTargetArrow', @@ -160,7 +160,7 @@ export class TimelineActivityWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.note, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Note`, description: msg`Event note`, icon: 'IconTargetArrow', @@ -175,7 +175,7 @@ export class TimelineActivityWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.task, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Task`, description: msg`Event task`, icon: 'IconTargetArrow', @@ -190,7 +190,7 @@ export class TimelineActivityWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.workflow, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Workflow`, description: msg`Event workflow`, icon: 'IconTargetArrow', @@ -205,7 +205,7 @@ export class TimelineActivityWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.workflowVersion, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`WorkflowVersion`, description: msg`Event workflow version`, icon: 'IconTargetArrow', @@ -220,7 +220,7 @@ export class TimelineActivityWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.workflowRun, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Workflow Run`, description: msg`Event workflow run`, icon: 'IconTargetArrow', @@ -234,7 +234,7 @@ export class TimelineActivityWorkspaceEntity extends BaseWorkspaceEntity { workflowRunId: string | null; @WorkspaceDynamicRelation({ - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, argsFactory: (oppositeObjectMetadata) => ({ standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.custom, name: oppositeObjectMetadata.nameSingular, diff --git a/packages/twenty-server/src/modules/view/standard-objects/view-field.workspace-entity.ts b/packages/twenty-server/src/modules/view/standard-objects/view-field.workspace-entity.ts index 7f7f08f51..6fe5dcf8d 100644 --- a/packages/twenty-server/src/modules/view/standard-objects/view-field.workspace-entity.ts +++ b/packages/twenty-server/src/modules/view/standard-objects/view-field.workspace-entity.ts @@ -1,11 +1,12 @@ import { registerEnumType } from '@nestjs/graphql'; import { msg } from '@lingui/core/macro'; -import { Relation } from 'typeorm'; import { FieldMetadataType } from 'twenty-shared/types'; +import { Relation } from 'typeorm'; + +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { AGGREGATE_OPERATIONS } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant'; -import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator'; @@ -19,6 +20,7 @@ import { VIEW_FIELD_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/work import { STANDARD_OBJECT_ICONS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-icons'; import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity'; +import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; registerEnumType(AGGREGATE_OPERATIONS, { name: 'AggregateOperations', @@ -81,12 +83,13 @@ export class ViewFieldWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: VIEW_FIELD_STANDARD_FIELD_IDS.view, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`View`, description: msg`View Field related view`, icon: 'IconLayoutCollage', inverseSideTarget: () => ViewWorkspaceEntity, inverseSideFieldKey: 'viewFields', + onDelete: RelationOnDeleteAction.CASCADE, }) view: Relation; diff --git a/packages/twenty-server/src/modules/view/standard-objects/view-filter-group.workspace-entity.ts b/packages/twenty-server/src/modules/view/standard-objects/view-filter-group.workspace-entity.ts index f94f1b7b4..04a93446c 100644 --- a/packages/twenty-server/src/modules/view/standard-objects/view-filter-group.workspace-entity.ts +++ b/packages/twenty-server/src/modules/view/standard-objects/view-filter-group.workspace-entity.ts @@ -1,8 +1,9 @@ import { msg } from '@lingui/core/macro'; -import { Relation } from 'typeorm'; import { FieldMetadataType } from 'twenty-shared/types'; +import { Relation } from 'typeorm'; + +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; -import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator'; @@ -14,6 +15,7 @@ import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-re import { VIEW_FILTER_GROUP_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity'; +import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; export enum ViewFilterGroupLogicalOperator { AND = 'AND', @@ -34,11 +36,12 @@ export enum ViewFilterGroupLogicalOperator { export class ViewFilterGroupWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: VIEW_FILTER_GROUP_STANDARD_FIELD_IDS.view, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`View`, description: msg`View`, inverseSideTarget: () => ViewWorkspaceEntity, inverseSideFieldKey: 'viewFilterGroups', + onDelete: RelationOnDeleteAction.CASCADE, }) view: Relation; diff --git a/packages/twenty-server/src/modules/view/standard-objects/view-filter.workspace-entity.ts b/packages/twenty-server/src/modules/view/standard-objects/view-filter.workspace-entity.ts index 3c469f543..d82283c52 100644 --- a/packages/twenty-server/src/modules/view/standard-objects/view-filter.workspace-entity.ts +++ b/packages/twenty-server/src/modules/view/standard-objects/view-filter.workspace-entity.ts @@ -1,9 +1,9 @@ import { msg } from '@lingui/core/macro'; import { FieldMetadataType } from 'twenty-shared/types'; +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; -import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator'; @@ -16,6 +16,7 @@ import { VIEW_FILTER_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/wor import { STANDARD_OBJECT_ICONS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-icons'; import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity'; +import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; @WorkspaceEntity({ standardId: STANDARD_OBJECT_IDS.viewFilter, @@ -63,12 +64,13 @@ export class ViewFilterWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: VIEW_FILTER_STANDARD_FIELD_IDS.view, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`View`, description: msg`View Filter related view`, icon: 'IconLayoutCollage', inverseSideTarget: () => ViewWorkspaceEntity, inverseSideFieldKey: 'viewFilters', + onDelete: RelationOnDeleteAction.CASCADE, }) @WorkspaceIsNullable() view: Relation | null; diff --git a/packages/twenty-server/src/modules/view/standard-objects/view-group.workspace-entity.ts b/packages/twenty-server/src/modules/view/standard-objects/view-group.workspace-entity.ts index b2054bb4b..8d7150ab7 100644 --- a/packages/twenty-server/src/modules/view/standard-objects/view-group.workspace-entity.ts +++ b/packages/twenty-server/src/modules/view/standard-objects/view-group.workspace-entity.ts @@ -1,7 +1,8 @@ import { msg } from '@lingui/core/macro'; import { FieldMetadataType } from 'twenty-shared/types'; -import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; + import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator'; @@ -14,6 +15,7 @@ import { VIEW_GROUP_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/work import { STANDARD_OBJECT_ICONS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-icons'; import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity'; +import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; @WorkspaceEntity({ standardId: STANDARD_OBJECT_IDS.viewGroup, @@ -66,12 +68,13 @@ export class ViewGroupWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: VIEW_GROUP_STANDARD_FIELD_IDS.view, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`View`, description: msg`View Group related view`, icon: 'IconLayoutCollage', inverseSideTarget: () => ViewWorkspaceEntity, inverseSideFieldKey: 'viewGroups', + onDelete: RelationOnDeleteAction.CASCADE, }) @WorkspaceIsNullable() view?: ViewWorkspaceEntity | null; diff --git a/packages/twenty-server/src/modules/view/standard-objects/view-sort.workspace-entity.ts b/packages/twenty-server/src/modules/view/standard-objects/view-sort.workspace-entity.ts index aab984d18..3f37acf04 100644 --- a/packages/twenty-server/src/modules/view/standard-objects/view-sort.workspace-entity.ts +++ b/packages/twenty-server/src/modules/view/standard-objects/view-sort.workspace-entity.ts @@ -1,9 +1,9 @@ import { msg } from '@lingui/core/macro'; import { FieldMetadataType } from 'twenty-shared/types'; +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; -import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator'; @@ -17,6 +17,7 @@ import { VIEW_SORT_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/works import { STANDARD_OBJECT_ICONS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-icons'; import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity'; +import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; @WorkspaceEntity({ standardId: STANDARD_OBJECT_IDS.viewSort, @@ -53,12 +54,13 @@ export class ViewSortWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: VIEW_SORT_STANDARD_FIELD_IDS.view, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`View`, description: msg`View Sort related view`, icon: 'IconLayoutCollage', inverseSideTarget: () => ViewWorkspaceEntity, inverseSideFieldKey: 'viewSorts', + onDelete: RelationOnDeleteAction.CASCADE, }) @WorkspaceIsNullable() view: Relation | null; diff --git a/packages/twenty-server/src/modules/view/standard-objects/view.workspace-entity.ts b/packages/twenty-server/src/modules/view/standard-objects/view.workspace-entity.ts index 32f086841..8ac2b84df 100644 --- a/packages/twenty-server/src/modules/view/standard-objects/view.workspace-entity.ts +++ b/packages/twenty-server/src/modules/view/standard-objects/view.workspace-entity.ts @@ -3,13 +3,11 @@ import { registerEnumType } from '@nestjs/graphql'; import { msg } from '@lingui/core/macro'; import { FieldMetadataType } from 'twenty-shared/types'; +import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface'; +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; import { AGGREGATE_OPERATIONS } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant'; -import { - RelationMetadataType, - RelationOnDeleteAction, -} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator'; @@ -147,7 +145,7 @@ export class ViewWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: VIEW_STANDARD_FIELD_IDS.viewFields, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`View Fields`, description: msg`View Fields`, icon: 'IconTag', @@ -159,7 +157,7 @@ export class ViewWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: VIEW_STANDARD_FIELD_IDS.viewGroups, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`View Groups`, description: msg`View Groups`, icon: 'IconTag', @@ -171,7 +169,7 @@ export class ViewWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: VIEW_STANDARD_FIELD_IDS.viewFilters, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`View Filters`, description: msg`View Filters`, icon: 'IconFilterBolt', @@ -183,7 +181,7 @@ export class ViewWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: VIEW_STANDARD_FIELD_IDS.viewFilterGroups, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`View Filter Groups`, description: msg`View Filter Groups`, icon: 'IconFilterBolt', @@ -195,7 +193,7 @@ export class ViewWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: VIEW_STANDARD_FIELD_IDS.viewSorts, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`View Sorts`, description: msg`View Sorts`, icon: 'IconArrowsSort', @@ -207,7 +205,7 @@ export class ViewWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: VIEW_STANDARD_FIELD_IDS.favorites, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Favorites`, description: msg`Favorites linked to the view`, icon: 'IconHeart', diff --git a/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-event-listener.workspace-entity.ts b/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-event-listener.workspace-entity.ts index 2f25cc10c..77389988b 100644 --- a/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-event-listener.workspace-entity.ts +++ b/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-event-listener.workspace-entity.ts @@ -1,9 +1,9 @@ import { msg } from '@lingui/core/macro'; import { FieldMetadataType } from 'twenty-shared/types'; +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; -import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator'; @@ -39,7 +39,7 @@ export class WorkflowEventListenerWorkspaceEntity extends BaseWorkspaceEntity { // Relations @WorkspaceRelation({ standardId: WORKFLOW_EVENT_LISTENER_STANDARD_FIELD_IDS.workflow, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Workflow`, description: msg`WorkflowEventListener workflow`, icon: 'IconSettingsAutomation', diff --git a/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-run.workspace-entity.ts b/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-run.workspace-entity.ts index c628fe60b..6cfa552d3 100644 --- a/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-run.workspace-entity.ts +++ b/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-run.workspace-entity.ts @@ -1,13 +1,11 @@ import { msg } from '@lingui/core/macro'; import { FieldMetadataType } from 'twenty-shared/types'; +import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface'; +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; import { ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; -import { - RelationMetadataType, - RelationOnDeleteAction, -} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator'; @@ -165,7 +163,7 @@ export class WorkflowRunWorkspaceEntity extends BaseWorkspaceEntity { // Relations @WorkspaceRelation({ standardId: WORKFLOW_RUN_STANDARD_FIELD_IDS.workflowVersion, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Workflow version`, description: msg`Workflow version linked to the run.`, icon: 'IconVersions', @@ -179,7 +177,7 @@ export class WorkflowRunWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: WORKFLOW_RUN_STANDARD_FIELD_IDS.workflow, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Workflow`, description: msg`Workflow linked to the run.`, icon: 'IconSettingsAutomation', @@ -193,7 +191,7 @@ export class WorkflowRunWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: WORKFLOW_RUN_STANDARD_FIELD_IDS.favorites, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Favorites`, description: msg`Favorites linked to the workflow run`, icon: 'IconHeart', @@ -205,7 +203,7 @@ export class WorkflowRunWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: WORKFLOW_RUN_STANDARD_FIELD_IDS.timelineActivities, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Timeline Activities`, description: msg`Timeline activities linked to the run`, inverseSideTarget: () => TimelineActivityWorkspaceEntity, diff --git a/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-version.workspace-entity.ts b/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-version.workspace-entity.ts index f58c2af95..7858caaf4 100644 --- a/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-version.workspace-entity.ts +++ b/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-version.workspace-entity.ts @@ -1,13 +1,11 @@ import { msg } from '@lingui/core/macro'; import { FieldMetadataType } from 'twenty-shared/types'; +import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface'; +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; import { FieldMetadataComplexOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input'; -import { - RelationMetadataType, - RelationOnDeleteAction, -} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator'; @@ -123,7 +121,7 @@ export class WorkflowVersionWorkspaceEntity extends BaseWorkspaceEntity { // Relations @WorkspaceRelation({ standardId: WORKFLOW_VERSION_STANDARD_FIELD_IDS.workflow, - type: RelationMetadataType.MANY_TO_ONE, + type: RelationType.MANY_TO_ONE, label: msg`Workflow`, description: msg`WorkflowVersion workflow`, icon: 'IconSettingsAutomation', @@ -138,7 +136,7 @@ export class WorkflowVersionWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: WORKFLOW_VERSION_STANDARD_FIELD_IDS.runs, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Runs`, description: msg`Workflow runs linked to the version.`, icon: 'IconRun', @@ -150,7 +148,7 @@ export class WorkflowVersionWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: WORKFLOW_VERSION_STANDARD_FIELD_IDS.favorites, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Favorites`, description: msg`Favorites linked to the workflow version`, icon: 'IconHeart', @@ -162,7 +160,7 @@ export class WorkflowVersionWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: WORKFLOW_VERSION_STANDARD_FIELD_IDS.timelineActivities, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Timeline Activities`, description: msg`Timeline activities linked to the version`, inverseSideTarget: () => TimelineActivityWorkspaceEntity, diff --git a/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow.workspace-entity.ts b/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow.workspace-entity.ts index 7fd97d169..3ceccbff4 100644 --- a/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow.workspace-entity.ts +++ b/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow.workspace-entity.ts @@ -1,14 +1,12 @@ import { msg } from '@lingui/core/macro'; import { FieldMetadataType } from 'twenty-shared/types'; +import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface'; +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; import { ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; import { FieldMetadataComplexOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input'; -import { - RelationMetadataType, - RelationOnDeleteAction, -} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator'; @@ -106,7 +104,7 @@ export class WorkflowWorkspaceEntity extends BaseWorkspaceEntity { // Relations @WorkspaceRelation({ standardId: WORKFLOW_STANDARD_FIELD_IDS.versions, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Versions`, description: msg`Workflow versions linked to the workflow.`, icon: 'IconVersions', @@ -117,7 +115,7 @@ export class WorkflowWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: WORKFLOW_STANDARD_FIELD_IDS.runs, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Runs`, description: msg`Workflow runs linked to the workflow.`, icon: 'IconRun', @@ -128,7 +126,7 @@ export class WorkflowWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: WORKFLOW_STANDARD_FIELD_IDS.eventListeners, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Event Listeners`, description: msg`Workflow event listeners linked to the workflow.`, inverseSideTarget: () => WorkflowEventListenerWorkspaceEntity, @@ -139,7 +137,7 @@ export class WorkflowWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: WORKFLOW_STANDARD_FIELD_IDS.favorites, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Favorites`, description: msg`Favorites linked to the workflow`, icon: 'IconHeart', @@ -151,7 +149,7 @@ export class WorkflowWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: WORKFLOW_STANDARD_FIELD_IDS.timelineActivities, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Timeline Activities`, description: msg`Timeline activities linked to the workflow`, inverseSideTarget: () => TimelineActivityWorkspaceEntity, diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/find-records.workflow-action.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/find-records.workflow-action.ts index d51f9c42a..17cbc92f7 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/find-records.workflow-action.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/find-records.workflow-action.ts @@ -12,6 +12,7 @@ import { WorkflowExecutor } from 'src/modules/workflow/workflow-executor/interfa import { QUERY_MAX_RECORDS } from 'src/engine/api/graphql/graphql-query-runner/constants/query-max-records.constant'; import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; @@ -86,6 +87,7 @@ export class FindRecordsWorkflowAction implements WorkflowExecutor { const graphqlQueryParser = new GraphqlQueryParser( objectMetadataItemWithFieldsMaps.fieldsByName, + objectMetadataItemWithFieldsMaps.fieldsByJoinColumnName, objectMetadataMaps, featureFlagMaps, ); @@ -120,6 +122,11 @@ export class FindRecordsWorkflowAction implements WorkflowExecutor { repository: WorkspaceRepository, graphqlQueryParser: GraphqlQueryParser, ) { + const isNewRelationEnabled = await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IsNewRelationEnabled, + objectMetadataItemWithFieldsMaps.workspaceId, + ); + const queryBuilder = repository.createQueryBuilder( workflowActionInput.objectName, ); @@ -150,6 +157,7 @@ export class FindRecordsWorkflowAction implements WorkflowExecutor { nonFormattedObjectRecords, objectMetadataItemWithFieldsMaps, objectMetadataMaps, + isNewRelationEnabled, ); } diff --git a/packages/twenty-server/src/modules/workspace-member/standard-objects/workspace-member.workspace-entity.ts b/packages/twenty-server/src/modules/workspace-member/standard-objects/workspace-member.workspace-entity.ts index 6de04496c..f4538e7a8 100644 --- a/packages/twenty-server/src/modules/workspace-member/standard-objects/workspace-member.workspace-entity.ts +++ b/packages/twenty-server/src/modules/workspace-member/standard-objects/workspace-member.workspace-entity.ts @@ -4,15 +4,13 @@ import { msg } from '@lingui/core/macro'; import { APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations'; import { FieldMetadataType } from 'twenty-shared/types'; +import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface'; +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants'; import { FullNameMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type'; import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; -import { - RelationMetadataType, - RelationOnDeleteAction, -} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator'; @@ -240,7 +238,7 @@ export class WorkspaceMemberWorkspaceEntity extends BaseWorkspaceEntity { // Relations @WorkspaceRelation({ standardId: WORKSPACE_MEMBER_STANDARD_FIELD_IDS.assignedTasks, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Assigned tasks`, description: msg`Tasks assigned to the workspace member`, icon: 'IconCheckbox', @@ -252,7 +250,7 @@ export class WorkspaceMemberWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: WORKSPACE_MEMBER_STANDARD_FIELD_IDS.favorites, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Favorites`, description: msg`Favorites linked to the workspace member`, icon: 'IconHeart', @@ -264,7 +262,7 @@ export class WorkspaceMemberWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: WORKSPACE_MEMBER_STANDARD_FIELD_IDS.accountOwnerForCompanies, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Account Owner For Companies`, description: msg`Account owner for companies`, icon: 'IconBriefcase', @@ -276,7 +274,7 @@ export class WorkspaceMemberWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: WORKSPACE_MEMBER_STANDARD_FIELD_IDS.authoredAttachments, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Authored attachments`, description: msg`Attachments created by the workspace member`, icon: 'IconFileImport', @@ -288,7 +286,7 @@ export class WorkspaceMemberWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: WORKSPACE_MEMBER_STANDARD_FIELD_IDS.connectedAccounts, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Connected accounts`, description: msg`Connected accounts`, icon: 'IconAt', @@ -300,7 +298,7 @@ export class WorkspaceMemberWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: WORKSPACE_MEMBER_STANDARD_FIELD_IDS.messageParticipants, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Message Participants`, description: msg`Message Participants`, icon: 'IconUserCircle', @@ -312,7 +310,7 @@ export class WorkspaceMemberWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: WORKSPACE_MEMBER_STANDARD_FIELD_IDS.blocklist, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Blocklist`, description: msg`Blocklisted handles`, icon: 'IconForbid2', @@ -324,7 +322,7 @@ export class WorkspaceMemberWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: WORKSPACE_MEMBER_STANDARD_FIELD_IDS.calendarEventParticipants, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Calendar Event Participants`, description: msg`Calendar Event Participants`, icon: 'IconCalendar', @@ -338,7 +336,7 @@ export class WorkspaceMemberWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: WORKSPACE_MEMBER_STANDARD_FIELD_IDS.timelineActivities, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Events`, description: msg`Events linked to the workspace member`, icon: 'IconTimelineEvent', @@ -351,7 +349,7 @@ export class WorkspaceMemberWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceRelation({ standardId: WORKSPACE_MEMBER_STANDARD_FIELD_IDS.auditLogs, - type: RelationMetadataType.ONE_TO_MANY, + type: RelationType.ONE_TO_MANY, label: msg`Audit Logs`, description: msg`Audit Logs linked to the workspace member`, icon: 'IconTimelineEvent', diff --git a/packages/twenty-ui/src/components/chip/Chip.tsx b/packages/twenty-ui/src/components/chip/Chip.tsx index fc967b691..98103b08a 100644 --- a/packages/twenty-ui/src/components/chip/Chip.tsx +++ b/packages/twenty-ui/src/components/chip/Chip.tsx @@ -151,7 +151,7 @@ export const Chip = ({ maxWidth={maxWidth} > {leftComponent} - {!isLabelHidden && label.trim() ? ( + {!isLabelHidden && label && label.trim() ? ( ) : ( Untitled