From f080b952eb58eaae54852668751e628f776f8aa3 Mon Sep 17 00:00:00 2001 From: martmull Date: Wed, 25 Feb 2026 10:37:50 +0100 Subject: [PATCH] Add manifest integration tests (#18203) - add integration test on sync manifest endpoint - fix invalid standard uuid + migration command --- ...-standard-universal-identifiers.command.ts | 561 ++++++++++++++++++ .../1-19-upgrade-version-command.module.ts | 3 + .../upgrade.command.ts | 3 + .../graphql-types/scalars/uuid.scalar.ts | 2 +- ...-to-universal-flat-object-metadata.util.ts | 2 +- .../field-metadata/dtos/create-field.input.ts | 1 + .../field-metadata/dtos/field-metadata.dto.ts | 5 + ...eld-metadata-to-field-metadata-dto.util.ts | 2 + ...data-names-synced-with-labels.util.spec.ts | 143 +++++ ...-metadata-names-synced-with-labels.util.ts | 12 +- ...ct-metadata-to-object-metadata-dto.util.ts | 2 + ...at-object-metadata-name-and-labels.util.ts | 2 +- .../dtos/object-metadata.dto.ts | 3 + .../utils/fromRoleEntityToRoleDto.util.ts | 1 + .../constants/standard-agent.constant.ts | 2 +- .../constants/standard-role.constant.ts | 2 +- .../failed-flat-entity-validation.type.ts | 6 +- ...ect-system-fields.integration-spec.ts.snap | 153 ++--- ...rkspace-migration.integration-spec.ts.snap | 99 +--- ...n-object-system-fields.integration-spec.ts | 67 +-- ...-manifest-update-field.integration-spec.ts | 243 ++++++++ ...manifest-update-object.integration-spec.ts | 218 +++++++ ...l-manifest-update-role.integration-spec.ts | 187 ++++++ ...-manifest-update-skill.integration-spec.ts | 202 +++++++ ...on-workspace-migration.integration-spec.ts | 278 ++++----- .../utils/build-base-manifest.util.ts | 40 ++ ...-many-object-metadata-with-indexes.util.ts | 5 + .../utils/find-roles-query-factory.util.ts | 1 + .../computeMetadataNameFromLabel.test.ts | 6 + .../constants/standard-object.constant.ts | 138 ++--- 30 files changed, 1916 insertions(+), 473 deletions(-) create mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-19/1-19-fix-invalid-standard-universal-identifiers.command.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/__tests__/are-flat-object-metadata-names-synced-with-labels.util.spec.ts create mode 100644 packages/twenty-server/test/integration/metadata/suites/application/successful-manifest-update-field.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/metadata/suites/application/successful-manifest-update-object.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/metadata/suites/application/successful-manifest-update-role.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/metadata/suites/application/successful-manifest-update-skill.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/metadata/suites/application/utils/build-base-manifest.util.ts diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-19/1-19-fix-invalid-standard-universal-identifiers.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-19/1-19-fix-invalid-standard-universal-identifiers.command.ts new file mode 100644 index 00000000000..572ab6969d2 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-19/1-19-fix-invalid-standard-universal-identifiers.command.ts @@ -0,0 +1,561 @@ +import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; + +import { Command } from 'nest-commander'; +import { DataSource, Repository } from 'typeorm'; + +import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; +import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; +import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; +import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; +import { STANDARD_AGENT } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-agent.constant'; +import { STANDARD_ROLE } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-role.constant'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; + +const OLD_ROLE_ADMIN_UNIVERSAL_IDENTIFIER = + '20202020-0001-0001-0001-000000000001'; +const OLD_AGENT_HELPER_UNIVERSAL_IDENTIFIER = + '20202020-0002-0001-0001-000000000004'; + +const STANDARD_OBJECT_MISMATCHES = [ + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: 'c9e5a0b4-1d36-4f8c-0a7b-3e4f5d6c7a81', + newValue: '9d31ea73-13b6-4e06-84ee-c66c72bf7787', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: 'd0f6b1c5-2e47-4a9d-1b8c-4f5a6e7d8b92', + newValue: '55637a5a-1edc-4351-8d76-d40020bf8944', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: 'e1a7c2d6-3f58-4b0e-2c9d-5a6b7f8e9c03', + newValue: '4137ba06-184d-438f-b484-080f02a97659', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: 'f2b8d3e7-4a69-4c1f-3d0e-6b7c8a9f0d14', + newValue: '8cc162d1-c127-4981-878d-f78622f8f12d', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '03c9e4f8-5b70-4d2a-4e1f-7c8d9b0a1e25', + newValue: 'c10eba2d-ff1a-4eab-9285-50481c12a003', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '14d0f5a9-6c81-4e3b-5f2a-8d9e0c1b2f36', + newValue: 'fadeab4b-79ee-4173-af79-72c51fbad888', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '25e1a6b0-7d92-4f4c-6a3b-9e0f1d2c3a47', + newValue: '4daf320e-74d0-4f24-a45a-af3a09d741cb', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '36f2b7c1-8e03-4a5d-7b4c-0f1a2e3d4b58', + newValue: 'ff6b86c1-3112-4dfa-b734-c4789111a716', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '69c5e0f4-1b36-4d8a-0e7f-3c4d5b6a7e81', + newValue: 'c458ad97-8b95-43de-9003-88eb68576049', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '70d6f1a5-2c47-4e9b-1f8a-4d5e6c7b8f92', + newValue: '30e9b75a-881f-4a85-aaf1-f2d2464be1cf', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '81e7a2b6-3d58-4f0c-2a9b-5e6f7d8c9003', + newValue: '898aa202-428f-4a7a-a3b3-8f0a17a6658e', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '92f8b3c7-4e69-4a1d-3b0c-6f7a8e9d0114', + newValue: 'ec2ebfc9-0c9b-4597-a87d-aa295e2d8bfe', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: 'a3a9c4d8-5f70-4b2e-4c1d-7a8b9f0e1225', + newValue: 'dd300c61-f422-467a-91f4-de4f83c4175b', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: 'b4b0d5e9-6a81-4c3f-5d2e-8b9c0a1f2336', + newValue: 'c3eb62df-2cc1-4cc3-b7aa-e96a4d65c633', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: 'c5c1e6f0-7b92-4d4a-6e3f-9c0d1b2a3447', + newValue: '8e7ca28e-6002-4304-9dcc-0a8da93ca198', + }, + { + table: 'core."fieldMetadata"', + column: '"universalIdentifier"', + oldValue: '20202020-9b0c-5d6e-7f8a-9b0c1d2e3f4a', + newValue: '99c330c0-5b7d-4276-a764-aed84499dfb5', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: 'd6d2f7a1-8c03-4e5b-7f4a-0d1e2c3b4558', + newValue: 'e69f71aa-de0f-4b70-845f-7a8369c47928', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '0905c0d4-1f36-4b8e-0c7d-3a4b5f6e788b', + newValue: '1f360393-a336-435d-966d-8ec2645f875c', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '1016d1e5-2a47-4c9f-1d8e-4b5c6a7f899c', + newValue: 'c0369a13-49bd-48b0-a9f0-6ed0ee8e1b09', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '2127e2f6-3b58-4d0a-2e9f-5c6d7b80900d', + newValue: 'b8e9f696-5be4-48cb-815c-7c0bb8b69d38', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '3238f3a7-4c69-4e1b-3f0a-6d7e8c91011e', + newValue: 'd4f3ef9f-ae24-4cef-9d7a-684a24d4968b', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '4349a4b8-5d70-4f2c-4a1b-7e8f9d02122f', + newValue: 'a257158e-3a89-4715-970c-7fc38ac22370', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '5450b5c9-6e81-4a3d-5b2c-8f90ae132340', + newValue: 'd55711a3-3297-4fef-beab-48d733712a33', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '6561c6d0-7f92-4b4e-6c3d-90a1bf243451', + newValue: 'f30c9a75-a563-45a2-93b8-84f59004de8f', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '7672d7e1-8003-4c5f-7d4e-01b2c0354562', + newValue: 'dca73027-adbe-4078-ae47-8d17850b9f2b', + }, + { + table: 'core."fieldMetadata"', + column: '"universalIdentifier"', + oldValue: '20202020-c3d4-e5f6-a7b8-901234567890', + newValue: '9bfc9da7-ae2d-44fd-9563-ede90c5d6222', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '0905a0b4-1336-4f8c-0a7b-34e5f3687895', + newValue: '9bb24d40-60dd-4beb-8c64-a74e8c67f9ee', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '1a16b1c5-2447-4a9d-1b8c-45f6a47989a6', + newValue: '1b86ece8-7ce3-4df3-8771-fd4b5d45b2f2', + }, + { + table: 'core."fieldMetadata"', + column: '"universalIdentifier"', + oldValue: '20202020-d4e5-f6a7-b8c9-012345678901', + newValue: '7411cfa3-4fd9-4b90-a636-940015fd7243', + }, + { + table: 'core."fieldMetadata"', + column: '"universalIdentifier"', + oldValue: '20202020-e5f6-a7b8-c9d0-123456789012', + newValue: 'b3369d31-3856-4a7a-b007-ee353918127c', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '2b38d3e7-5779-4c1f-4e0f-78c9d70d1de9', + newValue: '8e6038aa-1f79-4a84-87b5-f33caa172e98', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '3c49e4f8-6880-4d2a-5f1a-89d0e81e2ef0', + newValue: '905299c3-ca81-435d-901c-f68b87562516', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '4d50f5a9-7991-4e3b-6a2b-90e1f92f3f01', + newValue: 'a3de1788-5dff-4849-ac5a-0dabe5fab216', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '2b27c2d6-3558-4b0e-2c9d-56a7b58a9ab7', + newValue: 'ab09a386-4dcc-41f7-8dc6-a6071e9c64b7', + }, + { + table: 'core."fieldMetadata"', + column: '"universalIdentifier"', + oldValue: '20202020-f6a7-b8c9-d0e1-234567890123', + newValue: 'cce5ce1e-31d0-42e6-83cd-90059244a484', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '3c38d3e7-4669-4c1f-3d0e-67b8c69b0bc8', + newValue: '6217f2a5-28ac-4b88-8a2a-45eee4580e57', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '4d49e4f8-5770-4d2a-4e1f-78c9d70c1cd9', + newValue: 'ab0863ba-f95e-493c-b86c-56e1bc7e5bc2', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '5e50f5a9-6881-4e3b-5f2a-89d0e81d2de0', + newValue: 'df805c2e-3bfe-4d51-8309-75e5eb4052fe', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '6f61a6b0-7992-4f4c-6a3b-90e1f92e3ef1', + newValue: 'ce1e3a9e-afe9-439d-abb7-6cc98a6fa405', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '7072b7c1-8003-4a5d-7b4c-01f2a03f4f02', + newValue: '7a05b45e-7aa6-4a7e-9bbc-299cbed53c96', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '0305e0f4-1336-4d8a-0e7f-34c5d36c7c35', + newValue: '7c069dc0-e83b-4cd5-aaa2-cac7f3e00d80', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '1416f1a5-2447-4e9b-1f8a-45d6e47d8d46', + newValue: '2d83909a-a383-4e82-b00a-8b7739f3f906', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '2527a2b6-3558-4f0c-2a9b-56e7f58e9e57', + newValue: '0d1a59b4-cc87-4b7d-804a-656e8504f371', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '3638b3c7-4669-4a1d-3b0c-67f8a69f0f68', + newValue: 'b8c2a673-a981-4357-a43d-313a358e4daa', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '4749c4d8-5770-4b2e-4c1d-78a9b70a1a79', + newValue: 'e161072d-37b1-477a-b944-ef0d65289574', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '5850d5e9-6881-4c3f-5d2e-89b0c81b2b80', + newValue: 'ae60d580-b562-44f2-a24d-7b8040063f83', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '6961e6f0-7992-4d4a-6e3f-90c1d92c3c91', + newValue: 'f53fdd28-a26b-47ba-81b5-6813ad622720', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '7072f7a1-8003-4e5b-7f4a-01d2e03d4d02', + newValue: '8a265a5c-d3ae-47dc-bdf9-b42cfa2ba639', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '0305c0d4-1336-4b8e-0c7d-34a5b36a7a35', + newValue: 'f48fa3b1-0cec-44da-a9e5-f8a5e766637e', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '1416d1e5-2447-4c9f-1d8e-45b6c47b8b46', + newValue: 'a86b32b3-01d3-4302-a152-8b7f247db7b4', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '2527e2f6-3558-4d0a-2e9f-56c7d58c9c57', + newValue: 'c882f7a4-b025-4d32-aa26-5ef2595bdbf9', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '3638f3a7-4669-4e1b-3f0a-67d8e69d0d68', + newValue: 'b7d305d1-6fae-4ed6-9bdc-354fe9032c0e', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '4749a4b8-5770-4f2c-4a1b-78e9f70e1e79', + newValue: 'c0af54c7-751b-4bb2-b102-677cc4e47402', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '5850b5c9-6881-4a3d-5b2c-89f0a81f2f80', + newValue: '6942e0ba-90f6-4c33-bf40-7f00b1ec35ab', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '6961c6d0-7992-4b4e-6c3d-90a1b92a3a91', + newValue: '5e0b2391-85ca-4a66-aef4-52d74245bec2', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '7072d7e1-8003-4c5f-7d4e-01b2c03b4b02', + newValue: '3e89a914-7bec-47bd-9cf9-743c6b83d001', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '0305a0b4-1336-4f8c-0a7b-34e5f36e7e35', + newValue: '995db1d8-0d3e-40f7-b0eb-5e6897bc9966', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '1416b1c5-2447-4a9d-1b8c-45f6a47f8f46', + newValue: '609cf622-86ef-48d1-812b-e1cab610a46c', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '2527c2d6-3558-4b0e-2c9d-56a7b58a9a57', + newValue: 'd6059ec2-92b0-4cfc-9fd8-78050f03108f', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '3638d3e7-4669-4c1f-3d0e-67b8c69b0b68', + newValue: 'd94329b3-5dc8-4141-ae28-31afe28f7135', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '4749e4f8-5770-4d2a-4e1f-78c9d70c1c79', + newValue: '1a2bd046-7c23-4e0a-9f8a-c3ca3a16d3b9', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '5850f5a9-6881-4e3b-5f2a-89d0e81d2d80', + newValue: 'e8821da9-728d-470a-bf5b-5a981fff7880', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '6961a6b0-7992-4f4c-6a3b-90e1f92e3e91', + newValue: 'c7e64c55-eb0c-4b93-b076-5cfcf2e2e042', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '7072b7c1-8003-4a5d-7b4c-01f2a03f4f03', + newValue: '7331ff89-a3f9-4ac0-9fa9-0de5663ae7b2', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '0305e0f4-1336-4d8a-0e7f-34c5d36c7c36', + newValue: 'e0ac5ad2-d0c8-4f72-b710-8e53b9dc18d9', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '1416f1a5-2447-4e9b-1f8a-45d6e47d8d47', + newValue: '8138c3b3-0b14-4ee1-be0e-debdde6b3219', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '2527a2b6-3558-4f0c-2a9b-56e7f58e9e58', + newValue: '6f3a65eb-2aee-4108-b8a0-c62da419d1dc', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '3638b3c7-4669-4a1d-3b0c-67f8a69f0f69', + newValue: '76da5f27-523c-44b6-ad06-12954f6b949f', + }, + { + table: 'core."indexMetadata"', + column: '"universalIdentifier"', + oldValue: '4749c4d8-5770-4b2e-4c1d-78a9b70a1a7a', + newValue: '8678dde9-a804-4a9e-80e3-9af35e471ec5', + }, +]; + +@Command({ + name: 'upgrade:1-19:fix-invalid-standard-universal-identifiers', + description: + 'Fix invalid universalIdentifier values in core tables to comply with UUID v4 format', +}) +export class FixInvalidStandardUniversalIdentifiersCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { + constructor( + @InjectRepository(WorkspaceEntity) + protected readonly workspaceRepository: Repository, + @InjectDataSource() + private readonly coreDataSource: DataSource, + protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, + protected readonly dataSourceService: DataSourceService, + ) { + super(workspaceRepository, twentyORMGlobalManager, dataSourceService); + } + + override async runOnWorkspace({ + workspaceId, + options, + }: RunOnWorkspaceArgs): Promise { + const isDryRun = options?.dryRun ?? false; + + this.logger.log( + `${isDryRun ? '[DRY RUN] ' : ''}Fixing universal identifiers for workspace ${workspaceId}`, + ); + + if (isDryRun) { + this.logger.log( + `[DRY RUN] Would fix role, agent, and ${STANDARD_OBJECT_MISMATCHES.length} standard object universal identifier mismatches in workspace ${workspaceId}. Skipping.`, + ); + + return; + } + + const queryRunner = this.coreDataSource.createQueryRunner(); + + await queryRunner.connect(); + + try { + let totalUpdated = 0; + + const roleResult = await queryRunner.query( + `UPDATE core."role" + SET "universalIdentifier" = $1 + WHERE "workspaceId" = $2 + AND "universalIdentifier" = $3`, + [ + STANDARD_ROLE.admin.universalIdentifier, + workspaceId, + OLD_ROLE_ADMIN_UNIVERSAL_IDENTIFIER, + ], + ); + + const roleUpdatedCount = roleResult?.[1] ?? 0; + + if (roleUpdatedCount > 0) { + this.logger.log( + `Updated ${roleUpdatedCount} role universalIdentifier in workspace ${workspaceId}`, + ); + totalUpdated += roleUpdatedCount; + } + + const agentResult = await queryRunner.query( + `UPDATE core."agent" + SET "universalIdentifier" = $1 + WHERE "workspaceId" = $2 + AND "universalIdentifier" = $3`, + [ + STANDARD_AGENT.helper.universalIdentifier, + workspaceId, + OLD_AGENT_HELPER_UNIVERSAL_IDENTIFIER, + ], + ); + + const agentUpdatedCount = agentResult?.[1] ?? 0; + + if (agentUpdatedCount > 0) { + this.logger.log( + `Updated ${agentUpdatedCount} agent universalIdentifier in workspace ${workspaceId}`, + ); + totalUpdated += agentUpdatedCount; + } + + const updateCountsByTable: Record = {}; + + for (const mismatch of STANDARD_OBJECT_MISMATCHES) { + const result = await queryRunner.query( + `UPDATE ${mismatch.table} + SET ${mismatch.column} = $1 + WHERE "workspaceId" = $2 + AND ${mismatch.column} = $3`, + [mismatch.newValue, workspaceId, mismatch.oldValue], + ); + + const updatedCount = result?.[1] ?? 0; + + if (updatedCount > 0) { + const tableKey = `${mismatch.table}.${mismatch.column}`; + + updateCountsByTable[tableKey] = + (updateCountsByTable[tableKey] ?? 0) + updatedCount; + totalUpdated += updatedCount; + } + } + + for (const [tableKey, count] of Object.entries(updateCountsByTable)) { + this.logger.log( + `Updated ${count} rows in ${tableKey} for workspace ${workspaceId}`, + ); + } + + if (totalUpdated === 0) { + this.logger.log( + `No universal identifiers needed updating in workspace ${workspaceId}`, + ); + } else { + this.logger.log( + `Updated ${totalUpdated} total universal identifiers in workspace ${workspaceId}`, + ); + } + } finally { + await queryRunner.release(); + } + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-19/1-19-upgrade-version-command.module.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-19/1-19-upgrade-version-command.module.ts index f95bdff6251..6fe5360a13c 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-19/1-19-upgrade-version-command.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-19/1-19-upgrade-version-command.module.ts @@ -5,6 +5,7 @@ import { AddMissingSystemFieldsToStandardObjectsCommand } from 'src/database/com import { BackfillMessageChannelMessageAssociationMessageFolderCommand } from 'src/database/commands/upgrade-version-command/1-19/1-19-backfill-message-channel-message-association-message-folder.command'; import { BackfillPageLayoutsCommand } from 'src/database/commands/upgrade-version-command/1-19/1-19-backfill-page-layouts.command'; import { BackfillSystemFieldsIsSystemCommand } from 'src/database/commands/upgrade-version-command/1-19/1-19-backfill-system-fields-is-system.command'; +import { FixInvalidStandardUniversalIdentifiersCommand } from 'src/database/commands/upgrade-version-command/1-19/1-19-fix-invalid-standard-universal-identifiers.command'; import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; @@ -32,12 +33,14 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace AddMissingSystemFieldsToStandardObjectsCommand, BackfillMessageChannelMessageAssociationMessageFolderCommand, BackfillPageLayoutsCommand, + FixInvalidStandardUniversalIdentifiersCommand, ], exports: [ BackfillSystemFieldsIsSystemCommand, AddMissingSystemFieldsToStandardObjectsCommand, BackfillMessageChannelMessageAssociationMessageFolderCommand, BackfillPageLayoutsCommand, + FixInvalidStandardUniversalIdentifiersCommand, ], }) export class V1_19_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 0a272543aad..9cfbfff8afc 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 @@ -30,6 +30,7 @@ import { AddMissingSystemFieldsToStandardObjectsCommand } from 'src/database/com import { BackfillMessageChannelMessageAssociationMessageFolderCommand } from 'src/database/commands/upgrade-version-command/1-19/1-19-backfill-message-channel-message-association-message-folder.command'; import { BackfillPageLayoutsCommand } from 'src/database/commands/upgrade-version-command/1-19/1-19-backfill-page-layouts.command'; import { BackfillSystemFieldsIsSystemCommand } from 'src/database/commands/upgrade-version-command/1-19/1-19-backfill-system-fields-is-system.command'; +import { FixInvalidStandardUniversalIdentifiersCommand } from 'src/database/commands/upgrade-version-command/1-19/1-19-fix-invalid-standard-universal-identifiers.command'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; @@ -75,6 +76,7 @@ export class UpgradeCommand extends UpgradeCommandRunner { protected readonly addMissingSystemFieldsToStandardObjectsCommand: AddMissingSystemFieldsToStandardObjectsCommand, protected readonly backfillMessageChannelMessageAssociationMessageFolderCommand: BackfillMessageChannelMessageAssociationMessageFolderCommand, protected readonly backfillPageLayoutsCommand: BackfillPageLayoutsCommand, + protected readonly fixRoleAndAgentUniversalIdentifiersCommand: FixInvalidStandardUniversalIdentifiersCommand, ) { super( workspaceRepository, @@ -115,6 +117,7 @@ export class UpgradeCommand extends UpgradeCommandRunner { this.addMissingSystemFieldsToStandardObjectsCommand, this.backfillMessageChannelMessageAssociationMessageFolderCommand, this.backfillPageLayoutsCommand, + this.fixRoleAndAgentUniversalIdentifiersCommand, ]; this.allCommands = { diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/uuid.scalar.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/uuid.scalar.ts index 6a2513b8971..6eb0c21bb75 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/uuid.scalar.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/uuid.scalar.ts @@ -9,7 +9,7 @@ const checkUUID = (value: any): string => { throw new ValidationError('UUID must be a string'); } if (!uuidValidate(value)) { - throw new ValidationError(`Invalid UUID`, { + throw new ValidationError(`Invalid UUID: '${value}'`, { value, }); } diff --git a/packages/twenty-server/src/engine/core-modules/application/utils/from-object-manifest-to-universal-flat-object-metadata.util.ts b/packages/twenty-server/src/engine/core-modules/application/utils/from-object-manifest-to-universal-flat-object-metadata.util.ts index 8b88c579709..e4ed3bbb525 100644 --- a/packages/twenty-server/src/engine/core-modules/application/utils/from-object-manifest-to-universal-flat-object-metadata.util.ts +++ b/packages/twenty-server/src/engine/core-modules/application/utils/from-object-manifest-to-universal-flat-object-metadata.util.ts @@ -31,7 +31,7 @@ export const fromObjectManifestToUniversalFlatObjectMetadata = ({ isSearchable: false, duplicateCriteria: null, shortcut: null, - isLabelSyncedWithName: true, + isLabelSyncedWithName: false, fieldUniversalIdentifiers: [], indexMetadataUniversalIdentifiers: [], viewUniversalIdentifiers: [], diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/create-field.input.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/create-field.input.ts index b1ef882b0ce..6538a8d6df7 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/create-field.input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/create-field.input.ts @@ -18,6 +18,7 @@ export class CreateFieldInput extends OmitType( 'standardOverrides', 'applicationId', 'morphId', + 'universalIdentifier', ] as const, InputType, ) { diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto.ts index 58bf017e2e7..c75a13851d9 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto.ts @@ -64,6 +64,11 @@ export class FieldMetadataDTO { @IDField(() => UUIDScalarType) id: string; + @IsUUID() + @IsNotEmpty() + @IDField(() => UUIDScalarType) + universalIdentifier: string; + @IsEnum(FieldMetadataType) @IsNotEmpty() @Field(() => FieldMetadataType) diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-flat-field-metadata-to-field-metadata-dto.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-flat-field-metadata-to-field-metadata-dto.util.ts index 69502a68edc..93a47a1219a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-flat-field-metadata-to-field-metadata-dto.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-flat-field-metadata-to-field-metadata-dto.util.ts @@ -14,6 +14,7 @@ export const fromFlatFieldMetadataToFieldMetadataDto = ( isUnique, settings, id, + universalIdentifier, label, name, objectMetadataId, @@ -32,6 +33,7 @@ export const fromFlatFieldMetadataToFieldMetadataDto = ( return { id, + universalIdentifier, label, name, objectMetadataId, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/__tests__/are-flat-object-metadata-names-synced-with-labels.util.spec.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/__tests__/are-flat-object-metadata-names-synced-with-labels.util.spec.ts new file mode 100644 index 00000000000..36ca5821358 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/__tests__/are-flat-object-metadata-names-synced-with-labels.util.spec.ts @@ -0,0 +1,143 @@ +import { areFlatObjectMetadataNamesSyncedWithLabels } from 'src/engine/metadata-modules/flat-object-metadata/utils/are-flat-object-metadata-names-synced-with-labels.util'; +import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications'; +import { type WorkspaceMigrationBuilderOptions } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-builder-options.type'; + +const THIRD_PARTY_BUILD_OPTIONS: WorkspaceMigrationBuilderOptions = { + isSystemBuild: false, + applicationUniversalIdentifier: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', +}; + +const TWENTY_STANDARD_BUILD_OPTIONS: WorkspaceMigrationBuilderOptions = { + isSystemBuild: false, + applicationUniversalIdentifier: + TWENTY_STANDARD_APPLICATION.universalIdentifier, +}; + +describe('areFlatObjectMetadataNamesSyncedWithLabels', () => { + it('should return true when names match computed names from labels', () => { + const result = areFlatObjectMetadataNamesSyncedWithLabels({ + flatObjectMetadata: { + nameSingular: 'ticket', + namePlural: 'tickets', + labelSingular: 'Ticket', + labelPlural: 'Tickets', + }, + buildOptions: THIRD_PARTY_BUILD_OPTIONS, + }); + + expect(result).toBe(true); + }); + + it('should return false when singular name does not match', () => { + const result = areFlatObjectMetadataNamesSyncedWithLabels({ + flatObjectMetadata: { + nameSingular: 'wrongName', + namePlural: 'tickets', + labelSingular: 'Ticket', + labelPlural: 'Tickets', + }, + buildOptions: THIRD_PARTY_BUILD_OPTIONS, + }); + + expect(result).toBe(false); + }); + + it('should return false when plural name does not match', () => { + const result = areFlatObjectMetadataNamesSyncedWithLabels({ + flatObjectMetadata: { + nameSingular: 'ticket', + namePlural: 'wrongPlural', + labelSingular: 'Ticket', + labelPlural: 'Tickets', + }, + buildOptions: THIRD_PARTY_BUILD_OPTIONS, + }); + + expect(result).toBe(false); + }); + + it('should apply custom suffix for reserved words when caller is a third-party app', () => { + // "Event" computes to "event" which is reserved, so suffix "Custom" is appended + const result = areFlatObjectMetadataNamesSyncedWithLabels({ + flatObjectMetadata: { + nameSingular: 'eventCustom', + namePlural: 'eventsCustom', + labelSingular: 'Event', + labelPlural: 'Events', + }, + buildOptions: THIRD_PARTY_BUILD_OPTIONS, + }); + + expect(result).toBe(true); + }); + + it('should return false for reserved words without custom suffix when caller is a third-party app', () => { + const result = areFlatObjectMetadataNamesSyncedWithLabels({ + flatObjectMetadata: { + nameSingular: 'event', + namePlural: 'events', + labelSingular: 'Event', + labelPlural: 'Events', + }, + buildOptions: THIRD_PARTY_BUILD_OPTIONS, + }); + + expect(result).toBe(false); + }); + + it('should not apply custom suffix for reserved words when caller is the Twenty Standard app', () => { + const result = areFlatObjectMetadataNamesSyncedWithLabels({ + flatObjectMetadata: { + nameSingular: 'event', + namePlural: 'events', + labelSingular: 'Event', + labelPlural: 'Events', + }, + buildOptions: TWENTY_STANDARD_BUILD_OPTIONS, + }); + + expect(result).toBe(true); + }); + + it('should handle multi-word labels by computing camelCase names', () => { + const result = areFlatObjectMetadataNamesSyncedWithLabels({ + flatObjectMetadata: { + nameSingular: 'supportTicket', + namePlural: 'supportTickets', + labelSingular: 'Support Ticket', + labelPlural: 'Support Tickets', + }, + buildOptions: THIRD_PARTY_BUILD_OPTIONS, + }); + + expect(result).toBe(true); + }); + + it('should return false when both names do not match', () => { + const result = areFlatObjectMetadataNamesSyncedWithLabels({ + flatObjectMetadata: { + nameSingular: 'foo', + namePlural: 'bar', + labelSingular: 'Ticket', + labelPlural: 'Tickets', + }, + buildOptions: THIRD_PARTY_BUILD_OPTIONS, + }); + + expect(result).toBe(false); + }); + + it('should return true with complex labels', () => { + const result = areFlatObjectMetadataNamesSyncedWithLabels({ + flatObjectMetadata: { + nameSingular: 'wrongCreatedAtObject', + namePlural: 'wrongCreatedAtObjects', + labelSingular: 'Wrong CreatedAt Object', + labelPlural: 'Wrong CreatedAt Objects', + }, + buildOptions: THIRD_PARTY_BUILD_OPTIONS, + }); + + expect(result).toBe(true); + }); +}); diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/are-flat-object-metadata-names-synced-with-labels.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/are-flat-object-metadata-names-synced-with-labels.util.ts index a7a9538f089..e04e669af8c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/are-flat-object-metadata-names-synced-with-labels.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/are-flat-object-metadata-names-synced-with-labels.util.ts @@ -5,18 +5,18 @@ import { type UniversalFlatObjectMetadata } from 'src/engine/workspace-manager/w import { type WorkspaceMigrationBuilderOptions } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-builder-options.type'; export const areFlatObjectMetadataNamesSyncedWithLabels = ({ - flatObjectdMetadata, + flatObjectMetadata, buildOptions, }: { buildOptions: WorkspaceMigrationBuilderOptions; - flatObjectdMetadata: Pick< + flatObjectMetadata: Pick< UniversalFlatObjectMetadata, 'namePlural' | 'nameSingular' | 'labelPlural' | 'labelSingular' >; }) => { const [computedSingularName, computedPluralName] = [ - flatObjectdMetadata.labelSingular, - flatObjectdMetadata.labelPlural, + flatObjectMetadata.labelSingular, + flatObjectMetadata.labelPlural, ].map((label) => computeMetadataNameFromLabel({ label, @@ -25,7 +25,7 @@ export const areFlatObjectMetadataNamesSyncedWithLabels = ({ ); return ( - flatObjectdMetadata.nameSingular === computedSingularName && - flatObjectdMetadata.namePlural === computedPluralName + flatObjectMetadata.nameSingular === computedSingularName && + flatObjectMetadata.namePlural === computedPluralName ); }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-flat-object-metadata-to-object-metadata-dto.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-flat-object-metadata-to-object-metadata-dto.util.ts index f0faa661882..9737ee9e109 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-flat-object-metadata-to-object-metadata-dto.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-flat-object-metadata-to-object-metadata-dto.util.ts @@ -13,6 +13,7 @@ export const fromFlatObjectMetadataToObjectMetadataDto = ( shortcut, duplicateCriteria, id, + universalIdentifier, isActive, isCustom, isLabelSyncedWithName, @@ -32,6 +33,7 @@ export const fromFlatObjectMetadataToObjectMetadataDto = ( return { id, + universalIdentifier, isActive, isCustom, isLabelSyncedWithName, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-name-and-labels.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-name-and-labels.util.ts index a85db844c20..4f5c867dc67 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-name-and-labels.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-name-and-labels.util.ts @@ -66,7 +66,7 @@ export const validateFlatObjectMetadataNameAndLabels = ({ if ( universalFlatObjectMetadataToValidate.isLabelSyncedWithName && !areFlatObjectMetadataNamesSyncedWithLabels({ - flatObjectdMetadata: universalFlatObjectMetadataToValidate, + flatObjectMetadata: universalFlatObjectMetadataToValidate, buildOptions, }) ) { diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto.ts index e35c7c13a04..b210911fb4b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto.ts @@ -32,6 +32,9 @@ export class ObjectMetadataDTO { @IDField(() => UUIDScalarType) id: string; + @IDField(() => UUIDScalarType) + universalIdentifier: string; + @Field() nameSingular: string; diff --git a/packages/twenty-server/src/engine/metadata-modules/role/utils/fromRoleEntityToRoleDto.util.ts b/packages/twenty-server/src/engine/metadata-modules/role/utils/fromRoleEntityToRoleDto.util.ts index 938c8ae5c70..a40b266b488 100644 --- a/packages/twenty-server/src/engine/metadata-modules/role/utils/fromRoleEntityToRoleDto.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/role/utils/fromRoleEntityToRoleDto.util.ts @@ -5,6 +5,7 @@ export const fromRoleEntityToRoleDto = (role: RoleEntity): RoleDTO => { return { id: role.id, label: role.label, + universalIdentifier: role.universalIdentifier, canUpdateAllSettings: role.canUpdateAllSettings, canAccessAllTools: role.canAccessAllTools, description: role.description ?? undefined, diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-agent.constant.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-agent.constant.ts index d784805ad01..93dbc2dc406 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-agent.constant.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-agent.constant.ts @@ -1,6 +1,6 @@ export const STANDARD_AGENT = { helper: { - universalIdentifier: '20202020-0002-0001-0001-000000000004', + universalIdentifier: '20202020-c7ab-4065-b822-0ca1d5de60a9', }, } as const satisfies Record< string, diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-role.constant.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-role.constant.ts index f697cef5dab..1866f69f366 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-role.constant.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-role.constant.ts @@ -1,3 +1,3 @@ export const STANDARD_ROLE = { - admin: { universalIdentifier: '20202020-0001-0001-0001-000000000001' }, + admin: { universalIdentifier: '20202020-02c2-43f2-b94d-cab1f2b532eb' }, } as const satisfies Record; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/types/failed-flat-entity-validation.type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/types/failed-flat-entity-validation.type.ts index e52515d9c78..073d09cc2d8 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/types/failed-flat-entity-validation.type.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/types/failed-flat-entity-validation.type.ts @@ -1,8 +1,8 @@ import { type MessageDescriptor } from '@lingui/core'; import { type AllMetadataName } from 'twenty-shared/metadata'; -import { type MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type'; import { type WorkspaceMigrationActionType } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-action-common'; +import { type MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type'; export type FlatEntityValidationError = { code: TCode; @@ -13,9 +13,9 @@ export type FlatEntityValidationError = { export type FailedFlatEntityValidation< TMetadataName extends AllMetadataName, - TAcionType extends WorkspaceMigrationActionType, + TActionType extends WorkspaceMigrationActionType, > = { - type: TAcionType; + type: TActionType; metadataName: TMetadataName; errors: FlatEntityValidationError[]; flatEntityMinimalInformation: Partial>; diff --git a/packages/twenty-server/test/integration/metadata/suites/application/__snapshots__/failing-sync-application-object-system-fields.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/application/__snapshots__/failing-sync-application-object-system-fields.integration-spec.ts.snap index 27073c88539..15ca8750619 100644 --- a/packages/twenty-server/test/integration/metadata/suites/application/__snapshots__/failing-sync-application-object-system-fields.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/application/__snapshots__/failing-sync-application-object-system-fields.integration-spec.ts.snap @@ -79,80 +79,6 @@ exports[`Sync application should fail due to object system fields integrity shou } `; -exports[`Sync application should fail due to object system fields integrity when object has createdAt field with wrong type (TEXT instead of DATE_TIME) 1`] = ` -{ - "extensions": { - "code": "METADATA_VALIDATION_FAILED", - "errors": { - "fieldMetadata": [ - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "name": "title", - "objectMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - }, - "metadataName": "fieldMetadata", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "name": "createdAt", - "objectMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - }, - "metadataName": "fieldMetadata", - "status": "fail", - "type": "create", - }, - ], - "objectMetadata": [ - { - "errors": [ - { - "code": "INVALID_OBJECT_INPUT", - "message": "Names are not synced with labels", - "userFriendlyMessage": "Names are not synced with labels", - }, - ], - "flatEntityMinimalInformation": { - "namePlural": "wrongCreatedAtObjects", - "nameSingular": "wrongCreatedAtObject", - "universalIdentifier": Any, - }, - "metadataName": "objectMetadata", - "status": "fail", - "type": "create", - }, - ], - }, - "message": "Validation failed for 2 fieldMetadatas, 1 objectMetadata", - "summary": { - "fieldMetadata": 2, - "objectMetadata": 1, - "totalErrors": 3, - }, - "userFriendlyMessage": "Metadata validation failed", - }, - "message": "Validation errors occurred while syncing application manifest metadata", - "name": "GraphQLError", -} -`; - exports[`Sync application should fail due to object system fields integrity when object has id field with wrong type (TEXT instead of UUID) 1`] = ` { "extensions": { @@ -389,3 +315,82 @@ exports[`Sync application should fail due to object system fields integrity when "name": "GraphQLError", } `; + +exports[`Sync application should fail due to object system fields integrity when object miss default fields 1`] = ` +{ + "extensions": { + "code": "METADATA_VALIDATION_FAILED", + "errors": { + "objectMetadata": [ + { + "errors": [ + { + "code": "MISSING_SYSTEM_FIELD", + "message": "System field id is missing", + "userFriendlyMessage": "System field id is missing", + "value": "id", + }, + { + "code": "MISSING_SYSTEM_FIELD", + "message": "System field createdAt is missing", + "userFriendlyMessage": "System field createdAt is missing", + "value": "createdAt", + }, + { + "code": "MISSING_SYSTEM_FIELD", + "message": "System field updatedAt is missing", + "userFriendlyMessage": "System field updatedAt is missing", + "value": "updatedAt", + }, + { + "code": "MISSING_SYSTEM_FIELD", + "message": "System field deletedAt is missing", + "userFriendlyMessage": "System field deletedAt is missing", + "value": "deletedAt", + }, + { + "code": "MISSING_SYSTEM_FIELD", + "message": "System field createdBy is missing", + "userFriendlyMessage": "System field createdBy is missing", + "value": "createdBy", + }, + { + "code": "MISSING_SYSTEM_FIELD", + "message": "System field updatedBy is missing", + "userFriendlyMessage": "System field updatedBy is missing", + "value": "updatedBy", + }, + { + "code": "MISSING_SYSTEM_FIELD", + "message": "System field position is missing", + "userFriendlyMessage": "System field position is missing", + "value": "position", + }, + { + "code": "MISSING_SYSTEM_FIELD", + "message": "System field searchVector is missing", + "userFriendlyMessage": "System field searchVector is missing", + "value": "searchVector", + }, + ], + "flatEntityMinimalInformation": { + "namePlural": "wrongCreatedAtObjects", + "nameSingular": "wrongCreatedAtObject", + "universalIdentifier": Any, + }, + "metadataName": "objectMetadata", + "type": "create", + }, + ], + }, + "message": "Validation failed for 1 objectMetadata", + "summary": { + "objectMetadata": 1, + "totalErrors": 1, + }, + "userFriendlyMessage": "Metadata validation failed", + }, + "message": "Validation errors occurred while syncing application manifest metadata", + "name": "GraphQLError", +} +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/application/__snapshots__/successful-sync-application-workspace-migration.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/application/__snapshots__/successful-sync-application-workspace-migration.integration-spec.ts.snap index 2d84059b44b..dff10e94de7 100644 --- a/packages/twenty-server/test/integration/metadata/suites/application/__snapshots__/successful-sync-application-workspace-migration.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/application/__snapshots__/successful-sync-application-workspace-migration.integration-spec.ts.snap @@ -78,7 +78,7 @@ exports[`syncApplication should return workspace migration actions on initial sy "isActive": true, "isAuditLogged": true, "isCustom": true, - "isLabelSyncedWithName": true, + "isLabelSyncedWithName": false, "isRemote": false, "isSearchable": false, "isSystem": false, @@ -364,81 +364,6 @@ exports[`syncApplication should return workspace migration actions on initial sy "metadataName": "role", "type": "create", }, - ], - "applicationUniversalIdentifier": Any, - }, -} -`; - -exports[`syncApplication should return workspace migration actions on initial sync then on second sync with field rename and new role 2`] = ` -{ - "syncApplication": { - "actions": [ - { - "metadataName": "fieldMetadata", - "type": "update", - "universalIdentifier": Any, - "update": { - "label": "Body", - "name": "body", - }, - }, - { - "flatEntity": { - "applicationUniversalIdentifier": Any, - "canAccessAllTools": false, - "canBeAssignedToAgents": true, - "canBeAssignedToApiKeys": true, - "canBeAssignedToUsers": true, - "canDestroyAllObjectRecords": false, - "canReadAllObjectRecords": false, - "canSoftDeleteAllObjectRecords": false, - "canUpdateAllObjectRecords": false, - "canUpdateAllSettings": false, - "createdAt": Any, - "description": "A read-only role", - "icon": null, - "isEditable": true, - "label": "Viewer Role", - "universalIdentifier": Any, - "updatedAt": Any, - }, - "metadataName": "role", - "type": "create", - }, - ], - "applicationUniversalIdentifier": Any, - }, -} -`; - -exports[`syncApplication should sync a skill then update it on second sync 1`] = ` -{ - "syncApplication": { - "actions": [ - { - "flatEntity": { - "applicationUniversalIdentifier": Any, - "canAccessAllTools": false, - "canBeAssignedToAgents": true, - "canBeAssignedToApiKeys": true, - "canBeAssignedToUsers": true, - "canDestroyAllObjectRecords": false, - "canReadAllObjectRecords": false, - "canSoftDeleteAllObjectRecords": false, - "canUpdateAllObjectRecords": false, - "canUpdateAllSettings": false, - "createdAt": Any, - "description": "A test role", - "icon": null, - "isEditable": true, - "label": "Test Role", - "universalIdentifier": Any, - "updatedAt": Any, - }, - "metadataName": "role", - "type": "create", - }, { "flatEntity": { "applicationUniversalIdentifier": Any, @@ -463,25 +388,3 @@ This is a test skill.", }, } `; - -exports[`syncApplication should sync a skill then update it on second sync 2`] = ` -{ - "syncApplication": { - "actions": [ - { - "metadataName": "skill", - "type": "update", - "universalIdentifier": Any, - "update": { - "content": "# Test Skill - -This is an updated test skill with more content.", - "description": "An updated skill for testing", - "label": "Test Skill Updated", - }, - }, - ], - "applicationUniversalIdentifier": Any, - }, -} -`; diff --git a/packages/twenty-server/test/integration/metadata/suites/application/failing-sync-application-object-system-fields.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/application/failing-sync-application-object-system-fields.integration-spec.ts index 979a4a95c19..2ddf0722b15 100644 --- a/packages/twenty-server/test/integration/metadata/suites/application/failing-sync-application-object-system-fields.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/application/failing-sync-application-object-system-fields.integration-spec.ts @@ -1,4 +1,5 @@ import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util'; +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 { 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'; @@ -20,37 +21,12 @@ type TestContext = { type SyncApplicationTestingContext = EachTestingContext[]; -const buildBaseManifest = ( - overrides: Pick, -): Manifest => ({ - application: { - apiClientChecksum: '', - marketplaceData: undefined, - universalIdentifier: TEST_APP_ID, - defaultRoleUniversalIdentifier: TEST_ROLE_ID, - displayName: 'Test System Fields App', - description: 'App for testing system field validation', - icon: 'IconTestPipe', - applicationVariables: {}, - packageJsonChecksum: null, - yarnLockChecksum: null, - }, - roles: [ - { - universalIdentifier: TEST_ROLE_ID, - label: 'Test Role', - description: 'A test role', - }, - ], - logicFunctions: [], - frontComponents: [], - publicAssets: [], - views: [], - navigationMenuItems: [], - pageLayouts: [], - skills: [], - ...overrides, -}); +const buildManifest = (overrides: Pick) => + buildBaseManifest({ + appId: TEST_APP_ID, + roleId: TEST_ROLE_ID, + overrides, + }); const buildObjectWithLabelField = ({ nameSingular, @@ -104,7 +80,7 @@ const failingSyncApplicationSystemFieldsTestCases: SyncApplicationTestingContext title: 'when object is created without any system fields (missing all 8 system fields)', context: { - manifest: buildBaseManifest( + manifest: buildManifest( buildObjectWithLabelField({ nameSingular: 'noSystemFieldsObject', namePlural: 'noSystemFieldsObjects', @@ -118,7 +94,7 @@ const failingSyncApplicationSystemFieldsTestCases: SyncApplicationTestingContext { title: 'when object has id field with wrong type (TEXT instead of UUID)', context: { - manifest: buildBaseManifest( + manifest: buildManifest( buildObjectWithLabelField({ nameSingular: 'wrongIdTypeObject', namePlural: 'wrongIdTypeObjects', @@ -140,26 +116,15 @@ const failingSyncApplicationSystemFieldsTestCases: SyncApplicationTestingContext }, }, { - title: - 'when object has createdAt field with wrong type (TEXT instead of DATE_TIME)', + title: 'when object miss default fields', context: { - manifest: buildBaseManifest( + manifest: buildManifest( buildObjectWithLabelField({ nameSingular: 'wrongCreatedAtObject', namePlural: 'wrongCreatedAtObjects', labelSingular: 'Wrong CreatedAt Object', labelPlural: 'Wrong CreatedAt Objects', description: 'Object with wrong createdAt field type', - additionalFields: [ - { - universalIdentifier: uuidv4(), - type: FieldMetadataType.TEXT, - name: 'createdAt', - label: 'Created At', - description: 'Created at field with wrong type', - icon: 'IconCalendar', - }, - ], }), ), }, @@ -168,7 +133,7 @@ const failingSyncApplicationSystemFieldsTestCases: SyncApplicationTestingContext title: 'when object has position field with wrong type (TEXT instead of POSITION)', context: { - manifest: buildBaseManifest( + manifest: buildManifest( buildObjectWithLabelField({ nameSingular: 'wrongPositionObject', namePlural: 'wrongPositionObjects', @@ -240,7 +205,7 @@ describe('Sync application should fail due to object system fields integrity', ( description: 'Object for testing system field deletion', }); - const validManifest = buildBaseManifest({ + const validManifest = buildManifest({ objects: [testObject], fields: [], }); @@ -250,7 +215,7 @@ describe('Sync application should fail due to object system fields integrity', ( expectToFail: false, }); - const manifestWithDeletedIdField = buildBaseManifest({ + const manifestWithDeletedIdField = buildManifest({ objects: [ { ...testObject, @@ -277,7 +242,7 @@ describe('Sync application should fail due to object system fields integrity', ( description: 'Object for testing system field update', }); - const validManifest = buildBaseManifest({ + const validManifest = buildManifest({ objects: [testObject], fields: [], }); @@ -287,7 +252,7 @@ describe('Sync application should fail due to object system fields integrity', ( expectToFail: false, }); - const manifestWithUpdatedIdField = buildBaseManifest({ + const manifestWithUpdatedIdField = buildManifest({ objects: [ { ...testObject, diff --git a/packages/twenty-server/test/integration/metadata/suites/application/successful-manifest-update-field.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/application/successful-manifest-update-field.integration-spec.ts new file mode 100644 index 00000000000..8300dd44998 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/application/successful-manifest-update-field.integration-spec.ts @@ -0,0 +1,243 @@ +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 { 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 { uninstallApplication } from 'test/integration/metadata/suites/application/utils/uninstall-application.util'; +import { type Manifest } from 'twenty-shared/application'; +import { FieldMetadataType } from 'twenty-shared/types'; +import { v4 as uuidv4 } from 'uuid'; +import { findManyObjectMetadataWithIndexes } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata-with-indexes.util'; + +const TEST_APP_ID = uuidv4(); +const TEST_ROLE_ID = uuidv4(); +const TEST_FIELD_ID = uuidv4(); +const TEST_SECOND_FIELD_ID = uuidv4(); + +const TEST_OBJECT = buildDefaultObjectManifest({ + nameSingular: 'ticket', + namePlural: 'tickets', + labelSingular: 'Ticket', + labelPlural: 'Tickets', + description: 'A support ticket', + icon: 'IconTicket', +}); + +const buildManifest = (overrides?: Partial>) => + buildBaseManifest({ + appId: TEST_APP_ID, + roleId: TEST_ROLE_ID, + overrides: { objects: [TEST_OBJECT], ...overrides }, + }); + +const findObjectFields = async () => { + const objects = await findManyObjectMetadataWithIndexes({ + expectToFail: false, + }); + + const object = objects.find( + (o) => o.universalIdentifier === TEST_OBJECT.universalIdentifier, + ); + + return object?.fieldsList ?? []; +}; + +describe('Manifest update - fields', () => { + beforeEach(async () => { + await setupApplicationForSync({ + applicationUniversalIdentifier: TEST_APP_ID, + name: 'Test Application', + description: 'App for testing field manifest updates', + sourcePath: 'test-manifest-update-field', + }); + }, 60000); + + afterEach(async () => { + await uninstallApplication({ + universalIdentifier: TEST_APP_ID, + expectToFail: false, + }); + }); + + it('should create a new field when added to manifest on second sync', async () => { + await syncApplication({ + manifest: buildManifest({ fields: [] }), + expectToFail: false, + }); + + const fieldsAfterFirstSync = await findObjectFields(); + + const descriptionBefore = fieldsAfterFirstSync.find( + (field: { name: string }) => field.name === 'description', + ); + + expect(descriptionBefore).toBeUndefined(); + + await syncApplication({ + manifest: buildManifest({ + fields: [ + { + universalIdentifier: TEST_FIELD_ID, + type: FieldMetadataType.TEXT, + name: 'description', + label: 'Description', + description: 'Ticket description', + icon: 'IconFileDescription', + objectUniversalIdentifier: TEST_OBJECT.universalIdentifier, + }, + ], + }), + expectToFail: false, + }); + + const fieldsAfterSecondSync = await findObjectFields(); + const descriptionAfter = fieldsAfterSecondSync.find( + (field: { name: string }) => field.name === 'description', + ); + + expect(descriptionAfter).toBeDefined(); + expect(descriptionAfter).toMatchObject({ + name: 'description', + label: 'Description', + type: FieldMetadataType.TEXT, + description: 'Ticket description', + icon: 'IconFileDescription', + }); + }, 60000); + + it('should update a field when properties change in manifest on second sync', async () => { + await syncApplication({ + manifest: buildManifest({ + fields: [ + { + universalIdentifier: TEST_FIELD_ID, + type: FieldMetadataType.TEXT, + name: 'description', + label: 'Description', + description: 'Ticket description', + icon: 'IconFileDescription', + objectUniversalIdentifier: TEST_OBJECT.universalIdentifier, + }, + ], + }), + expectToFail: false, + }); + + const fieldsAfterFirstSync = await findObjectFields(); + const fieldBefore = fieldsAfterFirstSync.find( + (field: { name: string }) => field.name === 'description', + ); + + expect(fieldBefore).toBeDefined(); + expect(fieldBefore).toMatchObject({ + name: 'description', + label: 'Description', + }); + + await syncApplication({ + manifest: buildManifest({ + fields: [ + { + universalIdentifier: TEST_FIELD_ID, + type: FieldMetadataType.TEXT, + name: 'body', + label: 'Body', + description: 'Ticket body content', + icon: 'IconFileDescription', + objectUniversalIdentifier: TEST_OBJECT.universalIdentifier, + }, + ], + }), + expectToFail: false, + }); + + const fieldsAfterSecondSync = await findObjectFields(); + const renamedField = fieldsAfterSecondSync.find( + (field: { name: string }) => field.name === 'body', + ); + + expect(renamedField).toBeDefined(); + expect(renamedField).toMatchObject({ + name: 'body', + label: 'Body', + type: FieldMetadataType.TEXT, + description: 'Ticket body content', + }); + + const oldField = fieldsAfterSecondSync.find( + (field: { name: string }) => field.name === 'description', + ); + + expect(oldField).toBeUndefined(); + }, 60000); + + it('should delete a field when removed from manifest on second sync', async () => { + await syncApplication({ + manifest: buildManifest({ + fields: [ + { + universalIdentifier: TEST_FIELD_ID, + type: FieldMetadataType.TEXT, + name: 'description', + label: 'Description', + description: 'Ticket description', + icon: 'IconFileDescription', + objectUniversalIdentifier: TEST_OBJECT.universalIdentifier, + }, + { + universalIdentifier: TEST_SECOND_FIELD_ID, + type: FieldMetadataType.TEXT, + name: 'priority', + label: 'Priority', + description: 'Ticket priority', + icon: 'IconFlag', + objectUniversalIdentifier: TEST_OBJECT.universalIdentifier, + }, + ], + }), + expectToFail: false, + }); + + const fieldsAfterFirstSync = await findObjectFields(); + + expect( + fieldsAfterFirstSync.find( + (field: { name: string }) => field.name === 'description', + ), + ).toBeDefined(); + expect( + fieldsAfterFirstSync.find( + (field: { name: string }) => field.name === 'priority', + ), + ).toBeDefined(); + + await syncApplication({ + manifest: buildManifest({ + fields: [ + { + universalIdentifier: TEST_FIELD_ID, + type: FieldMetadataType.TEXT, + name: 'description', + label: 'Description', + description: 'Ticket description', + icon: 'IconFileDescription', + objectUniversalIdentifier: TEST_OBJECT.universalIdentifier, + }, + ], + }), + expectToFail: false, + }); + + const fieldsAfterSecondSync = await findObjectFields(); + + expect( + fieldsAfterSecondSync.find( + (field: { name: string }) => field.name === 'description', + ), + ).toBeDefined(); + expect( + fieldsAfterSecondSync.find( + (field: { name: string }) => field.name === 'priority', + ), + ).toBeUndefined(); + }, 60000); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/application/successful-manifest-update-object.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/application/successful-manifest-update-object.integration-spec.ts new file mode 100644 index 00000000000..3e2ca156b83 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/application/successful-manifest-update-object.integration-spec.ts @@ -0,0 +1,218 @@ +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 { 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 { uninstallApplication } from 'test/integration/metadata/suites/application/utils/uninstall-application.util'; +import { findManyObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata.util'; +import { type Manifest } from 'twenty-shared/application'; +import { v4 as uuidv4 } from 'uuid'; + +const TEST_APP_ID = uuidv4(); +const TEST_ROLE_ID = uuidv4(); + +const buildManifest = ( + overrides?: Partial>, +) => buildBaseManifest({ appId: TEST_APP_ID, roleId: TEST_ROLE_ID, overrides }); + +const OBJECT_GQL_FIELDS = + 'id nameSingular namePlural labelSingular labelPlural description icon isCustom isActive'; + +const findCustomObjects = async () => { + const { objects } = await findManyObjectMetadata({ + input: { + filter: { isCustom: { is: true } }, + paging: { first: 100 }, + }, + gqlFields: OBJECT_GQL_FIELDS, + expectToFail: false, + }); + + return objects; +}; + +describe('Manifest update - objects', () => { + beforeEach(async () => { + await setupApplicationForSync({ + applicationUniversalIdentifier: TEST_APP_ID, + name: 'Test Application', + description: 'App for testing object manifest updates', + sourcePath: 'test-manifest-update-object', + }); + }, 60000); + + afterEach(async () => { + await uninstallApplication({ + universalIdentifier: TEST_APP_ID, + expectToFail: false, + }); + }); + + it('should create a new object when added to manifest on second sync', async () => { + const ticketObject = buildDefaultObjectManifest({ + nameSingular: 'ticket', + namePlural: 'tickets', + labelSingular: 'Ticket', + labelPlural: 'Tickets', + description: 'A support ticket', + icon: 'IconTicket', + }); + + await syncApplication({ + manifest: buildManifest({ objects: [ticketObject] }), + expectToFail: false, + }); + + const objectsAfterFirstSync = await findCustomObjects(); + const ticket = objectsAfterFirstSync.find( + (obj) => obj.nameSingular === 'ticket', + ); + + expect(ticket).toBeDefined(); + expect(ticket).toMatchObject({ + nameSingular: 'ticket', + labelSingular: 'Ticket', + description: 'A support ticket', + icon: 'IconTicket', + }); + + const invoiceObject = buildDefaultObjectManifest({ + nameSingular: 'invoice', + namePlural: 'invoices', + labelSingular: 'Invoice', + labelPlural: 'Invoices', + description: 'A billing invoice', + icon: 'IconFileInvoice', + }); + + await syncApplication({ + manifest: buildManifest({ + objects: [ticketObject, invoiceObject], + }), + expectToFail: false, + }); + + const objectsAfterSecondSync = await findCustomObjects(); + const ticketAfter = objectsAfterSecondSync.find( + (obj) => obj.nameSingular === 'ticket', + ); + const invoiceAfter = objectsAfterSecondSync.find( + (obj) => obj.nameSingular === 'invoice', + ); + + expect(ticketAfter).toBeDefined(); + expect(invoiceAfter).toBeDefined(); + expect(invoiceAfter).toMatchObject({ + nameSingular: 'invoice', + namePlural: 'invoices', + labelSingular: 'Invoice', + labelPlural: 'Invoices', + description: 'A billing invoice', + icon: 'IconFileInvoice', + isCustom: true, + }); + }, 60000); + + it('should update object properties when changed in manifest on second sync', async () => { + const ticketObject = buildDefaultObjectManifest({ + nameSingular: 'ticket', + namePlural: 'tickets', + labelSingular: 'Ticket', + labelPlural: 'Tickets', + description: 'A support ticket', + icon: 'IconTicket', + }); + + await syncApplication({ + manifest: buildManifest({ objects: [ticketObject] }), + expectToFail: false, + }); + + const objectsAfterFirstSync = await findCustomObjects(); + const ticketBefore = objectsAfterFirstSync.find( + (obj) => obj.nameSingular === 'ticket', + ); + + expect(ticketBefore).toMatchObject({ + labelSingular: 'Ticket', + description: 'A support ticket', + icon: 'IconTicket', + }); + + const updatedTicketObject = { + ...ticketObject, + labelSingular: 'Support Ticket', + labelPlural: 'Support Tickets', + description: 'A customer support ticket', + icon: 'IconHeadset', + }; + + await syncApplication({ + manifest: buildManifest({ objects: [updatedTicketObject] }), + expectToFail: false, + }); + + const objectsAfterSecondSync = await findCustomObjects(); + const ticketAfter = objectsAfterSecondSync.find( + (obj) => obj.nameSingular === 'ticket', + ); + + expect(ticketAfter).toBeDefined(); + expect(ticketAfter).toMatchObject({ + nameSingular: 'ticket', + labelSingular: 'Support Ticket', + labelPlural: 'Support Tickets', + description: 'A customer support ticket', + icon: 'IconHeadset', + }); + }, 60000); + + it('should delete an object when removed from manifest on second sync', async () => { + const ticketObject = buildDefaultObjectManifest({ + nameSingular: 'ticket', + namePlural: 'tickets', + labelSingular: 'Ticket', + labelPlural: 'Tickets', + description: 'A support ticket', + icon: 'IconTicket', + }); + + const invoiceObject = buildDefaultObjectManifest({ + nameSingular: 'invoice', + namePlural: 'invoices', + labelSingular: 'Invoice', + labelPlural: 'Invoices', + description: 'A billing invoice', + icon: 'IconFileInvoice', + }); + + await syncApplication({ + manifest: buildManifest({ + objects: [ticketObject, invoiceObject], + }), + expectToFail: false, + }); + + const objectsAfterFirstSync = await findCustomObjects(); + + expect( + objectsAfterFirstSync.find((obj) => obj.nameSingular === 'ticket'), + ).toBeDefined(); + expect( + objectsAfterFirstSync.find((obj) => obj.nameSingular === 'invoice'), + ).toBeDefined(); + + await syncApplication({ + manifest: buildManifest({ objects: [ticketObject] }), + expectToFail: false, + }); + + const objectsAfterSecondSync = await findCustomObjects(); + + expect( + objectsAfterSecondSync.find((obj) => obj.nameSingular === 'ticket'), + ).toBeDefined(); + expect( + objectsAfterSecondSync.find((obj) => obj.nameSingular === 'invoice'), + ).toBeUndefined(); + }, 60000); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/application/successful-manifest-update-role.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/application/successful-manifest-update-role.integration-spec.ts new file mode 100644 index 00000000000..48a9352520c --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/application/successful-manifest-update-role.integration-spec.ts @@ -0,0 +1,187 @@ +import { buildBaseManifest } from 'test/integration/metadata/suites/application/utils/build-base-manifest.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 { uninstallApplication } from 'test/integration/metadata/suites/application/utils/uninstall-application.util'; +import { findRoles } from 'test/integration/metadata/suites/role/utils/find-roles.util'; +import { type Manifest } from 'twenty-shared/application'; +import { v4 as uuidv4 } from 'uuid'; + +const TEST_APP_ID = uuidv4(); +const TEST_ROLE_ID = uuidv4(); +const TEST_SECOND_ROLE_ID = uuidv4(); + +const buildManifest = (overrides?: Partial>) => + buildBaseManifest({ + appId: TEST_APP_ID, + roleId: TEST_ROLE_ID, + overrides: { + roles: [ + { + universalIdentifier: TEST_ROLE_ID, + label: 'Editor', + description: 'Can edit', + }, + ], + ...overrides, + }, + }); + +const findAppRoles = async () => { + const { data } = await findRoles({ + gqlFields: 'id label description universalIdentifier', + expectToFail: false, + }); + + return data.getRoles.filter( + (role) => + role.universalIdentifier === TEST_ROLE_ID || + role.universalIdentifier === TEST_SECOND_ROLE_ID, + ); +}; + +describe('Manifest update - roles', () => { + beforeEach(async () => { + await setupApplicationForSync({ + applicationUniversalIdentifier: TEST_APP_ID, + name: 'Test Application', + description: 'App for testing role manifest updates', + sourcePath: 'test-manifest-update-role', + }); + }, 60000); + + afterEach(async () => { + await uninstallApplication({ + universalIdentifier: TEST_APP_ID, + expectToFail: false, + }); + }); + + it('should create a new role when added to manifest on second sync', async () => { + await syncApplication({ + manifest: buildManifest({}), + expectToFail: false, + }); + + const rolesAfterFirstSync = await findAppRoles(); + + expect(rolesAfterFirstSync).toHaveLength(1); + expect(rolesAfterFirstSync[0]).toMatchObject({ + label: 'Editor', + description: 'Can edit', + }); + + await syncApplication({ + manifest: buildManifest({ + roles: [ + { + universalIdentifier: TEST_ROLE_ID, + label: 'Editor', + description: 'Can edit', + }, + { + universalIdentifier: TEST_SECOND_ROLE_ID, + label: 'Viewer', + description: 'Read-only access', + }, + ], + }), + expectToFail: false, + }); + + const rolesAfterSecondSync = await findAppRoles(); + + expect(rolesAfterSecondSync).toHaveLength(2); + expect( + rolesAfterSecondSync.find((r) => r.label === 'Editor'), + ).toBeDefined(); + expect( + rolesAfterSecondSync.find((r) => r.label === 'Viewer'), + ).toBeDefined(); + expect( + rolesAfterSecondSync.find((r) => r.label === 'Viewer'), + ).toMatchObject({ + description: 'Read-only access', + }); + }, 60000); + + it('should update a role when properties change in manifest on second sync', async () => { + await syncApplication({ + manifest: buildManifest({}), + expectToFail: false, + }); + + const rolesAfterFirstSync = await findAppRoles(); + + expect(rolesAfterFirstSync).toHaveLength(1); + expect(rolesAfterFirstSync[0]).toMatchObject({ + label: 'Editor', + description: 'Can edit', + }); + + await syncApplication({ + manifest: buildManifest({ + roles: [ + { + universalIdentifier: TEST_ROLE_ID, + label: 'Senior Editor', + description: 'Can edit everything', + }, + ], + }), + expectToFail: false, + }); + + const rolesAfterSecondSync = await findAppRoles(); + + expect(rolesAfterSecondSync).toHaveLength(1); + expect(rolesAfterSecondSync[0]).toMatchObject({ + label: 'Senior Editor', + description: 'Can edit everything', + }); + }, 60000); + + it('should delete a role when removed from manifest on second sync', async () => { + await syncApplication({ + manifest: buildManifest({ + roles: [ + { + universalIdentifier: TEST_ROLE_ID, + label: 'Editor', + description: 'Can edit', + }, + { + universalIdentifier: TEST_SECOND_ROLE_ID, + label: 'Viewer', + description: 'Read-only access', + }, + ], + }), + expectToFail: false, + }); + + const rolesAfterFirstSync = await findAppRoles(); + + expect(rolesAfterFirstSync).toHaveLength(2); + + await syncApplication({ + manifest: buildManifest({ + roles: [ + { + universalIdentifier: TEST_ROLE_ID, + label: 'Editor', + description: 'Can edit', + }, + ], + }), + expectToFail: false, + }); + + const rolesAfterSecondSync = await findAppRoles(); + + expect(rolesAfterSecondSync).toHaveLength(1); + expect(rolesAfterSecondSync[0]).toMatchObject({ + label: 'Editor', + description: 'Can edit', + }); + }, 60000); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/application/successful-manifest-update-skill.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/application/successful-manifest-update-skill.integration-spec.ts new file mode 100644 index 00000000000..498c0d0faa2 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/application/successful-manifest-update-skill.integration-spec.ts @@ -0,0 +1,202 @@ +import { buildBaseManifest } from 'test/integration/metadata/suites/application/utils/build-base-manifest.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 { uninstallApplication } from 'test/integration/metadata/suites/application/utils/uninstall-application.util'; +import { findSkills } from 'test/integration/metadata/suites/skill/utils/find-skills.util'; +import { type Manifest } from 'twenty-shared/application'; +import { v4 as uuidv4 } from 'uuid'; + +const TEST_APP_ID = uuidv4(); +const TEST_ROLE_ID = uuidv4(); +const TEST_SKILL_ID = uuidv4(); +const TEST_SECOND_SKILL_ID = uuidv4(); + +const SKILL_GQL_FIELDS = 'id name label description content icon applicationId'; + +const buildManifest = (overrides?: Partial>) => + buildBaseManifest({ appId: TEST_APP_ID, roleId: TEST_ROLE_ID, overrides }); + +const findAppSkills = async () => { + const { data } = await findSkills({ + gqlFields: SKILL_GQL_FIELDS, + expectToFail: false, + input: undefined, + }); + + return data.skills.filter( + (skill) => skill.name === 'enrichment' || skill.name === 'scoring', + ); +}; + +describe('Manifest update - skills', () => { + beforeEach(async () => { + await setupApplicationForSync({ + applicationUniversalIdentifier: TEST_APP_ID, + name: 'Test Application', + description: 'App for testing skill manifest updates', + sourcePath: 'test-manifest-update-skill', + }); + }, 60000); + + afterEach(async () => { + await uninstallApplication({ + universalIdentifier: TEST_APP_ID, + expectToFail: false, + }); + }); + + it('should create a new skill when added to manifest on second sync', async () => { + await syncApplication({ + manifest: buildManifest({ skills: [] }), + expectToFail: false, + }); + + const skillsAfterFirstSync = await findAppSkills(); + + expect(skillsAfterFirstSync).toHaveLength(0); + + await syncApplication({ + manifest: buildManifest({ + skills: [ + { + universalIdentifier: TEST_SKILL_ID, + name: 'enrichment', + label: 'Enrichment', + description: 'Enrich contact data', + icon: 'IconSparkles', + content: '# Enrichment\n\nEnrich your data.', + }, + ], + }), + expectToFail: false, + }); + + const skillsAfterSecondSync = await findAppSkills(); + + expect(skillsAfterSecondSync).toHaveLength(1); + expect(skillsAfterSecondSync[0]).toMatchObject({ + name: 'enrichment', + label: 'Enrichment', + description: 'Enrich contact data', + icon: 'IconSparkles', + content: '# Enrichment\n\nEnrich your data.', + }); + }, 60000); + + it('should update a skill when properties change in manifest on second sync', async () => { + await syncApplication({ + manifest: buildManifest({ + skills: [ + { + universalIdentifier: TEST_SKILL_ID, + name: 'enrichment', + label: 'Enrichment', + description: 'Enrich contact data', + icon: 'IconSparkles', + content: '# Enrichment\n\nEnrich your data.', + }, + ], + }), + expectToFail: false, + }); + + const skillsAfterFirstSync = await findAppSkills(); + + expect(skillsAfterFirstSync).toHaveLength(1); + expect(skillsAfterFirstSync[0]).toMatchObject({ + label: 'Enrichment', + description: 'Enrich contact data', + }); + + await syncApplication({ + manifest: buildManifest({ + skills: [ + { + universalIdentifier: TEST_SKILL_ID, + name: 'enrichment', + label: 'Data Enrichment', + description: 'Enrich contact and company data', + icon: 'IconSparkles', + content: + '# Data Enrichment\n\nEnrich your contact and company data from multiple sources.', + }, + ], + }), + expectToFail: false, + }); + + const skillsAfterSecondSync = await findAppSkills(); + + expect(skillsAfterSecondSync).toHaveLength(1); + expect(skillsAfterSecondSync[0]).toMatchObject({ + name: 'enrichment', + label: 'Data Enrichment', + description: 'Enrich contact and company data', + content: + '# Data Enrichment\n\nEnrich your contact and company data from multiple sources.', + }); + }, 60000); + + it('should delete a skill when removed from manifest on second sync', async () => { + await syncApplication({ + manifest: buildManifest({ + skills: [ + { + universalIdentifier: TEST_SKILL_ID, + name: 'enrichment', + label: 'Enrichment', + description: 'Enrich contact data', + icon: 'IconSparkles', + content: '# Enrichment\n\nEnrich your data.', + }, + { + universalIdentifier: TEST_SECOND_SKILL_ID, + name: 'scoring', + label: 'Lead Scoring', + description: 'Score your leads', + icon: 'IconChartBar', + content: '# Lead Scoring\n\nScore leads automatically.', + }, + ], + }), + expectToFail: false, + }); + + const skillsAfterFirstSync = await findAppSkills(); + + expect(skillsAfterFirstSync).toHaveLength(2); + expect( + skillsAfterFirstSync.find((s) => s.name === 'enrichment'), + ).toBeDefined(); + expect( + skillsAfterFirstSync.find((s) => s.name === 'scoring'), + ).toBeDefined(); + + await syncApplication({ + manifest: buildManifest({ + skills: [ + { + universalIdentifier: TEST_SKILL_ID, + name: 'enrichment', + label: 'Enrichment', + description: 'Enrich contact data', + icon: 'IconSparkles', + content: '# Enrichment\n\nEnrich your data.', + }, + ], + }), + expectToFail: false, + }); + + const skillsAfterSecondSync = await findAppSkills(); + + expect(skillsAfterSecondSync).toHaveLength(1); + expect(skillsAfterSecondSync[0]).toMatchObject({ + name: 'enrichment', + label: 'Enrichment', + }); + expect( + skillsAfterSecondSync.find((s) => s.name === 'scoring'), + ).toBeUndefined(); + }, 60000); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/application/successful-sync-application-workspace-migration.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/application/successful-sync-application-workspace-migration.integration-spec.ts index b3a7ec04bf9..420ca2326e2 100644 --- a/packages/twenty-server/test/integration/metadata/suites/application/successful-sync-application-workspace-migration.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/application/successful-sync-application-workspace-migration.integration-spec.ts @@ -2,15 +2,19 @@ import { buildDefaultObjectManifest } from 'test/integration/metadata/suites/app 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 { uninstallApplication } from 'test/integration/metadata/suites/application/utils/uninstall-application.util'; +import { findManyObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata.util'; +import { findRoles } from 'test/integration/metadata/suites/role/utils/find-roles.util'; +import { findSkills } from 'test/integration/metadata/suites/skill/utils/find-skills.util'; import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any'; -import { type Manifest } from 'twenty-shared/application'; +import { type FieldManifest, type Manifest } from 'twenty-shared/application'; import { STANDARD_OBJECTS } from 'twenty-shared/metadata'; import { FieldMetadataType } from 'twenty-shared/types'; import { v4 as uuidv4 } from 'uuid'; +import { buildBaseManifest } from 'test/integration/metadata/suites/application/utils/build-base-manifest.util'; +import { findManyObjectMetadataWithIndexes } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata-with-indexes.util'; const TEST_APP_ID = uuidv4(); const TEST_ROLE_ID = uuidv4(); -const TEST_SECOND_ROLE_ID = uuidv4(); const TEST_FIELD_ID = uuidv4(); const TEST_SKILL_ID = uuidv4(); @@ -23,6 +27,39 @@ const TEST_OBJECT = buildDefaultObjectManifest({ icon: 'IconTicket', }); +const TEST_SKILL = { + universalIdentifier: TEST_SKILL_ID, + name: 'test-skill', + label: 'Test Skill', + description: 'A skill for testing', + icon: 'IconBrain', + content: '# Test Skill\n\nThis is a test skill.', +}; + +const TEST_FIELD: FieldManifest = { + universalIdentifier: TEST_FIELD_ID, + type: FieldMetadataType.TEXT, + name: 'description', + label: 'Description', + description: 'Ticket description', + icon: 'IconFileDescription', + objectUniversalIdentifier: TEST_OBJECT.universalIdentifier, +}; + +const buildManifest = ( + overrides?: Partial>, +) => + buildBaseManifest({ + appId: TEST_APP_ID, + roleId: TEST_ROLE_ID, + overrides: { + objects: [TEST_OBJECT], + skills: [TEST_SKILL], + fields: [TEST_FIELD], + ...overrides, + }, + }); + describe('syncApplication', () => { let appCreated = false; @@ -49,48 +86,8 @@ describe('syncApplication', () => { }); it('should return workspace migration actions on initial sync then on second sync with field rename and new role', async () => { - const initialManifest: Manifest = { - application: { - universalIdentifier: TEST_APP_ID, - defaultRoleUniversalIdentifier: TEST_ROLE_ID, - displayName: 'Test Application', - description: 'A test application for workspace migration', - icon: 'IconTestPipe', - applicationVariables: {}, - packageJsonChecksum: null, - yarnLockChecksum: null, - apiClientChecksum: null, - }, - roles: [ - { - universalIdentifier: TEST_ROLE_ID, - label: 'Test Role', - description: 'A test role', - }, - ], - skills: [], - objects: [TEST_OBJECT], - fields: [ - { - universalIdentifier: TEST_FIELD_ID, - type: FieldMetadataType.TEXT, - name: 'description', - label: 'Description', - description: 'Ticket description', - icon: 'IconFileDescription', - objectUniversalIdentifier: TEST_OBJECT.universalIdentifier, - }, - ], - logicFunctions: [], - frontComponents: [], - publicAssets: [], - views: [], - navigationMenuItems: [], - pageLayouts: [], - }; - const { data: firstSyncData } = await syncApplication({ - manifest: initialManifest, + manifest: buildManifest(), expectToFail: false, }); @@ -98,139 +95,92 @@ describe('syncApplication', () => { extractRecordIdsAndDatesAsExpectAny(firstSyncData), ); - const updatedManifest: Manifest = { - ...initialManifest, - roles: [ - { - universalIdentifier: TEST_ROLE_ID, - label: 'Test Role', - description: 'A test role', - }, - { - universalIdentifier: TEST_SECOND_ROLE_ID, - label: 'Viewer Role', - description: 'A read-only role', - }, - ], - fields: [ - { - universalIdentifier: TEST_FIELD_ID, - type: FieldMetadataType.TEXT, - name: 'body', - label: 'Body', - description: 'Ticket description', - icon: 'IconFileDescription', - objectUniversalIdentifier: TEST_OBJECT.universalIdentifier, - }, - ], - }; - - const { data: secondSyncData } = await syncApplication({ - manifest: updatedManifest, - expectToFail: false, - }); - - expect(secondSyncData).toMatchSnapshot( - extractRecordIdsAndDatesAsExpectAny(secondSyncData), - ); - }, 60000); - - it('should sync a skill then update it on second sync', async () => { - const initialManifest: Manifest = { - application: { - universalIdentifier: TEST_APP_ID, - defaultRoleUniversalIdentifier: TEST_ROLE_ID, - displayName: 'Test Application', - description: 'A test application for workspace migration', - icon: 'IconTestPipe', - applicationVariables: {}, - packageJsonChecksum: null, - yarnLockChecksum: null, - apiClientChecksum: null, + // Verify database state after first sync + const { objects: objectsAfterSync } = await findManyObjectMetadata({ + input: { + filter: { isCustom: { is: true } }, + paging: { first: 100 }, }, - roles: [ - { - universalIdentifier: TEST_ROLE_ID, - label: 'Test Role', - description: 'A test role', - }, - ], - skills: [ - { - universalIdentifier: TEST_SKILL_ID, - name: 'test-skill', - label: 'Test Skill', - description: 'A skill for testing', - icon: 'IconBrain', - content: '# Test Skill\n\nThis is a test skill.', - }, - ], - objects: [], - fields: [], - logicFunctions: [], - frontComponents: [], - publicAssets: [], - views: [], - navigationMenuItems: [], - pageLayouts: [], - }; - - const { data: firstSyncData } = await syncApplication({ - manifest: initialManifest, + gqlFields: + 'id nameSingular namePlural labelSingular labelPlural description icon isCustom', expectToFail: false, }); - expect(firstSyncData).toMatchSnapshot( - extractRecordIdsAndDatesAsExpectAny(firstSyncData), + const ticketObject = objectsAfterSync.find( + (obj) => obj.nameSingular === 'ticket', ); - const updatedManifest: Manifest = { - ...initialManifest, - skills: [ - { - universalIdentifier: TEST_SKILL_ID, - name: 'test-skill', - label: 'Test Skill Updated', - description: 'An updated skill for testing', - icon: 'IconBrain', - content: - '# Test Skill\n\nThis is an updated test skill with more content.', - }, - ], - }; + expect(ticketObject).toBeDefined(); + expect(ticketObject).toMatchObject({ + nameSingular: 'ticket', + namePlural: 'tickets', + labelSingular: 'Ticket', + labelPlural: 'Tickets', + description: 'A support ticket', + icon: 'IconTicket', + isCustom: true, + }); - const { data: secondSyncData } = await syncApplication({ - manifest: updatedManifest, + const objects = await findManyObjectMetadataWithIndexes({ expectToFail: false, }); - expect(secondSyncData).toMatchSnapshot( - extractRecordIdsAndDatesAsExpectAny(secondSyncData), + const fieldsAfterSync = objects.find( + (o) => o.universalIdentifier === TEST_OBJECT.universalIdentifier, + )?.fieldsList; + + const descriptionField = fieldsAfterSync?.find( + (f) => f.universalIdentifier === TEST_FIELD_ID, ); + + expect(descriptionField).toBeDefined(); + expect(descriptionField).toMatchObject({ + name: 'description', + label: 'Description', + type: FieldMetadataType.TEXT, + description: 'Ticket description', + icon: 'IconFileDescription', + }); + + const { data: rolesAfterSync } = await findRoles({ + gqlFields: 'id label description universalIdentifier', + expectToFail: false, + }); + + const testRole = rolesAfterSync.getRoles.find( + (role) => role.universalIdentifier === TEST_ROLE_ID, + ); + + const { data: skillsAfterSync } = await findSkills({ + gqlFields: 'id name label description content icon', + expectToFail: false, + input: undefined, + }); + + const testSkill = skillsAfterSync.skills.find( + (skill) => skill.name === 'test-skill', + ); + + expect(testRole).toBeDefined(); + expect(testRole).toMatchObject({ + label: 'Test Role', + description: 'A test role', + }); + + expect(testSkill).toBeDefined(); + expect(testSkill).toMatchObject({ + name: 'test-skill', + label: 'Test Skill', + description: 'A skill for testing', + icon: 'IconBrain', + content: '# Test Skill\n\nThis is a test skill.', + }); }, 60000); it('should create a TEXT field on the standard Company object', async () => { const companyFieldId = uuidv4(); - const manifest: Manifest = { - application: { - universalIdentifier: TEST_APP_ID, - defaultRoleUniversalIdentifier: TEST_ROLE_ID, - displayName: 'Test Application', - description: 'A test application for workspace migration', - icon: 'IconTestPipe', - applicationVariables: {}, - packageJsonChecksum: null, - yarnLockChecksum: null, - apiClientChecksum: null, - }, - roles: [ - { - universalIdentifier: TEST_ROLE_ID, - label: 'Test Role', - description: 'A test role', - }, - ], + const manifest = buildManifest({ skills: [], objects: [], fields: [ @@ -245,13 +195,7 @@ describe('syncApplication', () => { STANDARD_OBJECTS.company.universalIdentifier, }, ], - logicFunctions: [], - frontComponents: [], - publicAssets: [], - views: [], - navigationMenuItems: [], - pageLayouts: [], - }; + }); const { data: syncData } = await syncApplication({ manifest, diff --git a/packages/twenty-server/test/integration/metadata/suites/application/utils/build-base-manifest.util.ts b/packages/twenty-server/test/integration/metadata/suites/application/utils/build-base-manifest.util.ts new file mode 100644 index 00000000000..6bb5f474d9a --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/application/utils/build-base-manifest.util.ts @@ -0,0 +1,40 @@ +import { type Manifest } from 'twenty-shared/application'; + +export const buildBaseManifest = ({ + appId, + roleId, + overrides, +}: { + appId: string; + roleId: string; + overrides?: Partial; +}): Manifest => ({ + application: { + universalIdentifier: appId, + defaultRoleUniversalIdentifier: roleId, + displayName: 'Test Application', + description: 'Test application', + icon: 'IconTestPipe', + applicationVariables: {}, + packageJsonChecksum: null, + yarnLockChecksum: null, + apiClientChecksum: null, + }, + roles: [ + { + universalIdentifier: roleId, + label: 'Test Role', + description: 'A test role', + }, + ], + skills: [], + objects: [], + fields: [], + logicFunctions: [], + frontComponents: [], + publicAssets: [], + views: [], + navigationMenuItems: [], + pageLayouts: [], + ...overrides, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata-with-indexes.util.ts b/packages/twenty-server/test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata-with-indexes.util.ts index cbd44534b40..5f001443ab6 100644 --- a/packages/twenty-server/test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata-with-indexes.util.ts +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata-with-indexes.util.ts @@ -22,10 +22,15 @@ export const findManyObjectMetadataWithIndexes = async ({ gqlFields: ` id nameSingular + universalIdentifier fieldsList { id type name + description + icon + label + universalIdentifier relation { type sourceObjectMetadata { diff --git a/packages/twenty-server/test/integration/metadata/suites/role/utils/find-roles-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/role/utils/find-roles-query-factory.util.ts index 72b81e22a0d..11dfb79129e 100644 --- a/packages/twenty-server/test/integration/metadata/suites/role/utils/find-roles-query-factory.util.ts +++ b/packages/twenty-server/test/integration/metadata/suites/role/utils/find-roles-query-factory.util.ts @@ -5,6 +5,7 @@ const DEFAULT_ROLE_GQL_FIELDS = ` label description icon + universalIdentifier isEditable canUpdateAllSettings canAccessAllTools diff --git a/packages/twenty-shared/src/metadata/__tests__/computeMetadataNameFromLabel.test.ts b/packages/twenty-shared/src/metadata/__tests__/computeMetadataNameFromLabel.test.ts index 30c959cd5c5..16b42bcb2c6 100644 --- a/packages/twenty-shared/src/metadata/__tests__/computeMetadataNameFromLabel.test.ts +++ b/packages/twenty-shared/src/metadata/__tests__/computeMetadataNameFromLabel.test.ts @@ -7,6 +7,12 @@ describe('computeMetadataNameFromLabel', () => { ); }); + it('should convert a label to camelCase', () => { + expect(computeMetadataNameFromLabel({ label: 'My CreatedAt Field' })).toBe( + 'myCreatedatField', + ); + }); + it('should return empty string for empty label', () => { expect(computeMetadataNameFromLabel({ label: '' })).toBe(''); }); diff --git a/packages/twenty-shared/src/metadata/constants/standard-object.constant.ts b/packages/twenty-shared/src/metadata/constants/standard-object.constant.ts index 03f58b85fdc..a875434a89b 100644 --- a/packages/twenty-shared/src/metadata/constants/standard-object.constant.ts +++ b/packages/twenty-shared/src/metadata/constants/standard-object.constant.ts @@ -68,22 +68,22 @@ export const STANDARD_OBJECTS = { universalIdentifier: 'b8d4f9a3-0c25-4e7b-9f6a-2d3e4c5b6f70', }, noteIdIndex: { - universalIdentifier: 'c9e5a0b4-1d36-4f8c-0a7b-3e4f5d6c7a81', + universalIdentifier: '9d31ea73-13b6-4e06-84ee-c66c72bf7787', }, personIdIndex: { - universalIdentifier: 'd0f6b1c5-2e47-4a9d-1b8c-4f5a6e7d8b92', + universalIdentifier: '55637a5a-1edc-4351-8d76-d40020bf8944', }, companyIdIndex: { - universalIdentifier: 'e1a7c2d6-3f58-4b0e-2c9d-5a6b7f8e9c03', + universalIdentifier: '4137ba06-184d-438f-b484-080f02a97659', }, opportunityIdIndex: { - universalIdentifier: 'f2b8d3e7-4a69-4c1f-3d0e-6b7c8a9f0d14', + universalIdentifier: '8cc162d1-c127-4981-878d-f78622f8f12d', }, dashboardIdIndex: { - universalIdentifier: '03c9e4f8-5b70-4d2a-4e1f-7c8d9b0a1e25', + universalIdentifier: 'c10eba2d-ff1a-4eab-9285-50481c12a003', }, workflowIdIndex: { - universalIdentifier: '14d0f5a9-6c81-4e3b-5f2a-8d9e0c1b2f36', + universalIdentifier: 'fadeab4b-79ee-4173-af79-72c51fbad888', }, }, views: { @@ -159,7 +159,7 @@ export const STANDARD_OBJECTS = { }, indexes: { workspaceMemberIdIndex: { - universalIdentifier: '25e1a6b0-7d92-4f4c-6a3b-9e0f1d2c3a47', + universalIdentifier: '4daf320e-74d0-4f24-a45a-af3a09d741cb', }, }, }, @@ -205,7 +205,7 @@ export const STANDARD_OBJECTS = { }, indexes: { calendarChannelIdIndex: { - universalIdentifier: '36f2b7c1-8e03-4a5d-7b4c-0f1a2e3d4b58', + universalIdentifier: 'ff6b86c1-3112-4dfa-b734-c4789111a716', }, calendarEventIdIndex: { universalIdentifier: '47a3c8d2-9f14-4b6e-8c5d-1a2b3f4e5c69', @@ -334,13 +334,13 @@ export const STANDARD_OBJECTS = { }, indexes: { calendarEventIdIndex: { - universalIdentifier: '69c5e0f4-1b36-4d8a-0e7f-3c4d5b6a7e81', + universalIdentifier: 'c458ad97-8b95-43de-9003-88eb68576049', }, personIdIndex: { - universalIdentifier: '70d6f1a5-2c47-4e9b-1f8a-4d5e6c7b8f92', + universalIdentifier: '30e9b75a-881f-4a85-aaf1-f2d2464be1cf', }, workspaceMemberIdIndex: { - universalIdentifier: '81e7a2b6-3d58-4f0c-2a9b-5e6f7d8c9003', + universalIdentifier: '898aa202-428f-4a7a-a3b3-8f0a17a6658e', }, }, }, @@ -507,13 +507,13 @@ export const STANDARD_OBJECTS = { }, indexes: { accountOwnerIdIndex: { - universalIdentifier: '92f8b3c7-4e69-4a1d-3b0c-6f7a8e9d0114', + universalIdentifier: 'ec2ebfc9-0c9b-4597-a87d-aa295e2d8bfe', }, domainNameUniqueIndex: { - universalIdentifier: 'a3a9c4d8-5f70-4b2e-4c1d-7a8b9f0e1225', + universalIdentifier: 'dd300c61-f422-467a-91f4-de4f83c4175b', }, searchVectorGinIndex: { - universalIdentifier: 'b4b0d5e9-6a81-4c3f-5d2e-8b9c0a1f2336', + universalIdentifier: 'c3eb62df-2cc1-4cc3-b7aa-e96a4d65c633', }, }, views: { @@ -661,7 +661,7 @@ export const STANDARD_OBJECTS = { }, indexes: { accountOwnerIdIndex: { - universalIdentifier: 'c5c1e6f0-7b92-4d4a-6e3f-9c0d1b2a3447', + universalIdentifier: '8e7ca28e-6002-4304-9dcc-0a8da93ca198', }, }, }, @@ -690,7 +690,7 @@ export const STANDARD_OBJECTS = { universalIdentifier: '53ee42e7-f157-42b5-b278-a5fa9b378307', }, timelineActivities: { - universalIdentifier: '20202020-9b0c-5d6e-7f8a-9b0c1d2e3f4a', + universalIdentifier: '99c330c0-5b7d-4276-a764-aed84499dfb5', }, favorites: { universalIdentifier: '20202020-f032-478f-88fa-6426ff6f1e4c', @@ -704,7 +704,7 @@ export const STANDARD_OBJECTS = { }, indexes: { searchVectorGinIndex: { - universalIdentifier: 'd6d2f7a1-8c03-4e5b-7f4a-0d1e2c3b4558', + universalIdentifier: 'e69f71aa-de0f-4b70-845f-7a8369c47928', }, }, views: { @@ -783,28 +783,28 @@ export const STANDARD_OBJECTS = { universalIdentifier: 'f8f4b9c3-0e25-4a7d-9b6c-2f3a4e5d677a', }, companyIdIndex: { - universalIdentifier: '0905c0d4-1f36-4b8e-0c7d-3a4b5f6e788b', + universalIdentifier: '1f360393-a336-435d-966d-8ec2645f875c', }, favoriteFolderIdIndex: { - universalIdentifier: '1016d1e5-2a47-4c9f-1d8e-4b5c6a7f899c', + universalIdentifier: 'c0369a13-49bd-48b0-a9f0-6ed0ee8e1b09', }, opportunityIdIndex: { - universalIdentifier: '2127e2f6-3b58-4d0a-2e9f-5c6d7b80900d', + universalIdentifier: 'b8e9f696-5be4-48cb-815c-7c0bb8b69d38', }, workflowIdIndex: { - universalIdentifier: '3238f3a7-4c69-4e1b-3f0a-6d7e8c91011e', + universalIdentifier: 'd4f3ef9f-ae24-4cef-9d7a-684a24d4968b', }, workflowVersionIdIndex: { - universalIdentifier: '4349a4b8-5d70-4f2c-4a1b-7e8f9d02122f', + universalIdentifier: 'a257158e-3a89-4715-970c-7fc38ac22370', }, workflowRunIdIndex: { - universalIdentifier: '5450b5c9-6e81-4a3d-5b2c-8f90ae132340', + universalIdentifier: 'd55711a3-3297-4fef-beab-48d733712a33', }, taskIdIndex: { - universalIdentifier: '6561c6d0-7f92-4b4e-6c3d-90a1bf243451', + universalIdentifier: 'f30c9a75-a563-45a2-93b8-84f59004de8f', }, noteIdIndex: { - universalIdentifier: '7672d7e1-8003-4c5f-7d4e-01b2c0354562', + universalIdentifier: 'dca73027-adbe-4078-ae47-8d17850b9f2b', }, dashboardIdIndex: { universalIdentifier: '8783e8f2-9114-4d6a-8e5f-12c3d1465673', @@ -889,7 +889,7 @@ export const STANDARD_OBJECTS = { universalIdentifier: 'edddd409-d9f0-4b93-8e3f-37faef6a3387', }, messageFolders: { - universalIdentifier: '20202020-c3d4-e5f6-a7b8-901234567890', + universalIdentifier: '9bfc9da7-ae2d-44fd-9563-ede90c5d6222', }, }, indexes: { @@ -897,10 +897,10 @@ export const STANDARD_OBJECTS = { universalIdentifier: '9894f9a3-0225-4e7b-9f6a-23d4e2576784', }, messageIdIndex: { - universalIdentifier: '0905a0b4-1336-4f8c-0a7b-34e5f3687895', + universalIdentifier: '9bb24d40-60dd-4beb-8c64-a74e8c67f9ee', }, messageChannelIdMessageIdUniqueIndex: { - universalIdentifier: '1a16b1c5-2447-4a9d-1b8c-45f6a47989a6', + universalIdentifier: '1b86ece8-7ce3-4df3-8771-fd4b5d45b2f2', }, }, }, @@ -932,21 +932,21 @@ export const STANDARD_OBJECTS = { universalIdentifier: '38633a97-0e88-44de-9903-b8c9e0f59a36', }, messageChannelMessageAssociation: { - universalIdentifier: '20202020-d4e5-f6a7-b8c9-012345678901', + universalIdentifier: '7411cfa3-4fd9-4b90-a636-940015fd7243', }, messageFolder: { - universalIdentifier: '20202020-e5f6-a7b8-c9d0-123456789012', + universalIdentifier: 'b3369d31-3856-4a7a-b007-ee353918127c', }, }, indexes: { messageChannelMessageAssociationIdIndex: { - universalIdentifier: '2b38d3e7-5779-4c1f-4e0f-78c9d70d1de9', + universalIdentifier: '8e6038aa-1f79-4a84-87b5-f33caa172e98', }, messageFolderIdIndex: { - universalIdentifier: '3c49e4f8-6880-4d2a-5f1a-89d0e81e2ef0', + universalIdentifier: '905299c3-ca81-435d-901c-f68b87562516', }, messageChannelMessageAssociationIdMessageFolderIdUniqueIndex: { - universalIdentifier: '4d50f5a9-7991-4e3b-6a2b-90e1f92f3f01', + universalIdentifier: 'a3de1788-5dff-4849-ac5a-0dabe5fab216', }, }, }, @@ -1036,7 +1036,7 @@ export const STANDARD_OBJECTS = { }, indexes: { connectedAccountIdIndex: { - universalIdentifier: '2b27c2d6-3558-4b0e-2c9d-56a7b58a9ab7', + universalIdentifier: 'ab09a386-4dcc-41f7-8dc6-a6071e9c64b7', }, }, }, @@ -1088,12 +1088,12 @@ export const STANDARD_OBJECTS = { universalIdentifier: '5f2d3937-bafd-4d71-b4cb-b34037efd2e1', }, messageChannelMessageAssociationMessageFolders: { - universalIdentifier: '20202020-f6a7-b8c9-d0e1-234567890123', + universalIdentifier: 'cce5ce1e-31d0-42e6-83cd-90059244a484', }, }, indexes: { messageChannelIdIndex: { - universalIdentifier: '3c38d3e7-4669-4c1f-3d0e-67b8c69b0bc8', + universalIdentifier: '6217f2a5-28ac-4b88-8a2a-45eee4580e57', }, }, }, @@ -1143,13 +1143,13 @@ export const STANDARD_OBJECTS = { }, indexes: { messageIdIndex: { - universalIdentifier: '4d49e4f8-5770-4d2a-4e1f-78c9d70c1cd9', + universalIdentifier: 'ab0863ba-f95e-493c-b86c-56e1bc7e5bc2', }, personIdIndex: { - universalIdentifier: '5e50f5a9-6881-4e3b-5f2a-89d0e81d2de0', + universalIdentifier: 'df805c2e-3bfe-4d51-8309-75e5eb4052fe', }, workspaceMemberIdIndex: { - universalIdentifier: '6f61a6b0-7992-4f4c-6a3b-90e1f92e3ef1', + universalIdentifier: 'ce1e3a9e-afe9-439d-abb7-6cc98a6fa405', }, }, }, @@ -1248,7 +1248,7 @@ export const STANDARD_OBJECTS = { }, indexes: { messageThreadIdIndex: { - universalIdentifier: '7072b7c1-8003-4a5d-7b4c-01f2a03f4f02', + universalIdentifier: '7a05b45e-7aa6-4a7e-9bbc-299cbed53c96', }, }, views: { @@ -1415,13 +1415,13 @@ export const STANDARD_OBJECTS = { universalIdentifier: '9294d9e3-0225-4c7f-9d6e-23b4c25b6b24', }, personIdIndex: { - universalIdentifier: '0305e0f4-1336-4d8a-0e7f-34c5d36c7c35', + universalIdentifier: '7c069dc0-e83b-4cd5-aaa2-cac7f3e00d80', }, companyIdIndex: { - universalIdentifier: '1416f1a5-2447-4e9b-1f8a-45d6e47d8d46', + universalIdentifier: '2d83909a-a383-4e82-b00a-8b7739f3f906', }, opportunityIdIndex: { - universalIdentifier: '2527a2b6-3558-4f0c-2a9b-56e7f58e9e57', + universalIdentifier: '0d1a59b4-cc87-4b7d-804a-656e8504f371', }, }, views: { @@ -1501,16 +1501,16 @@ export const STANDARD_OBJECTS = { }, indexes: { pointOfContactIdIndex: { - universalIdentifier: '3638b3c7-4669-4a1d-3b0c-67f8a69f0f68', + universalIdentifier: 'b8c2a673-a981-4357-a43d-313a358e4daa', }, companyIdIndex: { - universalIdentifier: '4749c4d8-5770-4b2e-4c1d-78a9b70a1a79', + universalIdentifier: 'e161072d-37b1-477a-b944-ef0d65289574', }, stageIndex: { - universalIdentifier: '5850d5e9-6881-4c3f-5d2e-89b0c81b2b80', + universalIdentifier: 'ae60d580-b562-44f2-a24d-7b8040063f83', }, searchVectorGinIndex: { - universalIdentifier: '6961e6f0-7992-4d4a-6e3f-90c1d92c3c91', + universalIdentifier: 'f53fdd28-a26b-47ba-81b5-6813ad622720', }, }, views: { @@ -1685,7 +1685,7 @@ export const STANDARD_OBJECTS = { }, indexes: { companyIdIndex: { - universalIdentifier: '7072f7a1-8003-4e5b-7f4a-01d2e03d4d02', + universalIdentifier: '8a265a5c-d3ae-47dc-bdf9-b42cfa2ba639', }, emailsUniqueIndex: { universalIdentifier: '8183a8b2-9114-4f6c-8a5b-12e3f14e5e13', @@ -1821,10 +1821,10 @@ export const STANDARD_OBJECTS = { }, indexes: { assigneeIdIndex: { - universalIdentifier: '0305c0d4-1336-4b8e-0c7d-34a5b36a7a35', + universalIdentifier: 'f48fa3b1-0cec-44da-a9e5-f8a5e766637e', }, searchVectorGinIndex: { - universalIdentifier: '1416d1e5-2447-4c9f-1d8e-45b6c47b8b46', + universalIdentifier: 'a86b32b3-01d3-4302-a152-8b7f247db7b4', }, }, views: { @@ -2010,16 +2010,16 @@ export const STANDARD_OBJECTS = { }, indexes: { taskIdIndex: { - universalIdentifier: '2527e2f6-3558-4d0a-2e9f-56c7d58c9c57', + universalIdentifier: 'c882f7a4-b025-4d32-aa26-5ef2595bdbf9', }, personIdIndex: { - universalIdentifier: '3638f3a7-4669-4e1b-3f0a-67d8e69d0d68', + universalIdentifier: 'b7d305d1-6fae-4ed6-9bdc-354fe9032c0e', }, companyIdIndex: { - universalIdentifier: '4749a4b8-5770-4f2c-4a1b-78e9f70e1e79', + universalIdentifier: 'c0af54c7-751b-4bb2-b102-677cc4e47402', }, opportunityIdIndex: { - universalIdentifier: '5850b5c9-6881-4a3d-5b2c-89f0a81f2f80', + universalIdentifier: '6942e0ba-90f6-4c33-bf40-7f00b1ec35ab', }, }, views: { @@ -2122,10 +2122,10 @@ export const STANDARD_OBJECTS = { }, indexes: { workspaceMemberIdIndex: { - universalIdentifier: '6961c6d0-7992-4b4e-6c3d-90a1b92a3a91', + universalIdentifier: '5e0b2391-85ca-4a66-aef4-52d74245bec2', }, personIdIndex: { - universalIdentifier: '7072d7e1-8003-4c5f-7d4e-01b2c03b4b02', + universalIdentifier: '3e89a914-7bec-47bd-9cf9-743c6b83d001', }, companyIdIndex: { universalIdentifier: '8183e8f2-9114-4d6a-8e5f-12c3d14c5c13', @@ -2134,22 +2134,22 @@ export const STANDARD_OBJECTS = { universalIdentifier: '9294f9a3-0225-4e7b-9f6a-23d4e25d6d24', }, noteIdIndex: { - universalIdentifier: '0305a0b4-1336-4f8c-0a7b-34e5f36e7e35', + universalIdentifier: '995db1d8-0d3e-40f7-b0eb-5e6897bc9966', }, taskIdIndex: { - universalIdentifier: '1416b1c5-2447-4a9d-1b8c-45f6a47f8f46', + universalIdentifier: '609cf622-86ef-48d1-812b-e1cab610a46c', }, workflowIdIndex: { - universalIdentifier: '2527c2d6-3558-4b0e-2c9d-56a7b58a9a57', + universalIdentifier: 'd6059ec2-92b0-4cfc-9fd8-78050f03108f', }, workflowVersionIdIndex: { - universalIdentifier: '3638d3e7-4669-4c1f-3d0e-67b8c69b0b68', + universalIdentifier: 'd94329b3-5dc8-4141-ae28-31afe28f7135', }, workflowRunIdIndex: { - universalIdentifier: '4749e4f8-5770-4d2a-4e1f-78c9d70c1c79', + universalIdentifier: '1a2bd046-7c23-4e0a-9f8a-c3ca3a16d3b9', }, dashboardIdIndex: { - universalIdentifier: '5850f5a9-6881-4e3b-5f2a-89d0e81d2d80', + universalIdentifier: 'e8821da9-728d-470a-bf5b-5a981fff7880', }, }, views: { @@ -2247,7 +2247,7 @@ export const STANDARD_OBJECTS = { }, indexes: { searchVectorGinIndex: { - universalIdentifier: '6961a6b0-7992-4f4c-6a3b-90e1f92e3e91', + universalIdentifier: 'c7e64c55-eb0c-4b93-b076-5cfcf2e2e042', }, }, views: { @@ -2315,7 +2315,7 @@ export const STANDARD_OBJECTS = { }, indexes: { workflowIdIndex: { - universalIdentifier: '7072b7c1-8003-4a5d-7b4c-01f2a03f4f03', + universalIdentifier: '7331ff89-a3f9-4ac0-9fa9-0de5663ae7b2', }, }, }, @@ -2377,7 +2377,7 @@ export const STANDARD_OBJECTS = { universalIdentifier: '9294d9e3-0225-4c7f-9d6e-23b4c25b6b25', }, searchVectorGinIndex: { - universalIdentifier: '0305e0f4-1336-4d8a-0e7f-34c5d36c7c36', + universalIdentifier: 'e0ac5ad2-d0c8-4f72-b710-8e53b9dc18d9', }, }, views: { @@ -2492,10 +2492,10 @@ export const STANDARD_OBJECTS = { }, indexes: { workflowIdIndex: { - universalIdentifier: '1416f1a5-2447-4e9b-1f8a-45d6e47d8d47', + universalIdentifier: '8138c3b3-0b14-4ee1-be0e-debdde6b3219', }, searchVectorGinIndex: { - universalIdentifier: '2527a2b6-3558-4f0c-2a9b-56e7f58e9e58', + universalIdentifier: '6f3a65eb-2aee-4108-b8a0-c62da419d1dc', }, }, views: { @@ -2638,10 +2638,10 @@ export const STANDARD_OBJECTS = { }, indexes: { userEmailUniqueIndex: { - universalIdentifier: '3638b3c7-4669-4a1d-3b0c-67f8a69f0f69', + universalIdentifier: '76da5f27-523c-44b6-ad06-12954f6b949f', }, searchVectorGinIndex: { - universalIdentifier: '4749c4d8-5770-4b2e-4c1d-78a9b70a1a7a', + universalIdentifier: '8678dde9-a804-4a9e-80e3-9af35e471ec5', }, }, views: {