diff --git a/packages/twenty-server/src/engine/metadata-modules/metadata-side-effect/handlers/field-metadata/services/__tests__/field-index-view-field-on-create-side-effect-handler.service.spec.ts b/packages/twenty-server/src/engine/metadata-modules/metadata-side-effect/handlers/field-metadata/services/__tests__/field-index-view-field-on-create-side-effect-handler.service.spec.ts index 1f789bf79c..7c6860779a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/metadata-side-effect/handlers/field-metadata/services/__tests__/field-index-view-field-on-create-side-effect-handler.service.spec.ts +++ b/packages/twenty-server/src/engine/metadata-modules/metadata-side-effect/handlers/field-metadata/services/__tests__/field-index-view-field-on-create-side-effect-handler.service.spec.ts @@ -312,22 +312,54 @@ describe('FieldIndexViewFieldOnCreateSideEffectHandlerService', () => { expect(result.status).toBe('success'); }); - it('should noop for a non-displayable field (relation) at object creation', () => { - const relationField = buildPendingFieldMetadata( - 'assignee', - FieldMetadataType.RELATION, - ); + // Parity with the existing-object path: a relation created in the same + // batch as its object gets a visible INDEX view field too, otherwise the + // same manifest yields different views depending on whether the object + // pre-existed (twentyhq/core-team-issues#2749). + it.each([ + ['relation', FieldMetadataType.RELATION], + ['morph relation', FieldMetadataType.MORPH_RELATION], + ])( + 'should emit a visible view field for a %s field created in the same batch as its object', + (_label, fieldMetadataType) => { + const relationField = buildPendingFieldMetadata( + 'assignee', + fieldMetadataType, + ); - const result = handler.buildSideEffects( - buildArgs({ - triggerFieldMetadata: relationField, - pendingFieldMetadatas: [NAME_FIELD, relationField], - objectMetadataCreatedInBatch: true, - }), - ); + const result = handler.buildSideEffects( + buildArgs({ + triggerFieldMetadata: relationField, + pendingFieldMetadatas: [NAME_FIELD, relationField], + objectMetadataCreatedInBatch: true, + }), + ); - expect(result.status).toBe('noop'); - }); + expect(result.status).toBe('success'); + + if (result.status !== 'success') { + throw new Error('expected success'); + } + + const viewFields = Object.values( + result.operations.viewField?.flatEntityToCreate ?? {}, + ); + + expect(viewFields).toHaveLength(1); + expect(viewFields[0].universalIdentifier).toBe( + computeViewFieldUniversalIdentifier({ + viewUniversalIdentifier: DERIVED_INDEX_VIEW_UNIVERSAL_IDENTIFIER, + fieldMetadataUniversalIdentifier: relationField.universalIdentifier, + }), + ); + expect(viewFields[0].viewUniversalIdentifier).toBe( + DERIVED_INDEX_VIEW_UNIVERSAL_IDENTIFIER, + ); + expect(viewFields[0].position).toBe(1); + expect(viewFields[0].isVisible).toBe(true); + expect(viewFields[0].isSystemSideEffect).toBe(true); + }, + ); }); describe('field created on an existing object (historical createOneField behavior)', () => { diff --git a/packages/twenty-server/src/engine/metadata-modules/metadata-side-effect/handlers/field-metadata/services/field-index-view-field-on-create-side-effect-handler.service.ts b/packages/twenty-server/src/engine/metadata-modules/metadata-side-effect/handlers/field-metadata/services/field-index-view-field-on-create-side-effect-handler.service.ts index 99ab348062..68feee7661 100644 --- a/packages/twenty-server/src/engine/metadata-modules/metadata-side-effect/handlers/field-metadata/services/field-index-view-field-on-create-side-effect-handler.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/metadata-side-effect/handlers/field-metadata/services/field-index-view-field-on-create-side-effect-handler.service.ts @@ -17,8 +17,6 @@ import { MetadataSideEffectHandler, } from 'src/engine/metadata-modules/metadata-side-effect/interfaces/base-metadata-side-effect-handler.service'; import { type MetadataSideEffectResult } from 'src/engine/metadata-modules/metadata-side-effect/types/metadata-side-effect-result.type'; -import { computeFlatViewFieldsToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-view-fields-to-create.util'; -import { isFlatFieldMetadataDisplayableInDefaultView } from 'src/engine/metadata-modules/object-metadata/utils/is-flat-field-metadata-displayable-in-default-view.util'; import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type'; import { type UniversalFlatViewField } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field.type'; @@ -29,7 +27,7 @@ export class FieldIndexViewFieldOnCreateSideEffectHandlerService extends Metadat metadataName: 'fieldMetadata', name: 'fieldIndexViewFieldOnCreate', description: - 'When a field is created, provision its visible view field on the parent object INDEX view. Owns the view fields of every caller-provided field; engine-emitted fields get theirs from the handler that emits them.', + 'When a field is created, provision its visible view field on the parent object INDEX view. Owns the view fields of every caller-provided field, relations included; engine-emitted fields get theirs from the handler that emits them.', }, ) { buildSideEffects({ @@ -111,27 +109,16 @@ export class FieldIndexViewFieldOnCreateSideEffectHandlerService extends Metadat }; indexViewUniversalIdentifier: string; allFlatEntityOperationRecordByMetadataName: BuildSideEffectsArgs<'fieldMetadata'>['allFlatEntityOperationRecordByMetadataName']; - }): UniversalFlatViewField | undefined { + }): UniversalFlatViewField { const { labelIdentifierFieldMetadataUniversalIdentifier } = parentFlatObjectMetadata; - if ( - !isFlatFieldMetadataDisplayableInDefaultView({ - flatFieldMetadata: sourceFlatFieldMetadata, - labelIdentifierFieldMetadataUniversalIdentifier, - }) - ) { - return undefined; - } - - const displayableCallerFlatFieldMetadatas = - computeCallerFlatFieldMetadatasForObject({ - objectMetadataUniversalIdentifier: - sourceFlatFieldMetadata.objectMetadataUniversalIdentifier, - labelIdentifierFieldMetadataUniversalIdentifier, - allFlatEntityOperationRecordByMetadataName, - displayableOnly: true, - }); + const callerFlatFieldMetadatas = computeCallerFlatFieldMetadatasForObject({ + objectMetadataUniversalIdentifier: + sourceFlatFieldMetadata.objectMetadataUniversalIdentifier, + labelIdentifierFieldMetadataUniversalIdentifier, + allFlatEntityOperationRecordByMetadataName, + }); const positionByFieldUniversalIdentifier = computeDefaultIndexViewFieldPositionByFieldUniversalIdentifier({ @@ -140,7 +127,7 @@ export class FieldIndexViewFieldOnCreateSideEffectHandlerService extends Metadat objectMetadataUniversalIdentifier: sourceFlatFieldMetadata.objectMetadataUniversalIdentifier, labelIdentifierFieldMetadataUniversalIdentifier, - displayableCallerFlatFieldMetadatas, + callerFlatFieldMetadatas, }); const position = @@ -148,16 +135,11 @@ export class FieldIndexViewFieldOnCreateSideEffectHandlerService extends Metadat sourceFlatFieldMetadata.universalIdentifier, ) ?? 0; - const [flatViewFieldToCreate] = computeFlatViewFieldsToCreate({ - objectFlatFieldMetadatas: [sourceFlatFieldMetadata], - viewUniversalIdentifier: indexViewUniversalIdentifier, - applicationUniversalIdentifier: - sourceFlatFieldMetadata.applicationUniversalIdentifier, - labelIdentifierFieldMetadataUniversalIdentifier, - startPosition: position, + return this.buildIndexFlatViewFieldToCreate({ + sourceFlatFieldMetadata, + indexViewUniversalIdentifier, + position, }); - - return flatViewFieldToCreate; } private buildViewFieldForExistingObject({ @@ -213,17 +195,12 @@ export class FieldIndexViewFieldOnCreateSideEffectHandlerService extends Metadat existingActivePositions.length > 0 ? Math.min(...existingActivePositions) : 1; - const [flatLabelViewFieldToCreate] = computeFlatViewFieldsToCreate({ - objectFlatFieldMetadatas: [sourceFlatFieldMetadata], - viewUniversalIdentifier: indexViewUniversalIdentifier, - applicationUniversalIdentifier: - sourceFlatFieldMetadata.applicationUniversalIdentifier, - labelIdentifierFieldMetadataUniversalIdentifier: - parentFlatObjectMetadata.labelIdentifierFieldMetadataUniversalIdentifier, - startPosition: lowestExistingPosition - 1, - }); - return flatLabelViewFieldToCreate; + return this.buildIndexFlatViewFieldToCreate({ + sourceFlatFieldMetadata, + indexViewUniversalIdentifier, + position: lowestExistingPosition - 1, + }); } const appendBasePosition = @@ -238,7 +215,6 @@ export class FieldIndexViewFieldOnCreateSideEffectHandlerService extends Metadat labelIdentifierFieldMetadataUniversalIdentifier: parentFlatObjectMetadata.labelIdentifierFieldMetadataUniversalIdentifier, allFlatEntityOperationRecordByMetadataName, - displayableOnly: false, }); const indexAmongCallerFlatFieldMetadatas = Math.max( @@ -250,6 +226,22 @@ export class FieldIndexViewFieldOnCreateSideEffectHandlerService extends Metadat 0, ); + return this.buildIndexFlatViewFieldToCreate({ + sourceFlatFieldMetadata, + indexViewUniversalIdentifier, + position: appendBasePosition + indexAmongCallerFlatFieldMetadatas, + }); + } + + private buildIndexFlatViewFieldToCreate({ + sourceFlatFieldMetadata, + indexViewUniversalIdentifier, + position, + }: { + sourceFlatFieldMetadata: UniversalFlatFieldMetadata; + indexViewUniversalIdentifier: string; + position: number; + }): UniversalFlatViewField { const createdAt = new Date().toISOString(); const { applicationUniversalIdentifier } = sourceFlatFieldMetadata; @@ -268,7 +260,7 @@ export class FieldIndexViewFieldOnCreateSideEffectHandlerService extends Metadat viewFieldGroupUniversalIdentifier: null, isVisible: true, size: DEFAULT_VIEW_FIELD_SIZE, - position: appendBasePosition + indexAmongCallerFlatFieldMetadatas, + position, aggregateOperation: null, isActive: true, isSystemSideEffect: true, diff --git a/packages/twenty-server/src/engine/metadata-modules/metadata-side-effect/handlers/object-metadata/services/object-system-fields-and-index-view-on-create-side-effect-handler.service.ts b/packages/twenty-server/src/engine/metadata-modules/metadata-side-effect/handlers/object-metadata/services/object-system-fields-and-index-view-on-create-side-effect-handler.service.ts index e73be3d0b3..89afa6873a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/metadata-side-effect/handlers/object-metadata/services/object-system-fields-and-index-view-on-create-side-effect-handler.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/metadata-side-effect/handlers/object-metadata/services/object-system-fields-and-index-view-on-create-side-effect-handler.service.ts @@ -58,14 +58,12 @@ export class ObjectSystemFieldsAndIndexViewOnCreateSideEffectHandlerService exte applicationUniversalIdentifier, }); - const displayableCallerFlatFieldMetadatas = - computeCallerFlatFieldMetadatasForObject({ - objectMetadataUniversalIdentifier: universalIdentifier, - labelIdentifierFieldMetadataUniversalIdentifier: - sourceFlatObjectMetadata.labelIdentifierFieldMetadataUniversalIdentifier, - allFlatEntityOperationRecordByMetadataName, - displayableOnly: true, - }); + const callerFlatFieldMetadatas = computeCallerFlatFieldMetadatasForObject({ + objectMetadataUniversalIdentifier: universalIdentifier, + labelIdentifierFieldMetadataUniversalIdentifier: + sourceFlatObjectMetadata.labelIdentifierFieldMetadataUniversalIdentifier, + allFlatEntityOperationRecordByMetadataName, + }); const positionByFieldUniversalIdentifier = computeDefaultIndexViewFieldPositionByFieldUniversalIdentifier({ @@ -73,7 +71,7 @@ export class ObjectSystemFieldsAndIndexViewOnCreateSideEffectHandlerService exte objectMetadataUniversalIdentifier: universalIdentifier, labelIdentifierFieldMetadataUniversalIdentifier: sourceFlatObjectMetadata.labelIdentifierFieldMetadataUniversalIdentifier, - displayableCallerFlatFieldMetadatas, + callerFlatFieldMetadatas, }); const flatViewFieldsToCreate = computeFlatViewFieldsToCreate({ diff --git a/packages/twenty-server/src/engine/metadata-modules/metadata-side-effect/handlers/utils/compute-caller-flat-field-metadatas-for-object.util.ts b/packages/twenty-server/src/engine/metadata-modules/metadata-side-effect/handlers/utils/compute-caller-flat-field-metadatas-for-object.util.ts index 69f5e1a856..f1be6f0a79 100644 --- a/packages/twenty-server/src/engine/metadata-modules/metadata-side-effect/handlers/utils/compute-caller-flat-field-metadatas-for-object.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/metadata-side-effect/handlers/utils/compute-caller-flat-field-metadatas-for-object.util.ts @@ -1,5 +1,4 @@ import { type AllFlatEntityOperationRecordByMetadataName } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-operation-record-by-metadata-name.type'; -import { isFlatFieldMetadataDisplayableInDefaultView } from 'src/engine/metadata-modules/object-metadata/utils/is-flat-field-metadata-displayable-in-default-view.util'; import { orderFlatFieldMetadatasForSystemIndexView } from 'src/engine/metadata-modules/object-metadata/utils/order-flat-field-metadatas-for-system-index-view.util'; import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type'; @@ -7,12 +6,10 @@ export const computeCallerFlatFieldMetadatasForObject = ({ objectMetadataUniversalIdentifier, labelIdentifierFieldMetadataUniversalIdentifier, allFlatEntityOperationRecordByMetadataName, - displayableOnly, }: { objectMetadataUniversalIdentifier: string; labelIdentifierFieldMetadataUniversalIdentifier: string | null; allFlatEntityOperationRecordByMetadataName: AllFlatEntityOperationRecordByMetadataName; - displayableOnly: boolean; }): UniversalFlatFieldMetadata[] => orderFlatFieldMetadatasForSystemIndexView({ labelIdentifierFieldMetadataUniversalIdentifier, @@ -25,11 +22,6 @@ export const computeCallerFlatFieldMetadatasForObject = ({ (flatFieldMetadata) => flatFieldMetadata.objectMetadataUniversalIdentifier === objectMetadataUniversalIdentifier && - !flatFieldMetadata.isSystemSideEffect && - (!displayableOnly || - isFlatFieldMetadataDisplayableInDefaultView({ - flatFieldMetadata, - labelIdentifierFieldMetadataUniversalIdentifier, - })), + !flatFieldMetadata.isSystemSideEffect, ), }); diff --git a/packages/twenty-server/src/engine/metadata-modules/metadata-side-effect/handlers/utils/compute-default-index-view-field-position-by-field-universal-identifier.util.ts b/packages/twenty-server/src/engine/metadata-modules/metadata-side-effect/handlers/utils/compute-default-index-view-field-position-by-field-universal-identifier.util.ts index f0fa9ccfbd..65619c4c7c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/metadata-side-effect/handlers/utils/compute-default-index-view-field-position-by-field-universal-identifier.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/metadata-side-effect/handlers/utils/compute-default-index-view-field-position-by-field-universal-identifier.util.ts @@ -7,12 +7,12 @@ export const computeDefaultIndexViewFieldPositionByFieldUniversalIdentifier = ({ applicationUniversalIdentifier, objectMetadataUniversalIdentifier, labelIdentifierFieldMetadataUniversalIdentifier, - displayableCallerFlatFieldMetadatas, + callerFlatFieldMetadatas, }: { applicationUniversalIdentifier: string; objectMetadataUniversalIdentifier: string; labelIdentifierFieldMetadataUniversalIdentifier: string | null; - displayableCallerFlatFieldMetadatas: UniversalFlatFieldMetadata[]; + callerFlatFieldMetadatas: UniversalFlatFieldMetadata[]; }): Map => { const displayableSystemFlatFieldMetadatas = Object.values( buildReservedSystemFlatFieldMetadatasForCustomObject({ @@ -28,17 +28,16 @@ export const computeDefaultIndexViewFieldPositionByFieldUniversalIdentifier = ({ }), ); - const orderedDisplayableFlatFieldMetadatas = - orderFlatFieldMetadatasForSystemIndexView({ - labelIdentifierFieldMetadataUniversalIdentifier, - flatFieldMetadatas: [ - ...displayableCallerFlatFieldMetadatas, - ...displayableSystemFlatFieldMetadatas, - ], - }); + const orderedFlatFieldMetadatas = orderFlatFieldMetadatasForSystemIndexView({ + labelIdentifierFieldMetadataUniversalIdentifier, + flatFieldMetadatas: [ + ...callerFlatFieldMetadatas, + ...displayableSystemFlatFieldMetadatas, + ], + }); return new Map( - orderedDisplayableFlatFieldMetadatas.map((flatFieldMetadata, position) => [ + orderedFlatFieldMetadatas.map((flatFieldMetadata, position) => [ flatFieldMetadata.universalIdentifier, position, ]), diff --git a/packages/twenty-server/test/integration/metadata/suites/application/same-batch-relation-index-view-field-manifest-sync.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/application/same-batch-relation-index-view-field-manifest-sync.integration-spec.ts new file mode 100644 index 0000000000..dd59ae630f --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/application/same-batch-relation-index-view-field-manifest-sync.integration-spec.ts @@ -0,0 +1,249 @@ +import { VIEW_FIELD_GQL_FIELDS } from 'test/integration/constants/view-gql-fields.constants'; +import { buildBaseManifest } from 'test/integration/metadata/suites/application/utils/build-base-manifest.util'; +import { buildDefaultObjectManifest } from 'test/integration/metadata/suites/application/utils/build-default-object-manifest.util'; +import { cleanupApplicationAndAppRegistration } from 'test/integration/metadata/suites/application/utils/cleanup-application-and-app-registration.util'; +import { setupApplicationForSync } from 'test/integration/metadata/suites/application/utils/setup-application-for-sync.util'; +import { syncApplication } from 'test/integration/metadata/suites/application/utils/sync-application.util'; +import { findManyObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata.util'; +import { findViewFields } from 'test/integration/metadata/suites/view-field/utils/find-view-fields.util'; +import { findViews } from 'test/integration/metadata/suites/view/utils/find-views.util'; +import { type ObjectManifest } from 'twenty-shared/application'; +import { + FieldMetadataType, + RelationOnDeleteAction, + RelationType, + ViewKey, +} from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; +import { v4 as uuidv4 } from 'uuid'; + +const TEST_APP_ID = uuidv4(); +const TEST_ROLE_ID = uuidv4(); +const TICKET_OBJECT_ID = uuidv4(); +const PROJECT_OBJECT_ID = uuidv4(); +const TICKET_NAME_FIELD_ID = uuidv4(); +const PROJECT_NAME_FIELD_ID = uuidv4(); +const TICKET_PROJECT_RELATION_FIELD_ID = uuidv4(); +const PROJECT_TICKETS_RELATION_FIELD_ID = uuidv4(); + +const TICKET_NAME_FIELD: ObjectManifest['fields'][number] = { + universalIdentifier: TICKET_NAME_FIELD_ID, + type: FieldMetadataType.TEXT, + name: 'name', + label: 'Name', +}; + +const PROJECT_NAME_FIELD: ObjectManifest['fields'][number] = { + universalIdentifier: PROJECT_NAME_FIELD_ID, + type: FieldMetadataType.TEXT, + name: 'name', + label: 'Name', +}; + +const TICKET_PROJECT_RELATION_FIELD: ObjectManifest['fields'][number] = { + universalIdentifier: TICKET_PROJECT_RELATION_FIELD_ID, + type: FieldMetadataType.RELATION, + name: 'project', + label: 'Project', + relationTargetFieldMetadataUniversalIdentifier: + PROJECT_TICKETS_RELATION_FIELD_ID, + relationTargetObjectMetadataUniversalIdentifier: PROJECT_OBJECT_ID, + universalSettings: { + relationType: RelationType.MANY_TO_ONE, + joinColumnName: 'projectId', + onDelete: RelationOnDeleteAction.SET_NULL, + }, +}; + +const PROJECT_TICKETS_RELATION_FIELD: ObjectManifest['fields'][number] = { + universalIdentifier: PROJECT_TICKETS_RELATION_FIELD_ID, + type: FieldMetadataType.RELATION, + name: 'tickets', + label: 'Tickets', + relationTargetFieldMetadataUniversalIdentifier: + TICKET_PROJECT_RELATION_FIELD_ID, + relationTargetObjectMetadataUniversalIdentifier: TICKET_OBJECT_ID, + universalSettings: { + relationType: RelationType.ONE_TO_MANY, + }, +}; + +const buildManifest = ({ + includeRelationFields, +}: { + includeRelationFields: boolean; +}) => + buildBaseManifest({ + appId: TEST_APP_ID, + roleId: TEST_ROLE_ID, + overrides: { + objects: [ + buildDefaultObjectManifest({ + applicationUniversalIdentifier: TEST_APP_ID, + universalIdentifier: TICKET_OBJECT_ID, + nameSingular: 'ticket', + namePlural: 'tickets', + labelSingular: 'Ticket', + labelPlural: 'Tickets', + description: 'A support ticket', + icon: 'IconTicket', + labelIdentifierFieldMetadataUniversalIdentifier: + TICKET_NAME_FIELD_ID, + additionalFields: [ + TICKET_NAME_FIELD, + ...(includeRelationFields ? [TICKET_PROJECT_RELATION_FIELD] : []), + ], + }), + buildDefaultObjectManifest({ + applicationUniversalIdentifier: TEST_APP_ID, + universalIdentifier: PROJECT_OBJECT_ID, + nameSingular: 'project', + namePlural: 'projects', + labelSingular: 'Project', + labelPlural: 'Projects', + description: 'A project', + icon: 'IconBriefcase', + labelIdentifierFieldMetadataUniversalIdentifier: + PROJECT_NAME_FIELD_ID, + additionalFields: [ + PROJECT_NAME_FIELD, + ...(includeRelationFields ? [PROJECT_TICKETS_RELATION_FIELD] : []), + ], + }), + ], + }, + }); + +const findObjectWithFields = async (objectUniversalIdentifier: string) => { + const { objects } = await findManyObjectMetadata({ + input: { + filter: {}, + paging: { first: 100 }, + }, + gqlFields: ` + id + universalIdentifier + fieldsList { + id + name + universalIdentifier + } + `, + expectToFail: false, + }); + + const object = objects.find( + (objectMetadata) => + objectMetadata.universalIdentifier === objectUniversalIdentifier, + ); + + if (!isDefined(object)) { + throw new Error( + `expected object ${objectUniversalIdentifier} to exist after sync`, + ); + } + + return object; +}; + +const findIndexViewFields = async (objectMetadataId: string) => { + const { data: viewsData } = await findViews({ + objectMetadataId, + gqlFields: 'id key', + expectToFail: false, + }); + + const indexView = viewsData?.getViews.find( + (view) => view.key === ViewKey.INDEX, + ); + + if (!isDefined(indexView)) { + throw new Error('expected an INDEX view for the object'); + } + + const { data: viewFieldsData } = await findViewFields({ + viewId: indexView.id, + gqlFields: VIEW_FIELD_GQL_FIELDS, + expectToFail: false, + }); + + return viewFieldsData?.getViewFields ?? []; +}; + +const expectRelationIndexViewFields = async () => { + const ticketObject = await findObjectWithFields(TICKET_OBJECT_ID); + const projectObject = await findObjectWithFields(PROJECT_OBJECT_ID); + + const ticketProjectField = ticketObject.fieldsList?.find( + (field) => + field.universalIdentifier === TICKET_PROJECT_RELATION_FIELD_ID, + ); + const projectTicketsField = projectObject.fieldsList?.find( + (field) => + field.universalIdentifier === PROJECT_TICKETS_RELATION_FIELD_ID, + ); + + expect(ticketProjectField).toBeDefined(); + expect(projectTicketsField).toBeDefined(); + + const ticketIndexViewFields = await findIndexViewFields(ticketObject.id); + const projectIndexViewFields = await findIndexViewFields(projectObject.id); + + const ticketProjectViewField = ticketIndexViewFields.find( + (viewField) => viewField.fieldMetadataId === ticketProjectField?.id, + ); + const projectTicketsViewField = projectIndexViewFields.find( + (viewField) => viewField.fieldMetadataId === projectTicketsField?.id, + ); + + expect(ticketProjectViewField).toBeDefined(); + expect(projectTicketsViewField).toBeDefined(); + + // Same visibility as a relation added to a pre-existing object. + expect(ticketProjectViewField?.isVisible).toBe(true); + expect(projectTicketsViewField?.isVisible).toBe(true); +}; + +describe('Manifest sync - INDEX view fields for relation fields created in the same batch as their objects', () => { + beforeEach(async () => { + await setupApplicationForSync({ + applicationUniversalIdentifier: TEST_APP_ID, + name: 'Test Application', + description: + 'App for testing INDEX view fields of same-batch relation fields', + sourcePath: 'test-same-batch-relation-index-view-field', + }); + }, 60000); + + afterEach(async () => { + await cleanupApplicationAndAppRegistration({ + applicationUniversalIdentifier: TEST_APP_ID, + }); + }); + + // Reproduces twentyhq/core-team-issues#2749: relations created in the same + // sync as their objects get no INDEX view field at all, not even a hidden + // one, while the same relations added in a later sync do (next test). + it('should provision INDEX view fields for relation fields created in the same sync as their objects', async () => { + await syncApplication({ + manifest: buildManifest({ includeRelationFields: true }), + expectToFail: false, + }); + + await expectRelationIndexViewFields(); + }, 60000); + + it('should provision INDEX view fields for relation fields added to pre-existing objects in a second sync', async () => { + await syncApplication({ + manifest: buildManifest({ includeRelationFields: false }), + expectToFail: false, + }); + + await syncApplication({ + manifest: buildManifest({ includeRelationFields: true }), + expectToFail: false, + }); + + await expectRelationIndexViewFields(); + }, 60000); +});