From 1607aebcc6d72a80bd5b7f6ccd78f4a89bee5d24 Mon Sep 17 00:00:00 2001 From: Weiko Date: Thu, 27 Nov 2025 13:43:34 +0100 Subject: [PATCH] Deprecate object metadata maps in favor of flat entities (#16080) ## Context Deprecating the old objectMetadataMap type in favour of split flat entities to match with our new caching. In the long run, trying to achieve: - Better performance through caching - Consistent data access patterns across the codebase - Reduced database queries Now that everything is based on flat entities, which are cached, we can finish the refactoring of workspace context cache which should already improve performances. Then the last step will be to consume that new cache in the new global datasource to get rid of the many workspace datasources stored in the server --- .../src/command/command.module.ts | 2 - .../__mocks__/object-metadata-item.mock.ts | 31 -- .../common-selected-fields.handler.ts | 63 ++- .../data-arg-processor/data-arg.processor.ts | 41 +- .../query-runner-args.factory.ts | 43 +- .../common-base-query-runner.service.ts | 62 ++- ...common-create-many-query-runner.service.ts | 161 +++--- .../get-conflicting-fields.util.spec.ts | 151 ++++-- .../utils/get-conflicting-fields.util.ts | 15 +- .../common-create-one-query-runner.service.ts | 24 +- ...common-delete-many-query-runner.service.ts | 52 +- .../common-delete-one-query-runner.service.ts | 18 +- ...ommon-destroy-many-query-runner.service.ts | 58 +- ...common-destroy-one-query-runner.service.ts | 14 +- ...on-find-duplicates-query-runner.service.ts | 56 +- .../common-find-many-query-runner.service.ts | 48 +- .../common-find-one-query-runner.service.ts | 39 +- .../common-group-by-query-runner.service.ts | 73 ++- .../common-merge-many-query-runner.service.ts | 177 ++++--- ...ommon-restore-many-query-runner.service.ts | 43 +- ...common-restore-one-query-runner.service.ts | 18 +- ...common-update-many-query-runner.service.ts | 54 +- .../common-update-one-query-runner.service.ts | 24 +- .../get-object-alias-for-group-by.util.ts | 6 +- .../common-result-getters.service.ts | 56 +- .../common-base-query-runner-context.type.ts | 11 +- .../__mocks__/mockPersonObjectMetadata.ts | 240 +++++---- .../graphql-query-filter-condition.parser.ts | 16 +- .../graphql-query-filter-field.parser.ts | 39 +- .../graphql-query-order.parser.ts | 95 ++-- .../parse-composite-field-for-order.util.ts | 4 +- ...or-order-by-relation-field-parsing.util.ts | 40 +- ...raphql-selected-fields-aggregate.parser.ts | 17 +- ...graphql-selected-fields-relation.parser.ts | 40 +- .../graphql-selected-fields.parser.ts | 70 +-- .../graphql-query.parser.ts | 51 +- .../resolvers/types/group-by-field.types.ts | 10 +- ...ult-with-group-by-dimension-values.util.ts | 25 +- .../utils/parse-group-by-args.util.ts | 34 +- .../parse-group-by-relation-field.util.ts | 61 ++- .../services/group-by-with-records.service.ts | 54 +- ...ct-records-to-graphql-connection.helper.ts | 122 ++--- .../process-nested-relations-v2.helper.ts | 110 ++-- .../process-nested-relations.helper.ts | 16 +- .../__tests__/build-columns-to-select.spec.ts | 423 ++++++++++----- .../utils/build-columns-to-return.ts | 20 +- .../utils/build-columns-to-select.ts | 64 +-- ...-field-metadata-from-graphql-field.util.ts | 77 +-- .../utils/get-target-object-metadata.util.ts | 13 +- .../query-runner-option.interface.ts | 12 - .../utils/handle-duplicate-key-error.util.ts | 74 +-- .../factories/create-many-resolver.factory.ts | 7 +- .../factories/create-one-resolver.factory.ts | 7 +- .../factories/delete-many-resolver.factory.ts | 7 +- .../factories/delete-one-resolver.factory.ts | 7 +- .../destroy-many-resolver.factory.ts | 7 +- .../factories/destroy-one-resolver.factory.ts | 7 +- .../find-duplicates-resolver.factory.ts | 7 +- .../factories/find-many-resolver.factory.ts | 7 +- .../factories/find-one-resolver.factory.ts | 7 +- .../factories/group-by-resolver.factory.ts | 7 +- .../factories/merge-many-resolver.factory.ts | 7 +- .../restore-many-resolver.factory.ts | 7 +- .../factories/restore-one-resolver.factory.ts | 7 +- .../factories/update-many-resolver.factory.ts | 7 +- .../factories/update-one-resolver.factory.ts | 7 +- .../workspace-resolver.factory.ts | 55 +- ...-field-metadata-gql-enum-type.generator.ts | 17 +- .../gql-type.generator.ts | 87 ++- ...etadata-create-gql-input-type.generator.ts | 47 +- ...etadata-filter-gql-input-type.generator.ts | 46 +- ...adata-group-by-gql-input-type.generator.ts | 46 +- ...bject-metadata-gql-input-type.generator.ts | 33 +- ...object-metadata-order-by-base.generator.ts | 32 +- ...adata-order-by-gql-input-type.generator.ts | 17 +- ...-with-group-by-gql-input-type.generator.ts | 80 +-- ...lation-connect-gql-input-type.generator.ts | 85 ++- ...ation-field-metadata-gql-type.generator.ts | 32 +- ...etadata-update-gql-input-type.generator.ts | 37 +- .../aggregation-type.generator.ts | 9 +- .../connection-gql-object-type.generator.ts | 30 +- .../edge-gql-object-type.generator.ts | 23 +- ...by-connection-gql-object-type.generator.ts | 23 +- ...ject-metadata-gql-object-type.generator.ts | 38 +- ...ith-relations-gql-object-type.generator.ts | 108 ++-- ...ield-metadata-gql-object-type.generator.ts | 4 +- .../root-types/mutation-type.generator.ts | 6 +- .../root-types/query-type.generator.ts | 6 +- .../root-types/root-type.generator.ts | 11 +- ...kspace-schema-builder-context.interface.ts | 11 +- .../types/schema-generation-context.type.ts | 10 + .../compute-field-input-type-options.util.ts | 8 +- ...tract-graphql-relation-field-names.util.ts | 7 +- ...le-aggregations-from-object-fields.util.ts | 4 +- ...at-fields-for-flat-object-metadata.util.ts | 14 + ...ject-contains-morph-relation-field.util.ts | 10 - .../utils/object-contains-relation-field.ts | 10 - .../workspace-graphql-schema.factory.ts | 6 +- .../api/graphql/workspace-schema.factory.ts | 57 +- .../core/handlers/rest-api-base.handler.ts | 73 ++- .../handlers/rest-api-create-many.handler.ts | 22 +- .../handlers/rest-api-create-one.handler.ts | 22 +- .../handlers/rest-api-delete-many.handler.ts | 20 +- .../handlers/rest-api-delete-one.handler.ts | 17 +- .../handlers/rest-api-destroy-many.handler.ts | 20 +- .../handlers/rest-api-destroy-one.handler.ts | 17 +- .../rest-api-find-duplicates.handler.ts | 19 +- .../handlers/rest-api-find-many.handler.ts | 19 +- .../handlers/rest-api-find-one.handler.ts | 22 +- .../handlers/rest-api-group-by.handler.ts | 32 +- .../handlers/rest-api-merge-many.handler.ts | 24 +- .../handlers/rest-api-restore-many.handler.ts | 24 +- .../handlers/rest-api-restore-one.handler.ts | 22 +- .../handlers/rest-api-update-many.handler.ts | 22 +- .../handlers/rest-api-update-one.handler.ts | 22 +- .../selected-fields-handler.ts | 77 +-- .../get-all-selectable-fields.util.spec.ts | 275 ++++++---- .../utils/get-all-selectable-fields.util.ts | 40 +- .../check-filter-enum-values.util.spec.ts | 82 --- .../check-filter-enum-values.util.ts | 43 -- .../build-duplicate-conditions.utils.spec.ts | 22 +- .../compute-cursor-arg-filter.utils.spec.ts | 190 ++++--- .../build-cursor-where-condition.utils.ts | 27 +- .../utils/build-duplicate-conditions.utils.ts | 20 +- .../utils/compute-cursor-arg-filter.utils.ts | 13 +- .../get-all-selectable-column-names.utils.ts | 10 +- .../__mocks__/mockFlatObjectMetadatas.ts | 168 ++++++ .../mockObjectMetadataItemsWithFieldMaps.ts | 482 ++++++++++------- .../created-by-from-auth-context.service.ts | 8 +- .../object-record-changed-values.spec.ts | 39 +- .../utils/object-record-changed-values.ts | 21 +- .../core-modules/open-api/open-api.module.ts | 8 +- .../open-api/open-api.service.spec.ts | 7 + .../core-modules/open-api/open-api.service.ts | 131 +++-- .../utils/__tests__/components.utils.spec.ts | 81 ++- .../open-api/utils/components.utils.ts | 97 +++- .../utils/compute-schema-tags.utils.ts | 4 +- .../open-api/utils/computeWebhooks.utils.ts | 13 +- .../utils/generate-random-field-value.util.ts | 3 +- .../core-modules/open-api/utils/path.utils.ts | 73 ++- .../open-api/utils/responses.utils.ts | 26 +- .../services/create-record.service.ts | 60 ++- .../services/delete-record.service.ts | 27 +- .../services/find-records.service.ts | 72 ++- .../services/update-record.service.ts | 41 +- .../services/upsert-record.service.ts | 58 +- .../object-metadata-for-tool-schema.type.ts | 7 + ...enerate-create-record-input-schema.util.ts | 4 +- ...enerate-update-record-input-schema.util.ts | 4 +- ...ted-columns-from-restricted-fields.util.ts | 10 +- .../zod-schemas/field-filters.zod-schema.ts | 5 +- .../zod-schemas/find-tool.zod-schema.ts | 4 +- .../record-properties.zod-schema.ts | 10 +- .../record-input-transformer.service.ts | 22 +- .../search/__tests__/search.service.spec.ts | 40 +- .../search/exceptions/search.exception.ts | 1 + .../filters/search-api-exception.filter.ts | 1 + .../core-modules/search/search.resolver.ts | 39 +- .../search/services/search.service.ts | 158 +++--- .../records-with-object-metadata-item.ts | 4 +- ...form-event-batch-to-webhook-events.spec.ts | 27 +- .../services/agent-execution.service.ts | 30 +- .../utils/is-workflow-related-object.util.ts | 8 +- .../ai/ai-tools/ai-tools.module.ts | 4 +- .../services/__tests__/tool.service.spec.ts | 18 +- .../ai/ai-tools/services/tool.service.ts | 37 +- .../field-metadata-morph-relation.service.ts | 26 +- .../field-metadata-relation.service.ts | 45 +- .../field-metadata-validation.service.ts | 13 +- .../utils/compute-column-name.util.ts | 13 +- .../utils/create-migration-actions.util.ts | 77 --- ...clude-field-from-agent-tool-schema.util.ts | 3 +- ...ld-maps-from-flat-object-metadata.util.ts} | 25 +- ...ject-metadata-item-with-field-maps.util.ts | 45 -- .../object-metadata-v2.service.ts | 30 +- ...ations-for-custom-object-relations.util.ts | 65 --- .../field-permission.service.ts | 43 +- .../object-metadata-item-with-field-maps.ts | 14 - .../types/object-metadata-maps.ts | 6 - ...ata-map-item-by-name-singular.util.spec.ts | 42 -- .../validate-field-name-availability.spec.ts | 120 ++++- .../generate-object-metadata-maps.util.ts | 60 --- ...ject-metadata-item-with-fields-map.util.ts | 17 - ...om-object-metadata-Item-with-field-maps.ts | 17 - ...t-metadata-map-item-by-name-plural.util.ts | 17 - ...metadata-map-item-by-name-singular.util.ts | 17 - .../validate-field-name-availability.utils.ts | 60 ++- ...ect-with-same-name-exists-or-throw.util.ts | 3 +- .../relation-connect-query-config.type.ts | 6 +- .../workspace-entity-manager.spec.ts | 153 ++++-- .../workspace-entity-manager.ts | 55 +- .../compute-twenty-orm-exception.ts | 4 +- .../factories/entity-schema-column.factory.ts | 16 +- .../entity-schema-relation.factory.ts | 56 +- .../factories/entity-schema.factory.ts | 34 +- .../factories/workspace-datasource.factory.ts | 48 +- .../global-workspace-datasource.ts | 5 +- .../global-workspace-orm.manager.ts | 54 +- .../workspace-internal-context.interface.ts | 10 +- .../relation-nested-queries.ts | 6 +- .../__tests__/workspace.repository.spec.ts | 64 ++- .../repository/permissions.utils.ts | 51 +- .../workspace-delete-query-builder.ts | 11 +- .../workspace-insert-query-builder.ts | 18 +- .../workspace-select-query-builder.ts | 19 +- .../workspace-soft-delete-query-builder.ts | 11 +- .../workspace-update-query-builder.ts | 42 +- .../repository/workspace.repository.ts | 21 +- .../storage/workspace-context.storage.ts | 10 +- ...elation-connect-query-configs.util.spec.ts | 498 +++++++++++++----- ...event-to-database-batch-event.util.spec.ts | 106 ++-- ...umn-name-to-field-metadata-id.util.spec.ts | 267 +++++----- ...tadata-id-to-column-names-map.util.spec.ts | 201 ++++--- ...ute-relation-connect-query-configs.util.ts | 88 +++- .../determine-schema-relation-details.util.ts | 22 +- .../twenty-orm/utils/format-data.util.ts | 32 +- .../twenty-orm/utils/format-result.util.ts | 67 ++- ...-orm-event-to-database-batch-event.util.ts | 13 +- ...t-column-name-to-field-metadata-id.util.ts | 15 +- ...get-composite-field-metadata-collection.ts | 13 +- ...ld-metadata-id-to-column-names-map.util.ts | 15 +- ...object-metadata-from-entity-target.util.ts | 26 +- ...d-metadata-for-column-name-mapping.util.ts | 32 +- .../types/workspace-context-data.type.ts | 11 - .../types/workspace-event-batch.type.ts | 4 +- .../workspace-event-emitter.ts | 4 +- .../workspace-manager.service.spec.ts | 7 + .../data/services/dev-seeder-data.service.ts | 21 +- .../services/dev-seeder-metadata.service.ts | 12 +- .../prefill-core-views.ts | 35 +- .../prefill-workflows.ts | 35 +- .../standard-objects-prefill-data.ts | 14 +- .../workspace-health-command.module.ts | 10 - .../commands/workspace-health.command.ts | 145 ----- .../fixer/abstract-workspace.fixer.ts | 68 --- .../workspace-health/fixer/index.ts | 12 - .../fixer/workspace-default-value.fixer.ts | 229 -------- .../fixer/workspace-missing-column.fixer.ts | 82 --- .../fixer/workspace-nullable.fixer.ts | 57 -- .../fixer/workspace-type.fixer.ts | 90 ---- .../workspace-health-fix-kind.interface.ts | 6 - .../workspace-health-issue.interface.ts | 104 ---- .../workspace-health-options.interface.ts | 9 - .../workspace-table-definition.interface.ts | 18 - .../services/database-structure.service.ts | 328 ------------ .../services/field-metadata-health.service.ts | 347 ------------ .../object-metadata-health.service.ts | 120 ----- .../services/workspace-fix.service.ts | 99 ---- .../workspace-health/utils/valid-name.util.ts | 3 - .../workspace-health.module.ts | 36 -- .../workspace-health.service.ts | 184 ------- .../workspace-manager.module.ts | 4 +- .../workspace-manager.service.ts | 17 +- ...workspace-sync-metadata-commands.module.ts | 4 +- .../workspace-metadata-updater.service.ts | 2 +- ...ected-account-delete-one.pre-query.hook.ts | 24 +- .../services/timeline-activity.service.ts | 28 +- .../timeline/timeline-activity.module.ts | 2 + .../workflow-common.workspace-service.ts | 76 ++- .../generate-fake-form-response.spec.ts | 26 +- .../generate-fake-object-record-event.spec.ts | 34 +- .../generate-fake-object-record.spec.ts | 31 +- .../generate-object-record-fields.spec.ts | 30 +- .../should-generate-field-fake-value.spec.ts | 59 +-- .../utils/generate-fake-form-response.ts | 35 +- .../generate-fake-object-record-event.ts | 10 +- .../utils/generate-fake-object-record.ts | 11 +- .../utils/generate-object-record-fields.ts | 121 +++-- .../utils/should-generate-field-fake-value.ts | 23 +- .../workflow-schema.workspace-service.ts | 43 +- ...rsion-step-operations.workspace-service.ts | 11 +- ...ow-database-event-trigger.listener.spec.ts | 100 ++-- ...orkflow-database-event-trigger.listener.ts | 110 ++-- ...ect-metadata-item-with-fields-maps.mock.ts | 37 -- .../group-by-resolver.integration-spec.ts | 21 +- ...ested-relation-queries.integration-spec.ts | 34 +- .../update-permissions.integration-spec.ts | 2 +- ...and-workspace-creation.integration-spec.ts | 22 +- .../agent-tool.service.integration-spec.ts | 18 +- ...ate-object-metadata-v2.integration-spec.ts | 19 +- .../rename-custom-object.integration-spec.ts | 17 +- 281 files changed, 6510 insertions(+), 6627 deletions(-) delete mode 100644 packages/twenty-server/src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface.ts create mode 100644 packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/types/schema-generation-context.type.ts create mode 100644 packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util.ts delete mode 100644 packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/object-contains-morph-relation-field.util.ts delete mode 100644 packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/object-contains-relation-field.ts delete mode 100644 packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/__tests__/check-filter-enum-values.util.spec.ts delete mode 100644 packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/check-filter-enum-values.util.ts create mode 100644 packages/twenty-server/src/engine/core-modules/__mocks__/mockFlatObjectMetadatas.ts create mode 100644 packages/twenty-server/src/engine/core-modules/record-crud/types/object-metadata-for-tool-schema.type.ts delete mode 100644 packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/create-migration-actions.util.ts rename packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/{build-field-maps-for-object.util.ts => build-field-maps-from-flat-object-metadata.util.ts} (61%) delete mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/build-object-metadata-item-with-field-maps.util.ts delete mode 100644 packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-migrations-for-custom-object-relations.util.ts delete mode 100644 packages/twenty-server/src/engine/metadata-modules/types/object-metadata-item-with-field-maps.ts delete mode 100644 packages/twenty-server/src/engine/metadata-modules/types/object-metadata-maps.ts delete mode 100644 packages/twenty-server/src/engine/metadata-modules/utils/__tests__/get-object-metadata-map-item-by-name-singular.util.spec.ts delete mode 100644 packages/twenty-server/src/engine/metadata-modules/utils/generate-object-metadata-maps.util.ts delete mode 100644 packages/twenty-server/src/engine/metadata-modules/utils/get-object-metadata-entity-from-object-metadata-item-with-fields-map.util.ts delete mode 100644 packages/twenty-server/src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps.ts delete mode 100644 packages/twenty-server/src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-plural.util.ts delete mode 100644 packages/twenty-server/src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util.ts delete mode 100644 packages/twenty-server/src/engine/workspace-cache/types/workspace-context-data.type.ts delete mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-health/commands/workspace-health-command.module.ts delete mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-health/commands/workspace-health.command.ts delete mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/abstract-workspace.fixer.ts delete mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/index.ts delete mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/workspace-default-value.fixer.ts delete mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/workspace-missing-column.fixer.ts delete mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/workspace-nullable.fixer.ts delete mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/workspace-type.fixer.ts delete mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-health/interfaces/workspace-health-fix-kind.interface.ts delete mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-health/interfaces/workspace-health-issue.interface.ts delete mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-health/interfaces/workspace-health-options.interface.ts delete mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-health/interfaces/workspace-table-definition.interface.ts delete mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-health/services/database-structure.service.ts delete mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-health/services/field-metadata-health.service.ts delete mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-health/services/object-metadata-health.service.ts delete mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-health/services/workspace-fix.service.ts delete mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-health/utils/valid-name.util.ts delete mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-health/workspace-health.module.ts delete mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-health/workspace-health.service.ts delete mode 100644 packages/twenty-server/src/utils/__test__/get-object-metadata-item-with-fields-maps.mock.ts diff --git a/packages/twenty-server/src/command/command.module.ts b/packages/twenty-server/src/command/command.module.ts index 2b76ad76e79..acb3c2084d7 100644 --- a/packages/twenty-server/src/command/command.module.ts +++ b/packages/twenty-server/src/command/command.module.ts @@ -5,7 +5,6 @@ import { DatabaseCommandModule } from 'src/database/commands/database-command.mo import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module'; import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module'; import { WorkspaceCleanerModule } from 'src/engine/workspace-manager/workspace-cleaner/workspace-cleaner.module'; -import { WorkspaceHealthCommandModule } from 'src/engine/workspace-manager/workspace-health/commands/workspace-health-command.module'; import { WorkspaceMigrationRunnerCommandsModule } from 'src/engine/workspace-manager/workspace-migration-runner/commands/workspace-migration-runner-commands.module'; import { WorkspaceSyncMetadataCommandsModule } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/workspace-sync-metadata-commands.module'; import { MessagingMessageCleanerModule } from 'src/modules/messaging/message-cleaner/messaging-message-cleaner.module'; @@ -15,7 +14,6 @@ import { MessagingMessageCleanerModule } from 'src/modules/messaging/message-cle AppModule, WorkspaceSyncMetadataCommandsModule, DatabaseCommandModule, - WorkspaceHealthCommandModule, MessagingMessageCleanerModule, WorkspaceMigrationRunnerCommandsModule, ObjectMetadataModule, diff --git a/packages/twenty-server/src/engine/api/__mocks__/object-metadata-item.mock.ts b/packages/twenty-server/src/engine/api/__mocks__/object-metadata-item.mock.ts index c6e7b08ecbf..888259737a1 100644 --- a/packages/twenty-server/src/engine/api/__mocks__/object-metadata-item.mock.ts +++ b/packages/twenty-server/src/engine/api/__mocks__/object-metadata-item.mock.ts @@ -9,8 +9,6 @@ import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfa import { type FieldMetadataComplexOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input'; import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock'; export const FIELD_LINKS_MOCK_NAME = 'fieldLinks'; @@ -463,32 +461,3 @@ export const objectMetadataItemMock: ObjectMetadataEntity = { createdAt: new Date(), updatedAt: new Date(), } as ObjectMetadataEntity; - -export const objectMetadataMapItemMock: ObjectMetadataItemWithFieldMaps = { - ...objectMetadataItemMock, - fieldsById: FIELDS_MOCK.reduce( - (acc, field) => ({ - ...acc, - [field.id]: field, - }), - {}, - ), - fieldIdByName: FIELDS_MOCK.reduce( - (acc, field) => ({ - ...acc, - [field.name]: field.id, - }), - {}, - ), - fieldIdByJoinColumnName: {}, - indexMetadatas: [], -}; -export const objectMetadataMapsMock: ObjectMetadataMaps = { - byId: { - [objectMetadataMapItemMock.id || 'mock-id']: objectMetadataMapItemMock, - }, - idByNameSingular: { - [objectMetadataMapItemMock.nameSingular]: - objectMetadataMapItemMock.id || 'mock-id', - }, -}; diff --git a/packages/twenty-server/src/engine/api/common/common-args-handlers/common-query-selected-fields/common-selected-fields.handler.ts b/packages/twenty-server/src/engine/api/common/common-args-handlers/common-query-selected-fields/common-selected-fields.handler.ts index 9c255e23f2a..2577034a8db 100644 --- a/packages/twenty-server/src/engine/api/common/common-args-handlers/common-query-selected-fields/common-selected-fields.handler.ts +++ b/packages/twenty-server/src/engine/api/common/common-args-handlers/common-query-selected-fields/common-selected-fields.handler.ts @@ -11,39 +11,43 @@ import { CommonSelectedFieldsResult } from 'src/engine/api/common/types/common-s import { getAllSelectableFields } from 'src/engine/api/rest/core/rest-to-common-args-handlers/utils/get-all-selectable-fields.util'; import { MAX_DEPTH } from 'src/engine/api/rest/input-request-parsers/constants/max-depth.constant'; import { Depth } from 'src/engine/api/rest/input-request-parsers/types/depth.type'; -import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; -import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; @Injectable() export class CommonSelectedFieldsHandler { computeFromDepth = ({ objectsPermissions, - objectMetadataMaps, - objectMetadataMapItem, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatObjectMetadata, depth, }: { objectsPermissions: ObjectsPermissions; - objectMetadataMaps: ObjectMetadataMaps; - objectMetadataMapItem: ObjectMetadataItemWithFieldMaps; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; + flatObjectMetadata: FlatObjectMetadata; depth: Depth | undefined; }): CommonSelectedFieldsResult => { const restrictedFields = - objectsPermissions[objectMetadataMapItem.id].restrictedFields; + objectsPermissions[flatObjectMetadata.id].restrictedFields; const { relations, relationsSelectFields } = this.getRelationsAndRelationsSelectFields({ - objectMetadataMaps, - objectMetadataMapItem, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatObjectMetadata, objectsPermissions, depth, }); const selectableFields = getAllSelectableFields({ restrictedFields, - objectMetadata: { - objectMetadataMapItem, - }, + flatObjectMetadata, + flatFieldMetadataMaps, }); return { @@ -57,13 +61,15 @@ export class CommonSelectedFieldsHandler { }; private getRelationsAndRelationsSelectFields({ - objectMetadataMaps, - objectMetadataMapItem, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatObjectMetadata, objectsPermissions, depth, }: { - objectMetadataMaps: ObjectMetadataMaps; - objectMetadataMapItem: ObjectMetadataItemWithFieldMaps; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; + flatObjectMetadata: FlatObjectMetadata; objectsPermissions: ObjectsPermissions; depth: Depth | undefined; }) { @@ -82,12 +88,19 @@ export class CommonSelectedFieldsHandler { | { [key: string]: boolean | { [key: string]: boolean } }; } = {}; - for (const field of Object.values(objectMetadataMapItem.fieldsById)) { - if (!isFieldMetadataEntityOfType(field, FieldMetadataType.RELATION)) + for (const fieldId of flatObjectMetadata.fieldMetadataIds) { + const field = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityMaps: flatFieldMetadataMaps, + flatEntityId: fieldId, + }); + + if (!isFlatFieldMetadataOfType(field, FieldMetadataType.RELATION)) continue; + if (!field.relationTargetObjectMetadataId) continue; + const relationTargetObjectMetadata = - objectMetadataMaps.byId[field.relationTargetObjectMetadataId]; + flatObjectMetadataMaps.byId[field.relationTargetObjectMetadataId]; if (!isDefined(relationTargetObjectMetadata)) { throw new CommonQueryRunnerException( @@ -98,9 +111,8 @@ export class CommonSelectedFieldsHandler { const relationFieldSelectFields = getAllSelectableFields({ restrictedFields: objectsPermissions[relationTargetObjectMetadata.id].restrictedFields, - objectMetadata: { - objectMetadataMapItem: relationTargetObjectMetadata, - }, + flatObjectMetadata: relationTargetObjectMetadata, + flatFieldMetadataMaps, }); if (Object.keys(relationFieldSelectFields).length === 0) continue; @@ -113,8 +125,9 @@ export class CommonSelectedFieldsHandler { relations: depth2Relations, relationsSelectFields: depth2RelationsSelectFields, } = this.getRelationsAndRelationsSelectFields({ - objectMetadataMaps, - objectMetadataMapItem: relationTargetObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatObjectMetadata: relationTargetObjectMetadata, objectsPermissions, depth: 1, }) as { diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/data-arg.processor.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/data-arg.processor.ts index d2661301b54..b2892621004 100644 --- a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/data-arg.processor.ts +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/data-arg.processor.ts @@ -52,8 +52,10 @@ import { transformLinksValue } from 'src/engine/core-modules/record-transformer/ import { transformPhonesValue } from 'src/engine/core-modules/record-transformer/utils/transform-phones-value.util'; import { transformRichTextV2Value } from 'src/engine/core-modules/record-transformer/utils/transform-rich-text-v2.util'; import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception'; -import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; +import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; @Injectable() export class DataArgProcessor { @@ -65,12 +67,14 @@ export class DataArgProcessor { async process({ partialRecordInputs, authContext, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, shouldBackfillPositionIfUndefined = true, }: { partialRecordInputs: Partial[] | undefined; authContext: AuthContext; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; + flatObjectMetadata: FlatObjectMetadata; + flatFieldMetadataMaps: FlatEntityMaps; shouldBackfillPositionIfUndefined?: boolean; }): Promise[]> { if (!isDefined(partialRecordInputs)) { @@ -87,6 +91,12 @@ export class DataArgProcessor { workspace.id, ); + const { fieldIdByName, fieldIdByJoinColumnName } = + buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + flatObjectMetadata, + ); + const processedRecords: Partial[] = []; for (const record of partialRecordInputs) { @@ -94,18 +104,23 @@ export class DataArgProcessor { for (const [key, value] of Object.entries(record)) { const fieldMetadataId = - objectMetadataItemWithFieldMaps.fieldIdByName[key] || - objectMetadataItemWithFieldMaps.fieldIdByJoinColumnName[key]; + fieldIdByName[key] || fieldIdByJoinColumnName[key]; if (!isDefined(fieldMetadataId)) { throw new CommonQueryRunnerException( - `Object ${objectMetadataItemWithFieldMaps.nameSingular} doesn't have any "${key}" field.`, + `Object ${flatObjectMetadata.nameSingular} doesn't have any "${key}" field.`, CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, ); } - const fieldMetadata = - objectMetadataItemWithFieldMaps.fieldsById[fieldMetadataId]; + const fieldMetadata = flatFieldMetadataMaps.byId[fieldMetadataId]; + + if (!fieldMetadata) { + throw new CommonQueryRunnerException( + `Field metadata not found for field ${key}`, + CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + ); + } if ( !isDefined(fieldMetadata.defaultValue) && @@ -137,9 +152,9 @@ export class DataArgProcessor { partialRecordInputs: processedRecords, workspaceId: workspace.id, objectMetadata: { - isCustom: objectMetadataItemWithFieldMaps.isCustom, - nameSingular: objectMetadataItemWithFieldMaps.nameSingular, - fieldIdByName: objectMetadataItemWithFieldMaps.fieldIdByName, + isCustom: flatObjectMetadata.isCustom, + nameSingular: flatObjectMetadata.nameSingular, + fieldIdByName, }, shouldBackfillPositionIfUndefined, }); @@ -148,7 +163,7 @@ export class DataArgProcessor { } private async processField( - fieldMetadata: FieldMetadataEntity, + fieldMetadata: FlatFieldMetadata, key: string, value: unknown, isNullEquivalenceEnabled: boolean, diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/query-runner-args.factory.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/query-runner-args.factory.ts index 2af321989f4..084a0139391 100644 --- a/packages/twenty-server/src/engine/api/common/common-args-processors/query-runner-args.factory.ts +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/query-runner-args.factory.ts @@ -5,15 +5,15 @@ import { isDefined } from 'twenty-shared/utils'; import { type ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; -import { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service'; import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service'; -import { type FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; +import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; @Injectable() export class QueryRunnerArgsFactory { constructor( - private readonly recordPositionService: RecordPositionService, private readonly recordInputTransformerService: RecordInputTransformerService, ) {} @@ -21,12 +21,18 @@ export class QueryRunnerArgsFactory { T extends ObjectRecordFilter | undefined, >( filter: T, - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, ): T { if (!isDefined(filter)) { return filter; } + const { fieldIdByName } = buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + flatObjectMetadata, + ); + const overrideFilter = (filterObject: ObjectRecordFilter) => { return Object.entries(filterObject).reduce((acc, [key, value]) => { if (key === 'and' || key === 'or') { @@ -42,7 +48,8 @@ export class QueryRunnerArgsFactory { acc[key] = this.transformFilterValueByType( key, value, - objectMetadataItemWithFieldMaps, + fieldIdByName, + flatFieldMetadataMaps, ); } @@ -57,11 +64,13 @@ export class QueryRunnerArgsFactory { key: string, // eslint-disable-next-line @typescript-eslint/no-explicit-any value: any, - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, + fieldIdByName: Record, + flatFieldMetadataMaps: FlatEntityMaps, ) { - const fieldMetadataId = objectMetadataItemWithFieldMaps.fieldIdByName[key]; - const fieldMetadata = - objectMetadataItemWithFieldMaps.fieldsById[fieldMetadataId]; + const fieldMetadataId = fieldIdByName[key]; + const fieldMetadata = fieldMetadataId + ? flatFieldMetadataMaps.byId[fieldMetadataId] + : undefined; if (!fieldMetadata) { return value; @@ -90,18 +99,22 @@ export class QueryRunnerArgsFactory { key: string, // eslint-disable-next-line @typescript-eslint/no-explicit-any value: any, - fieldMetadataMapByName: FieldMetadataMap, - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, + fieldIdByName: Record, + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, ) { - const fieldMetadata = fieldMetadataMapByName[key]; + const fieldMetadata = flatFieldMetadataMaps.byId[fieldIdByName[key]]; if (!fieldMetadata) { return value; } - return this.recordInputTransformerService.process({ + const processed = await this.recordInputTransformerService.process({ recordInput: { [key]: value }, - objectMetadataMapItem: objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, }); + + return processed[key] ?? value; } } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts index dc28fdffe25..d84d3c60632 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts @@ -36,6 +36,9 @@ import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service' import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type'; import { ThrottlerService } from 'src/engine/core-modules/throttler/throttler.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; +import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { type PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; import { PermissionsException, @@ -43,8 +46,6 @@ import { PermissionsExceptionMessage, } from 'src/engine/metadata-modules/permissions/permissions.exception'; import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service'; -import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; @@ -93,8 +94,12 @@ export abstract class CommonBaseQueryRunnerService< args: CommonInput, queryRunnerContext: CommonBaseQueryRunnerContext, ): Promise { - const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } = - queryRunnerContext; + const { + authContext, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + } = queryRunnerContext; if (!isWorkspaceAuthContext(authContext)) { throw new CommonQueryRunnerException( @@ -107,7 +112,7 @@ export abstract class CommonBaseQueryRunnerService< await this.validate(args, queryRunnerContext); - if (objectMetadataItemWithFieldMaps.isSystem === true) { + if (flatObjectMetadata.isSystem === true) { await this.validateSettingsPermissionsOnObjectOrThrow( authContext, queryRunnerContext, @@ -115,8 +120,9 @@ export abstract class CommonBaseQueryRunnerService< } const commonQueryParser = new GraphqlQueryParser( - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, ); const processedArgs = await this.processArgs( @@ -172,8 +178,9 @@ export abstract class CommonBaseQueryRunnerService< protected abstract processQueryResult( queryResult: Output, - objectMetadataItemId: string, - objectMetadataMaps: ObjectMetadataMaps, + flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, authContext: WorkspaceAuthContext, ): Promise; @@ -187,14 +194,14 @@ export abstract class CommonBaseQueryRunnerService< args.selectedFields, ); - const { authContext, objectMetadataItemWithFieldMaps } = queryRunnerContext; + const { authContext, flatObjectMetadata } = queryRunnerContext; const computedArgs = await this.computeArgs(args, queryRunnerContext); const hookedArgs = (await this.workspaceQueryHookService.executePreQueryHooks( authContext, - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, operationName, computedArgs as WorkspacePreQueryHookPayload, )) as CommonInput; @@ -231,9 +238,9 @@ export abstract class CommonBaseQueryRunnerService< results, operationName: this.operationName, authContext, - objectMetadataItemWithFieldMaps: - queryRunnerContext.objectMetadataItemWithFieldMaps, - objectMetadataMaps: queryRunnerContext.objectMetadataMaps, + flatObjectMetadata: queryRunnerContext.flatObjectMetadata, + flatObjectMetadataMaps: queryRunnerContext.flatObjectMetadataMaps, + flatFieldMetadataMaps: queryRunnerContext.flatFieldMetadataMaps, }); } @@ -241,25 +248,28 @@ export abstract class CommonBaseQueryRunnerService< results, operationName, authContext, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }: { results: Output; operationName: CommonQueryNames; authContext: WorkspaceAuthContext; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; - objectMetadataMaps: ObjectMetadataMaps; + flatObjectMetadata: FlatObjectMetadata; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; }): Promise { const resultWithGetters = await this.processQueryResult( results, - objectMetadataItemWithFieldMaps.id, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext, ); await this.workspaceQueryHookService.executePostQueryHooks( authContext, - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, operationName, resultWithGetters as QueryResultFieldValue, ); @@ -271,18 +281,18 @@ export abstract class CommonBaseQueryRunnerService< authContext: WorkspaceAuthContext, queryRunnerContext: CommonBaseQueryRunnerContext, ) { - const { objectMetadataItemWithFieldMaps } = queryRunnerContext; + const { flatObjectMetadata } = queryRunnerContext; const workspace = authContext.workspace; if ( Object.keys(OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS).includes( - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, ) ) { const permissionRequired: PermissionFlagType = OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS[ - objectMetadataItemWithFieldMaps.nameSingular as keyof typeof OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS + flatObjectMetadata.nameSingular as keyof typeof OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS ]; const userHasPermission = @@ -368,7 +378,7 @@ export abstract class CommonBaseQueryRunnerService< const rolePermissionConfig = { unionOf: [roleId] }; const repository = workspaceDataSource.getRepository( - queryRunnerContext.objectMetadataItemWithFieldMaps.nameSingular, + queryRunnerContext.flatObjectMetadata.nameSingular, rolePermissionConfig, authContext, ); @@ -397,7 +407,7 @@ export abstract class CommonBaseQueryRunnerService< const repository = await this.globalWorkspaceOrmManager.getRepository( workspaceId, - queryRunnerContext.objectMetadataItemWithFieldMaps.nameSingular, + queryRunnerContext.flatObjectMetadata.nameSingular, rolePermissionConfig, ); diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-many-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-many-query-runner.service.ts index cd6f733044b..e0e5a41e1ac 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-many-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-many-query-runner.service.ts @@ -31,9 +31,11 @@ import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runne import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; import { getAllSelectableColumnNames } from 'src/engine/api/utils/get-all-selectable-column-names.utils'; import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; -import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; import { RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; @@ -62,22 +64,25 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer repository, authContext, rolePermissionConfig, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, workspaceDataSource, } = queryRunnerContext; const objectRecords = await this.insertOrUpsertRecords({ repository, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, args, }); const upsertedRecords = await this.fetchUpsertedRecords({ objectRecords, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, repository, selectedFieldsResult: args.selectedFieldsResult, }); @@ -85,8 +90,9 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer await this.processNestedRelationsIfNeeded({ args, records: upsertedRecords, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext, workspaceDataSource, rolePermissionConfig, @@ -98,16 +104,18 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer private async processNestedRelationsIfNeeded({ args, records, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext, workspaceDataSource, rolePermissionConfig, }: { args: CommonExtendedInput; records: ObjectRecord[]; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; - objectMetadataMaps: ObjectMetadataMaps; + flatObjectMetadata: FlatObjectMetadata; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; authContext: AuthContext; workspaceDataSource: WorkspaceDataSource; rolePermissionConfig?: RolePermissionConfig; @@ -117,8 +125,9 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer } await this.processNestedRelationsHelper.processNestedRelations({ - objectMetadataMaps, - parentObjectMetadataItem: objectMetadataItemWithFieldMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + parentObjectMetadataItem: flatObjectMetadata, parentObjectRecords: records, relations: args.selectedFieldsResult.relations as Record< string, @@ -136,14 +145,16 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer args: CommonInput, queryRunnerContext: CommonBaseQueryRunnerContext, ): Promise> { - const { authContext, objectMetadataItemWithFieldMaps } = queryRunnerContext; + const { authContext, flatObjectMetadata, flatFieldMetadataMaps } = + queryRunnerContext; return { ...args, data: await this.dataArgProcessor.process({ partialRecordInputs: args.data, authContext, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, }), }; } @@ -152,9 +163,9 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer args: CommonInput, queryRunnerContext: CommonBaseQueryRunnerContext, ): Promise { - const { objectMetadataItemWithFieldMaps } = queryRunnerContext; + const { flatObjectMetadata } = queryRunnerContext; - assertMutationNotOnRemoteObject(objectMetadataItemWithFieldMaps); + assertMutationNotOnRemoteObject(flatObjectMetadata); args.data.forEach((record) => { if (record?.id) { @@ -165,13 +176,15 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer private async insertOrUpsertRecords({ repository, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, args, }: { repository: WorkspaceRepository; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; - objectMetadataMaps: ObjectMetadataMaps; + flatObjectMetadata: FlatObjectMetadata; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; args: CommonExtendedInput; }): Promise { const { selectedFieldsResult } = args; @@ -180,8 +193,9 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer const selectedColumns = buildColumnsToReturn({ select: selectedFieldsResult.select, relations: selectedFieldsResult.relations, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); return await repository.insert(args.data, undefined, selectedColumns); @@ -189,8 +203,9 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer return this.performUpsertOperation({ repository, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, args, selectedFieldsResult, }); @@ -198,23 +213,27 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer private async performUpsertOperation({ repository, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, args, selectedFieldsResult, }: { repository: WorkspaceRepository; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; - objectMetadataMaps: ObjectMetadataMaps; + flatObjectMetadata: FlatObjectMetadata; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; args: CreateManyQueryArgs; selectedFieldsResult: CommonSelectedFieldsResult; }): Promise { const conflictingFields = getConflictingFields( - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ); const existingRecords = await this.findExistingRecords({ repository, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, args, conflictingFields, }); @@ -234,15 +253,17 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer const columnsToReturn = buildColumnsToReturn({ select: selectedFieldsResult.select, relations: selectedFieldsResult.relations, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); if (recordsToUpdate.length > 0) { await this.processRecordsToUpdate({ partialRecordsToUpdate: recordsToUpdate, repository, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, result, columnsToReturn, }); @@ -260,12 +281,14 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer private async findExistingRecords({ repository, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, args, conflictingFields, }: { repository: WorkspaceRepository; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; + flatObjectMetadata: FlatObjectMetadata; + flatFieldMetadataMaps: FlatEntityMaps; args: CreateManyQueryArgs; conflictingFields: { baseField: string; @@ -274,7 +297,7 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer }[]; }): Promise { const queryBuilder = repository.createQueryBuilder( - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, ); const whereConditions = buildWhereConditions(args.data, conflictingFields); @@ -284,13 +307,14 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer }); const restrictedFields = - repository.objectRecordsPermissions?.[objectMetadataItemWithFieldMaps.id] + repository.objectRecordsPermissions?.[flatObjectMetadata.id] ?.restrictedFields; const selectOptions = getAllSelectableColumnNames({ restrictedFields: restrictedFields ?? {}, objectMetadata: { - objectMetadataMapItem: objectMetadataItemWithFieldMaps, + objectMetadataMapItem: flatObjectMetadata, + flatFieldMetadataMaps, }, }); @@ -305,19 +329,25 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer private async processRecordsToUpdate({ partialRecordsToUpdate, repository, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, result, columnsToReturn, }: { partialRecordsToUpdate: PartialObjectRecordWithId[]; repository: WorkspaceRepository; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; + flatObjectMetadata: FlatObjectMetadata; + flatFieldMetadataMaps: FlatEntityMaps; result: InsertResult; columnsToReturn: string[]; }): Promise { const partialRecordsToUpdateWithoutCreatedByUpdate = partialRecordsToUpdate.map((record) => - this.getRecordWithoutCreatedBy(record, objectMetadataItemWithFieldMaps), + this.getRecordWithoutCreatedBy( + record, + flatObjectMetadata, + flatFieldMetadataMaps, + ), ); const savedRecords = await repository.updateMany( @@ -363,26 +393,29 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer private async fetchUpsertedRecords({ objectRecords, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, repository, selectedFieldsResult, }: { objectRecords: InsertResult; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; - objectMetadataMaps: ObjectMetadataMaps; + flatObjectMetadata: FlatObjectMetadata; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; repository: WorkspaceRepository; selectedFieldsResult: CommonSelectedFieldsResult; }): Promise { const queryBuilder = repository.createQueryBuilder( - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, ); const columnsToSelect = buildColumnsToSelect({ select: selectedFieldsResult.select, relations: selectedFieldsResult.relations, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); const upsertedRecords = await queryBuilder @@ -401,32 +434,38 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer async processQueryResult( queryResult: ObjectRecord[], - objectMetadataItemId: string, - objectMetadataMaps: ObjectMetadataMaps, + flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, authContext: WorkspaceAuthContext, ): Promise { return await this.commonResultGettersService.processRecordArray( queryResult, - objectMetadataItemId, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext.workspace.id, ); } private getRecordWithoutCreatedBy( record: PartialObjectRecordWithId, - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, ): Omit { let recordWithoutCreatedByUpdate = record; - const createdByFieldMetadataId = - objectMetadataItemWithFieldMaps.fieldIdByName['createdBy']; + const { fieldIdByName } = buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + flatObjectMetadata, + ); + const createdByFieldMetadata = - objectMetadataItemWithFieldMaps.fieldsById[createdByFieldMetadataId]; + flatFieldMetadataMaps.byId[fieldIdByName['createdBy']]; if (!isDefined(createdByFieldMetadata)) { throw new CommonQueryRunnerException( - `Missing createdBy field metadata for object ${objectMetadataItemWithFieldMaps.nameSingular}`, + `Missing createdBy field metadata for object ${flatObjectMetadata.nameSingular}`, CommonQueryRunnerExceptionCode.MISSING_SYSTEM_FIELD, ); } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-many-query-runner/utils/__tests__/get-conflicting-fields.util.spec.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-many-query-runner/utils/__tests__/get-conflicting-fields.util.spec.ts index ce8758e9a84..4140dbe79ce 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-many-query-runner/utils/__tests__/get-conflicting-fields.util.spec.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-many-query-runner/utils/__tests__/get-conflicting-fields.util.spec.ts @@ -1,87 +1,128 @@ import { FieldMetadataType } from 'twenty-shared/types'; -import { objectMetadataMapItemMock } from 'src/engine/api/__mocks__/object-metadata-item.mock'; import { getConflictingFields } from 'src/engine/api/common/common-query-runners/common-create-many-query-runner/utils/get-conflicting-fields.util'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; describe('getConflictingFields', () => { const workspaceId = 'workspaceId'; const objectMetadataId = 'objectMetadataId'; - const idField = getMockFieldMetadataEntity({ - workspaceId, - objectMetadataId, + const createMockField = ( + overrides: Partial & { + id: string; + name: string; + type: FieldMetadataType; + }, + ): FlatFieldMetadata => + ({ + workspaceId, + objectMetadataId, + isNullable: false, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), + universalIdentifier: overrides.id, + viewFieldIds: [], + viewFilterIds: [], + viewGroupIds: [], + kanbanAggregateOperationViewIds: [], + calendarViewIds: [], + applicationId: null, + label: overrides.name, + ...overrides, + }) as FlatFieldMetadata; + + const idField = createMockField({ id: 'id-field-id', name: 'id', type: FieldMetadataType.UUID, isUnique: true, }); - const uniqueTextField = getMockFieldMetadataEntity({ - workspaceId, - objectMetadataId, + const uniqueTextField = createMockField({ id: 'unique-text-id', name: 'uniqueText', type: FieldMetadataType.TEXT, isUnique: true, }); - const emailsUniqueField = getMockFieldMetadataEntity({ - workspaceId, - objectMetadataId, + const emailsUniqueField = createMockField({ id: 'emails-unique-id', name: 'emailsField', type: FieldMetadataType.EMAILS, isUnique: true, }); - const phonesNotUniqueField = getMockFieldMetadataEntity({ - workspaceId, - objectMetadataId, + const phonesNotUniqueField = createMockField({ id: 'phones-not-unique-id', name: 'phonesField', type: FieldMetadataType.PHONES, isUnique: false, }); - const addressUniqueFieldNoIncludedProp = getMockFieldMetadataEntity({ - workspaceId, - objectMetadataId, + const addressUniqueFieldNoIncludedProp = createMockField({ id: 'address-unique-id', name: 'addressField', type: FieldMetadataType.ADDRESS, isUnique: true, }); - const buildObjectMetadataWithFields = ( - fields: (typeof idField)[], - ): ObjectMetadataItemWithFieldMaps => { - const fieldsById = fields.reduce>( + const buildFlatObjectMetadata = ( + fields: FlatFieldMetadata[], + ): FlatObjectMetadata => + ({ + id: objectMetadataId, + workspaceId, + nameSingular: 'testObject', + namePlural: 'testObjects', + labelSingular: 'Test Object', + labelPlural: 'Test Objects', + isCustom: false, + isRemote: false, + isActive: true, + isSystem: false, + createdAt: new Date(), + updatedAt: new Date(), + universalIdentifier: objectMetadataId, + fieldMetadataIds: fields.map((f) => f.id), + indexMetadataIds: [], + viewIds: [], + applicationId: null, + }) as unknown as FlatObjectMetadata; + + const buildFlatFieldMetadataMaps = ( + fields: FlatFieldMetadata[], + ): FlatEntityMaps => ({ + byId: fields.reduce( (acc, field) => { acc[field.id] = field; return acc; }, - {}, - ); + {} as Record, + ), + idByUniversalIdentifier: fields.reduce( + (acc, field) => { + acc[field.universalIdentifier] = field.id; - return { - ...objectMetadataMapItemMock, - fieldsById, - fieldIdByName: Object.fromEntries( - Object.values(fieldsById).map((f) => [f.name, f.id]), - ), - } as ObjectMetadataItemWithFieldMaps; - }; + return acc; + }, + {} as Record, + ), + universalIdentifiersByApplicationId: {}, + }); it('returns id and unique non-composite fields as conflicts', () => { - const objectMetadata = buildObjectMetadataWithFields([ - idField, - uniqueTextField, - ]); + const fields = [idField, uniqueTextField]; + const flatObjectMetadata = buildFlatObjectMetadata(fields); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields); - const result = getConflictingFields(objectMetadata); + const result = getConflictingFields( + flatObjectMetadata, + flatFieldMetadataMaps, + ); expect(result).toEqual( expect.arrayContaining([ @@ -96,12 +137,14 @@ describe('getConflictingFields', () => { }); it('returns composite field with included unique property using full path and computed column', () => { - const objectMetadata = buildObjectMetadataWithFields([ - idField, - emailsUniqueField, - ]); + const fields = [idField, emailsUniqueField]; + const flatObjectMetadata = buildFlatObjectMetadata(fields); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields); - const result = getConflictingFields(objectMetadata); + const result = getConflictingFields( + flatObjectMetadata, + flatFieldMetadataMaps, + ); expect(result).toEqual( expect.arrayContaining([ @@ -116,23 +159,27 @@ describe('getConflictingFields', () => { }); it('does not include composite fields without included unique property', () => { - const objectMetadata = buildObjectMetadataWithFields([ - idField, - addressUniqueFieldNoIncludedProp, - ]); + const fields = [idField, addressUniqueFieldNoIncludedProp]; + const flatObjectMetadata = buildFlatObjectMetadata(fields); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields); - const result = getConflictingFields(objectMetadata); + const result = getConflictingFields( + flatObjectMetadata, + flatFieldMetadataMaps, + ); expect(result).toEqual([{ baseField: 'id', fullPath: 'id', column: 'id' }]); }); it('ignores non-unique fields', () => { - const objectMetadata = buildObjectMetadataWithFields([ - idField, - phonesNotUniqueField, - ]); + const fields = [idField, phonesNotUniqueField]; + const flatObjectMetadata = buildFlatObjectMetadata(fields); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields); - const result = getConflictingFields(objectMetadata); + const result = getConflictingFields( + flatObjectMetadata, + flatFieldMetadataMaps, + ); expect(result).toEqual([{ baseField: 'id', fullPath: 'id', column: 'id' }]); }); diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-many-query-runner/utils/get-conflicting-fields.util.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-many-query-runner/utils/get-conflicting-fields.util.ts index 2657363e547..4207ef12da4 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-many-query-runner/utils/get-conflicting-fields.util.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-many-query-runner/utils/get-conflicting-fields.util.ts @@ -1,16 +1,23 @@ -import { capitalize } from 'twenty-shared/utils'; import { compositeTypeDefinitions } from 'twenty-shared/types'; +import { capitalize } from 'twenty-shared/utils'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; export const getConflictingFields = ( - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, ): { baseField: string; fullPath: string; column: string; }[] => { - return Object.values(objectMetadataItemWithFieldMaps.fieldsById) + return getFlatFieldsFromFlatObjectMetadata( + flatObjectMetadata, + flatFieldMetadataMaps, + ) .filter((field) => field.isUnique || field.name === 'id') .flatMap((field) => { const compositeType = compositeTypeDefinitions.get(field.type); diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-one-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-one-query-runner.service.ts index 635fad0b52c..ad30ae45720 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-one-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-one-query-runner.service.ts @@ -16,8 +16,10 @@ import { CreateOneQueryArgs, } from 'src/engine/api/common/types/common-query-args.type'; import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; +import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; @Injectable() export class CommonCreateOneQueryRunnerService extends CommonBaseQueryRunnerService< @@ -51,12 +53,14 @@ export class CommonCreateOneQueryRunnerService extends CommonBaseQueryRunnerServ args: CommonInput, queryRunnerContext: CommonBaseQueryRunnerContext, ): Promise> { - const { authContext, objectMetadataItemWithFieldMaps } = queryRunnerContext; + const { authContext, flatObjectMetadata, flatFieldMetadataMaps } = + queryRunnerContext; const coercedData = await this.dataArgProcessor.process({ partialRecordInputs: [args.data], authContext, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, }); return { @@ -67,14 +71,16 @@ export class CommonCreateOneQueryRunnerService extends CommonBaseQueryRunnerServ async processQueryResult( queryResult: ObjectRecord, - objectMetadataItemId: string, - objectMetadataMaps: ObjectMetadataMaps, + flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, authContext: WorkspaceAuthContext, ): Promise { return this.commonResultGettersService.processRecord( queryResult, - objectMetadataItemId, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext.workspace.id, ); } @@ -83,9 +89,9 @@ export class CommonCreateOneQueryRunnerService extends CommonBaseQueryRunnerServ args: CommonInput, queryRunnerContext: CommonBaseQueryRunnerContext, ): Promise { - const { objectMetadataItemWithFieldMaps } = queryRunnerContext; + const { flatObjectMetadata } = queryRunnerContext; - assertMutationNotOnRemoteObject(objectMetadataItemWithFieldMaps); + assertMutationNotOnRemoteObject(flatObjectMetadata); if (args.data?.id) { assertIsValidUuid(args.data.id); diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-delete-many-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-delete-many-query-runner.service.ts index b546793c94b..f3a872e1fed 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-delete-many-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-delete-many-query-runner.service.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; -import { isDefined } from 'class-validator'; import { QUERY_MAX_RECORDS } from 'twenty-shared/constants'; import { ObjectRecord } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; import { FindOptionsRelations, ObjectLiteral } from 'typeorm'; import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface'; @@ -22,8 +22,10 @@ import { } from 'src/engine/api/common/types/common-query-args.type'; import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return'; import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; +import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; @Injectable() export class CommonDeleteManyQueryRunnerService extends CommonBaseQueryRunnerService< @@ -41,26 +43,28 @@ export class CommonDeleteManyQueryRunnerService extends CommonBaseQueryRunnerSer authContext, rolePermissionConfig, workspaceDataSource, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatObjectMetadata, commonQueryParser, } = queryRunnerContext; const queryBuilder = repository.createQueryBuilder( - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, ); commonQueryParser.applyFilterToBuilder( queryBuilder, - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, args.filter, ); const columnsToReturn = buildColumnsToReturn({ select: args.selectedFieldsResult.select, relations: args.selectedFieldsResult.relations, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); const deletedObjectRecords = await queryBuilder @@ -72,10 +76,10 @@ export class CommonDeleteManyQueryRunnerService extends CommonBaseQueryRunnerSer if (isDefined(args.selectedFieldsResult.relations)) { await this.processNestedRelationsHelper.processNestedRelations({ - objectMetadataMaps, - parentObjectMetadataItem: objectMetadataItemWithFieldMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + parentObjectMetadataItem: flatObjectMetadata, parentObjectRecords: deletedRecords, - //TODO : Refacto-common - Typing to fix when switching processNestedRelationsHelper to Common relations: args.selectedFieldsResult.relations as Record< string, FindOptionsRelations @@ -95,39 +99,43 @@ export class CommonDeleteManyQueryRunnerService extends CommonBaseQueryRunnerSer args: CommonInput, queryRunnerContext: CommonBaseQueryRunnerContext, ): Promise> { - const { objectMetadataItemWithFieldMaps } = queryRunnerContext; + const { flatObjectMetadata, flatFieldMetadataMaps } = queryRunnerContext; return { ...args, filter: this.queryRunnerArgsFactory.overrideFilterByFieldMetadata( args.filter, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ), }; } async processQueryResult( queryResult: ObjectRecord[], - objectMetadataItemId: string, - objectMetadataMaps: ObjectMetadataMaps, + flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, authContext: WorkspaceAuthContext, ): Promise { return this.commonResultGettersService.processRecordArray( queryResult, - objectMetadataItemId, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext.workspace.id, ); } async validate( - args: DeleteManyQueryArgs, + args: CommonInput, queryRunnerContext: CommonBaseQueryRunnerContext, - ) { - const { objectMetadataItemWithFieldMaps } = queryRunnerContext; + ): Promise { + const { flatObjectMetadata } = queryRunnerContext; - assertMutationNotOnRemoteObject(objectMetadataItemWithFieldMaps); - if (!args.filter) { + assertMutationNotOnRemoteObject(flatObjectMetadata); + + if (!isDefined(args.filter)) { throw new CommonQueryRunnerException( 'Filter is required', CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT, diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-delete-one-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-delete-one-query-runner.service.ts index b9fb2c20fcd..b28eaa8e67b 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-delete-one-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-delete-one-query-runner.service.ts @@ -20,8 +20,10 @@ import { DeleteOneQueryArgs, } from 'src/engine/api/common/types/common-query-args.type'; import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; +import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; @Injectable() export class CommonDeleteOneQueryRunnerService extends CommonBaseQueryRunnerService< @@ -67,14 +69,16 @@ export class CommonDeleteOneQueryRunnerService extends CommonBaseQueryRunnerServ async processQueryResult( queryResult: ObjectRecord, - objectMetadataItemId: string, - objectMetadataMaps: ObjectMetadataMaps, + flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, authContext: WorkspaceAuthContext, ): Promise { return this.commonResultGettersService.processRecord( queryResult, - objectMetadataItemId, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext.workspace.id, ); } @@ -83,9 +87,9 @@ export class CommonDeleteOneQueryRunnerService extends CommonBaseQueryRunnerServ args: CommonInput, queryRunnerContext: CommonBaseQueryRunnerContext, ): Promise { - const { objectMetadataItemWithFieldMaps } = queryRunnerContext; + const { flatObjectMetadata } = queryRunnerContext; - assertMutationNotOnRemoteObject(objectMetadataItemWithFieldMaps); + assertMutationNotOnRemoteObject(flatObjectMetadata); assertIsValidUuid(args.id); } } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-destroy-many-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-destroy-many-query-runner.service.ts index 9cebffc580c..b90bb47e96e 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-destroy-many-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-destroy-many-query-runner.service.ts @@ -21,7 +21,11 @@ import { DestroyManyQueryArgs, } from 'src/engine/api/common/types/common-query-args.type'; import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; +import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; +import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; @Injectable() export class CommonDestroyManyQueryRunnerService extends CommonBaseQueryRunnerService< @@ -39,41 +43,44 @@ export class CommonDestroyManyQueryRunnerService extends CommonBaseQueryRunnerSe authContext, rolePermissionConfig, workspaceDataSource, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatObjectMetadata, commonQueryParser, } = queryRunnerContext; const queryBuilder = repository.createQueryBuilder( - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, ); commonQueryParser.applyFilterToBuilder( queryBuilder, - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, args.filter, ); const columnsToReturn = buildColumnsToReturn({ select: args.selectedFieldsResult.select, relations: args.selectedFieldsResult.relations, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); - const deletedObjectRecords = await queryBuilder + const destroyedObjectRecords = await queryBuilder .delete() .returning(columnsToReturn) .execute(); - const deletedRecords = deletedObjectRecords.generatedMaps as ObjectRecord[]; + const destroyedRecords = + destroyedObjectRecords.generatedMaps as ObjectRecord[]; if (isDefined(args.selectedFieldsResult.relations)) { await this.processNestedRelationsHelper.processNestedRelations({ - objectMetadataMaps, - parentObjectMetadataItem: objectMetadataItemWithFieldMaps, - parentObjectRecords: deletedRecords, - //TODO : Refacto-common - Typing to fix when switching processNestedRelationsHelper to Common + flatObjectMetadataMaps, + flatFieldMetadataMaps, + parentObjectMetadataItem: flatObjectMetadata, + parentObjectRecords: destroyedRecords, relations: args.selectedFieldsResult.relations as Record< string, FindOptionsRelations @@ -86,47 +93,56 @@ export class CommonDestroyManyQueryRunnerService extends CommonBaseQueryRunnerSe }); } - return deletedRecords; + return destroyedRecords; } async computeArgs( args: CommonInput, queryRunnerContext: CommonBaseQueryRunnerContext, ): Promise> { - const { objectMetadataItemWithFieldMaps } = queryRunnerContext; + const { flatObjectMetadata, flatFieldMetadataMaps } = queryRunnerContext; return { ...args, filter: this.queryRunnerArgsFactory.overrideFilterByFieldMetadata( args.filter, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ), }; } async processQueryResult( queryResult: ObjectRecord[], - objectMetadataItemId: string, - objectMetadataMaps: ObjectMetadataMaps, + flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, authContext: WorkspaceAuthContext, ): Promise { return this.commonResultGettersService.processRecordArray( queryResult, - objectMetadataItemId, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext.workspace.id, ); } async validate( args: CommonInput, - _queryRunnerContext: CommonBaseQueryRunnerContext, + queryRunnerContext: CommonBaseQueryRunnerContext, ): Promise { + const { flatObjectMetadata } = queryRunnerContext; + + assertMutationNotOnRemoteObject(flatObjectMetadata); + if (!isDefined(args.filter)) { throw new CommonQueryRunnerException( 'Filter is required', CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT, ); } + + args.filter.id?.in?.forEach((id: string) => assertIsValidUuid(id)); } } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-destroy-one-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-destroy-one-query-runner.service.ts index 9b8529c48ac..2d9c035f026 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-destroy-one-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-destroy-one-query-runner.service.ts @@ -19,7 +19,9 @@ import { CommonQueryNames, DestroyOneQueryArgs, } from 'src/engine/api/common/types/common-query-args.type'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; @Injectable() export class CommonDestroyOneQueryRunnerService extends CommonBaseQueryRunnerService< @@ -65,14 +67,16 @@ export class CommonDestroyOneQueryRunnerService extends CommonBaseQueryRunnerSer async processQueryResult( queryResult: ObjectRecord, - objectMetadataItemId: string, - objectMetadataMaps: ObjectMetadataMaps, + flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, authContext: WorkspaceAuthContext, ): Promise { return this.commonResultGettersService.processRecord( queryResult, - objectMetadataItemId, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext.workspace.id, ); } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-duplicates-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-duplicates-query-runner.service.ts index b16c488dc89..3973f8a594a 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-duplicates-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-duplicates-query-runner.service.ts @@ -25,7 +25,10 @@ import { import { getPageInfo } from 'src/engine/api/common/utils/get-page-info.util'; import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select'; import { buildDuplicateConditions } from 'src/engine/api/utils/build-duplicate-conditions.utils'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; +import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; @Injectable() export class CommonFindDuplicatesQueryRunnerService extends CommonBaseQueryRunnerService< @@ -40,8 +43,9 @@ export class CommonFindDuplicatesQueryRunnerService extends CommonBaseQueryRunne ): Promise { const { repository, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, commonQueryParser, authContext, workspaceDataSource, @@ -49,7 +53,7 @@ export class CommonFindDuplicatesQueryRunnerService extends CommonBaseQueryRunne } = queryRunnerContext; const existingRecordsQueryBuilder = repository.createQueryBuilder( - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, ); let objectRecords: Partial[] = []; @@ -57,8 +61,9 @@ export class CommonFindDuplicatesQueryRunnerService extends CommonBaseQueryRunne const columnsToSelect = buildColumnsToSelect({ select: args.selectedFieldsResult.select, relations: args.selectedFieldsResult.relations, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); if (isDefined(args.ids) && args.ids.length > 0) { @@ -76,7 +81,9 @@ export class CommonFindDuplicatesQueryRunnerService extends CommonBaseQueryRunne await Promise.all( objectRecords.map(async (record) => { const duplicateConditions = buildDuplicateConditions( - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, [record], record.id, ); @@ -93,12 +100,12 @@ export class CommonFindDuplicatesQueryRunnerService extends CommonBaseQueryRunne } const duplicateRecordsQueryBuilder = repository.createQueryBuilder( - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, ); commonQueryParser.applyFilterToBuilder( duplicateRecordsQueryBuilder, - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, duplicateConditions, ); @@ -132,8 +139,9 @@ export class CommonFindDuplicatesQueryRunnerService extends CommonBaseQueryRunne if (isDefined(args.selectedFieldsResult.relations)) { await this.processNestedRelationsHelper.processNestedRelations({ - objectMetadataMaps, - parentObjectMetadataItem: objectMetadataItemWithFieldMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + parentObjectMetadataItem: flatObjectMetadata, parentObjectRecords: findDuplicatesOutput.flatMap( (item) => item.records, ), @@ -157,7 +165,13 @@ export class CommonFindDuplicatesQueryRunnerService extends CommonBaseQueryRunne args: CommonInput, queryRunnerContext: CommonBaseQueryRunnerContext, ): Promise> { - const { authContext, objectMetadataItemWithFieldMaps } = queryRunnerContext; + const { authContext, flatObjectMetadata, flatFieldMetadataMaps } = + queryRunnerContext; + + const { fieldIdByName } = buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + flatObjectMetadata, + ); return { ...args, @@ -166,15 +180,17 @@ export class CommonFindDuplicatesQueryRunnerService extends CommonBaseQueryRunne this.queryRunnerArgsFactory.overrideValueByFieldMetadata( 'id', id, - objectMetadataItemWithFieldMaps.fieldsById, - objectMetadataItemWithFieldMaps, + fieldIdByName, + flatObjectMetadata, + flatFieldMetadataMaps, ), ) ?? [], ), data: await this.dataArgProcessor.process({ partialRecordInputs: args.data, authContext, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, shouldBackfillPositionIfUndefined: false, }), }; @@ -182,8 +198,9 @@ export class CommonFindDuplicatesQueryRunnerService extends CommonBaseQueryRunne async processQueryResult( queryResult: CommonFindDuplicatesOutputItem[], - objectMetadataItemId: string, - objectMetadataMaps: ObjectMetadataMaps, + flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, authContext: WorkspaceAuthContext, ): Promise { const processedResults = await Promise.all( @@ -192,8 +209,9 @@ export class CommonFindDuplicatesQueryRunnerService extends CommonBaseQueryRunne ...result, records: await this.commonResultGettersService.processRecordArray( result.records, - objectMetadataItemId, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext.workspace.id, ), }; diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-many-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-many-query-runner.service.ts index 38bba8c57d8..5105d9dd332 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-many-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-many-query-runner.service.ts @@ -30,7 +30,9 @@ import { ProcessAggregateHelper } from 'src/engine/api/graphql/graphql-query-run import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select'; import { getCursor } from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util'; import { computeCursorArgFilter } from 'src/engine/api/utils/compute-cursor-arg-filter.utils'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; @Injectable() export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerService< @@ -47,14 +49,15 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi repository, authContext, rolePermissionConfig, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, workspaceDataSource, commonQueryParser, } = queryRunnerContext; const queryBuilder = repository.createQueryBuilder( - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, ); const aggregateQueryBuilder = queryBuilder.clone(); @@ -63,7 +66,7 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi commonQueryParser.applyFilterToBuilder( aggregateQueryBuilder, - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, appliedFilters, ); @@ -85,7 +88,8 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi const cursorArgFilter = computeCursorArgFilter( cursor, orderByWithIdCondition, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, isForwardPagination, ); @@ -98,14 +102,14 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi commonQueryParser.applyFilterToBuilder( queryBuilder, - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, appliedFilters, ); commonQueryParser.applyOrderToBuilder( queryBuilder, orderByWithIdCondition, - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, isForwardPagination, ); @@ -114,7 +118,7 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi ProcessAggregateHelper.addSelectedAggregatedFieldsQueriesToQueryBuilder({ selectedAggregatedFields: args.selectedFieldsResult.aggregate, queryBuilder: aggregateQueryBuilder, - objectMetadataNameSingular: objectMetadataItemWithFieldMaps.nameSingular, + objectMetadataNameSingular: flatObjectMetadata.nameSingular, }); const limit = args.first ?? args.last ?? QUERY_MAX_RECORDS; @@ -122,8 +126,9 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi const columnsToSelect = buildColumnsToSelect({ select: args.selectedFieldsResult.select, relations: args.selectedFieldsResult.relations, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); if (isDefined(args.offset)) { @@ -153,11 +158,11 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi if (isDefined(args.selectedFieldsResult.relations)) { await this.processNestedRelationsHelper.processNestedRelations({ - objectMetadataMaps, - parentObjectMetadataItem: objectMetadataItemWithFieldMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + parentObjectMetadataItem: flatObjectMetadata, parentObjectRecords: objectRecords, parentObjectRecordsAggregatedValues, - //TODO : Refacto-common - Typing to fix when switching processNestedRelationsHelper to Common relations: args.selectedFieldsResult.relations as Record< string, FindOptionsRelations @@ -184,28 +189,31 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi args: CommonInput, queryRunnerContext: CommonBaseQueryRunnerContext, ): Promise> { - const { objectMetadataItemWithFieldMaps } = queryRunnerContext; + const { flatObjectMetadata, flatFieldMetadataMaps } = queryRunnerContext; return { ...args, filter: this.queryRunnerArgsFactory.overrideFilterByFieldMetadata( args.filter, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ), }; } async processQueryResult( queryResult: CommonFindManyOutput, - objectMetadataItemId: string, - objectMetadataMaps: ObjectMetadataMaps, + flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, authContext: WorkspaceAuthContext, ): Promise { const processedRecords = await this.commonResultGettersService.processRecordArray( queryResult.records, - objectMetadataItemId, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext.workspace.id, ); diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-one-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-one-query-runner.service.ts index 3cbd1100f7d..c9aadc716fc 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-one-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-one-query-runner.service.ts @@ -22,7 +22,9 @@ import { FindOneQueryArgs, } from 'src/engine/api/common/types/common-query-args.type'; import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; @Injectable() export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerService< @@ -40,18 +42,19 @@ export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerServic authContext, rolePermissionConfig, workspaceDataSource, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatObjectMetadata, commonQueryParser, } = queryRunnerContext; const queryBuilder = repository.createQueryBuilder( - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, ); commonQueryParser.applyFilterToBuilder( queryBuilder, - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, args.filter ?? ({} as ObjectRecordFilter), ); @@ -63,8 +66,9 @@ export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerServic const columnsToSelect = buildColumnsToSelect({ select: args.selectedFieldsResult.select, relations: args.selectedFieldsResult.relations, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); const objectRecord = await queryBuilder @@ -84,10 +88,10 @@ export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerServic if (isDefined(args.selectedFieldsResult.relations)) { await this.processNestedRelationsHelper.processNestedRelations({ - objectMetadataMaps, - parentObjectMetadataItem: objectMetadataItemWithFieldMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + parentObjectMetadataItem: flatObjectMetadata, parentObjectRecords: objectRecords, - //TODO : Refacto-common - To fix when switching processNestedRelationsHelper to Common relations: args.selectedFieldsResult.relations as Record< string, FindOptionsRelations @@ -107,27 +111,30 @@ export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerServic args: CommonInput, queryRunnerContext: CommonBaseQueryRunnerContext, ): Promise> { - const { objectMetadataItemWithFieldMaps } = queryRunnerContext; + const { flatObjectMetadata, flatFieldMetadataMaps } = queryRunnerContext; return { ...args, filter: this.queryRunnerArgsFactory.overrideFilterByFieldMetadata( args.filter, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ), }; } async processQueryResult( queryResult: ObjectRecord, - objectMetadataItemId: string, - objectMetadataMaps: ObjectMetadataMaps, + flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, authContext: WorkspaceAuthContext, ): Promise { return this.commonResultGettersService.processRecord( queryResult, - objectMetadataItemId, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext.workspace.id, ); } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-group-by-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-group-by-query-runner.service.ts index 6f709a3b1d1..ffb3ac62f30 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-group-by-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-group-by-query-runner.service.ts @@ -44,9 +44,11 @@ import { parseGroupByArgs } from 'src/engine/api/graphql/graphql-query-runner/gr import { GroupByWithRecordsService } from 'src/engine/api/graphql/graphql-query-runner/group-by/services/group-by-with-records.service'; import { getGroupLimit } from 'src/engine/api/graphql/graphql-query-runner/group-by/utils/get-group-limit.util'; import { ProcessAggregateHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper'; -import { isFieldMetadataRelationOrMorphRelation } from 'src/engine/api/graphql/workspace-schema-builder/utils/is-field-metadata-relation-or-morph-relation.utils'; -import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util'; +import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util'; +import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { ViewFilterGroupService } from 'src/engine/metadata-modules/view-filter-group/services/view-filter-group.service'; import { ViewFilterService } from 'src/engine/metadata-modules/view-filter/services/view-filter.service'; import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; @@ -77,12 +79,13 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic const { repository, commonQueryParser, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext, } = queryRunnerContext; - const objectMetadataNameSingular = - objectMetadataItemWithFieldMaps.nameSingular; + const objectMetadataNameSingular = flatObjectMetadata.nameSingular; let queryBuilder = repository.createQueryBuilder( objectMetadataNameSingular, @@ -90,11 +93,12 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic const groupByFields = parseGroupByArgs( args, - objectMetadataItemWithFieldMaps, - queryRunnerContext.objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, ); - const objectAlias = getObjectAlias(objectMetadataItemWithFieldMaps); + const objectAlias = getObjectAlias(flatObjectMetadata); this.addJoinForGroupByOnRelationFields({ queryBuilder, @@ -108,7 +112,8 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic args, appliedFilters, queryBuilder, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, workspaceId: authContext.workspace.id, commonQueryParser, }); @@ -166,8 +171,9 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic async processQueryResult( queryResult: CommonGroupByOutputItem[], - _objectMetadataItemId: string, - _objectMetadataMaps: ObjectMetadataMaps, + _flatObjectMetadata: FlatObjectMetadata, + _flatObjectMetadataMaps: FlatEntityMaps, + _flatFieldMetadataMaps: FlatEntityMaps, _authContext: WorkspaceAuthContext, ): Promise { return queryResult; @@ -175,12 +181,14 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic private async addFiltersFromView({ args, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, appliedFilters, workspaceId, }: { args: GroupByQueryArgs; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; + flatObjectMetadata: FlatObjectMetadata; + flatFieldMetadataMaps: FlatEntityMaps; appliedFilters: ObjectRecordFilter; workspaceId: string; }): Promise { @@ -197,14 +205,21 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic ); const recordFilters = viewFilters.map((viewFilter) => { - const fieldMetadataItem = - objectMetadataItemWithFieldMaps.fieldsById[viewFilter.fieldMetadataId]; + const fieldMetadata = + flatFieldMetadataMaps.byId[viewFilter.fieldMetadataId]; + + if (!fieldMetadata) { + throw new CommonQueryRunnerException( + `Field metadata not found for field ${viewFilter.fieldMetadataId}`, + CommonQueryRunnerExceptionCode.INTERNAL_SERVER_ERROR, + ); + } return { id: viewFilter.id, fieldMetadataId: viewFilter.fieldMetadataId, value: convertViewFilterValueToString(viewFilter.value), - type: getFilterTypeFromFieldType(fieldMetadataItem.type), + type: getFilterTypeFromFieldType(fieldMetadata.type), operand: viewFilter.operand, recordFilterGroupId: viewFilter.viewFilterGroupId, positionInRecordFilterGroup: viewFilter.positionInViewFilterGroup, @@ -221,8 +236,9 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic }; }); - const fields = Object.values( - objectMetadataItemWithFieldMaps.fieldsById, + const fields = getFlatFieldsFromFlatObjectMetadata( + flatObjectMetadata, + flatFieldMetadataMaps, ).map((field) => ({ id: field.id, name: field.name, @@ -263,24 +279,26 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic args, appliedFilters, queryBuilder, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, workspaceId, commonQueryParser, }: { args: GroupByQueryArgs; appliedFilters: ObjectRecordFilter; queryBuilder: WorkspaceSelectQueryBuilder; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; + flatObjectMetadata: FlatObjectMetadata; + flatFieldMetadataMaps: FlatEntityMaps; workspaceId: string; commonQueryParser: GraphqlQueryParser; }): Promise { - const objectMetadataNameSingular = - objectMetadataItemWithFieldMaps.nameSingular; + const objectMetadataNameSingular = flatObjectMetadata.nameSingular; if (args.viewId) { appliedFilters = await this.addFiltersFromView({ args, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, appliedFilters, workspaceId, }); @@ -336,7 +354,7 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic if ( !groupByField.fieldMetadata.settings || - !isFieldMetadataRelationOrMorphRelation(groupByField.fieldMetadata) + !isMorphOrRelationFlatFieldMetadata(groupByField.fieldMetadata) ) { throw new CommonQueryRunnerException( `Field metadata settings are missing or invalid for field ${groupByField.fieldMetadata.name}`, @@ -370,13 +388,14 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic args: CommonInput, queryRunnerContext: CommonBaseQueryRunnerContext, ): Promise> { - const { objectMetadataItemWithFieldMaps } = queryRunnerContext; + const { flatObjectMetadata, flatFieldMetadataMaps } = queryRunnerContext; return { ...args, filter: this.queryRunnerArgsFactory.overrideFilterByFieldMetadata( args.filter, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ), }; } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-merge-many-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-merge-many-query-runner.service.ts index 72b839353ad..d8e7f2415fd 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-merge-many-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-merge-many-query-runner.service.ts @@ -33,10 +33,12 @@ import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runne import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select'; import { hasRecordFieldValue } from 'src/engine/api/graphql/graphql-query-runner/utils/has-record-field-value.util'; import { mergeFieldValues } from 'src/engine/api/graphql/graphql-query-runner/utils/merge-field-values.util'; +import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; +import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util'; +import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; -import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; -import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; @Injectable() export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerService< @@ -50,8 +52,11 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ args: CommonExtendedInput, queryRunnerContext: CommonExtendedQueryRunnerContext, ): Promise { - const { objectMetadataMaps, objectMetadataItemWithFieldMaps } = - queryRunnerContext; + const { + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatObjectMetadata, + } = queryRunnerContext; const recordsToMerge = await this.fetchRecordsToMerge( queryRunnerContext, @@ -67,7 +72,8 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ const mergedData = this.performDeepMerge( recordsToMerge, priorityRecord.id, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, args.dryRun ?? false, ); @@ -84,14 +90,15 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ ); const queryBuilder = queryRunnerContext.repository.createQueryBuilder( - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, ); const columnsToReturn = buildColumnsToReturn({ select: args.selectedFieldsResult.select, relations: args.selectedFieldsResult.relations, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); await queryBuilder @@ -123,8 +130,9 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ const columnsToSelect = buildColumnsToSelect({ select: args.selectedFieldsResult.select, relations: args.selectedFieldsResult.relations, - objectMetadataItemWithFieldMaps: context.objectMetadataItemWithFieldMaps, - objectMetadataMaps: context.objectMetadataMaps, + flatObjectMetadata: context.flatObjectMetadata, + flatObjectMetadataMaps: context.flatObjectMetadataMaps, + flatFieldMetadataMaps: context.flatFieldMetadataMaps, }); const recordsToMerge = await context.repository.find({ @@ -141,8 +149,9 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ if (args.dryRun && args.selectedFieldsResult.relations) { await this.processNestedRelationsHelper.processNestedRelations({ - objectMetadataMaps: context.objectMetadataMaps, - parentObjectMetadataItem: context.objectMetadataItemWithFieldMaps, + flatObjectMetadataMaps: context.flatObjectMetadataMaps, + flatFieldMetadataMaps: context.flatFieldMetadataMaps, + parentObjectMetadataItem: context.flatObjectMetadata, parentObjectRecords: recordsToMerge as ObjectRecord[], relations: args.selectedFieldsResult.relations as Record< string, @@ -182,7 +191,8 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ private performDeepMerge( recordsToMerge: ObjectRecord[], priorityRecordId: string, - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, isDryRun = false, ): Partial { const mergedResult: Partial = {}; @@ -194,7 +204,8 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ if ( !this.shouldExcludeFieldFromMerge( fieldName, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ) ) { allFieldNames.add(fieldName); @@ -218,9 +229,12 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ } else if (recordsWithValues.length === 1) { mergedResult[fieldName] = recordsWithValues[0].value; } else { - const fieldMetadata = Object.values( - objectMetadataItemWithFieldMaps.fieldsById, - ).find((field) => field?.name === fieldName); + const { fieldIdByName } = buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + flatObjectMetadata, + ); + const fieldMetadata = + flatFieldMetadataMaps.byId[fieldIdByName[fieldName]]; if (!fieldMetadata) { return; @@ -247,11 +261,14 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ private shouldExcludeFieldFromMerge( fieldName: string, - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, ): boolean { - const fieldMetadata = Object.values( - objectMetadataItemWithFieldMaps.fieldsById, - ).find((field) => field?.name === fieldName); + const { fieldIdByName } = buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + flatObjectMetadata, + ); + const fieldMetadata = flatFieldMetadataMaps.byId[fieldIdByName[fieldName]]; return fieldMetadata?.isSystem ?? false; } @@ -276,18 +293,23 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ priorityRecordId: string, mergedData: Partial, ): Promise { - const { objectMetadataItemWithFieldMaps, objectMetadataMaps, repository } = - queryRunnerContext; + const { + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + repository, + } = queryRunnerContext; const queryBuilder = repository.createQueryBuilder( - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, ); const columnsToReturn = buildColumnsToReturn({ select: args.selectedFieldsResult.select, relations: args.selectedFieldsResult.relations, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); const updatedObjectRecords = await queryBuilder @@ -314,46 +336,54 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ fromIds: string[], toId: string, ): Promise { - const { objectMetadataMaps, objectMetadataItemWithFieldMaps } = context; + const { + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatObjectMetadata, + } = context; - const relationFieldsPointingToCurrentObject = Object.values( - objectMetadataMaps.byId, - ) - .filter(isDefined) - .flatMap((metadata) => { - const relationFields = Object.values(metadata.fieldsById) - .filter(isDefined) - .filter((field) => - isFieldMetadataEntityOfType(field, FieldMetadataType.RELATION), - ) - .filter( - (field) => - field.relationTargetObjectMetadataId === - objectMetadataItemWithFieldMaps.id && field.isActive, - ); + const relationFieldsPointingToCurrentObject: Array<{ + objectMetadata: FlatObjectMetadata; + fieldName: string; + fieldId: string; + joinColumnName: string | undefined; + }> = []; - return relationFields - .filter((field) => { - const relationSettings = - field.settings as FieldMetadataRelationSettings; + for (const field of Object.values(flatFieldMetadataMaps.byId).filter( + isDefined, + )) { + if ( + !isFlatFieldMetadataOfType(field, FieldMetadataType.RELATION) || + field.relationTargetObjectMetadataId !== flatObjectMetadata.id || + !field.isActive + ) { + continue; + } - return ( - relationSettings?.relationType === RelationType.MANY_TO_ONE && - relationSettings?.joinColumnName - ); - }) - .map((field) => { - const relationSettings = - field.settings as FieldMetadataRelationSettings; + const relationSettings = field.settings as + | FieldMetadataRelationSettings + | undefined; - return { - objectMetadata: metadata, - fieldName: field.name, - fieldId: field.id, - joinColumnName: relationSettings.joinColumnName, - }; - }); + if ( + relationSettings?.relationType !== RelationType.MANY_TO_ONE || + !relationSettings?.joinColumnName + ) { + continue; + } + + const objMetadata = flatObjectMetadataMaps.byId[field.objectMetadataId]; + + if (!objMetadata) { + continue; + } + + relationFieldsPointingToCurrentObject.push({ + objectMetadata: objMetadata, + fieldName: field.name, + fieldId: field.id, + joinColumnName: relationSettings.joinColumnName, }); + } for (const relationField of relationFieldsPointingToCurrentObject) { if (!relationField.joinColumnName) { @@ -396,8 +426,9 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ updatedRecords: ObjectRecord[]; }): Promise { const { - objectMetadataMaps, - objectMetadataItemWithFieldMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatObjectMetadata, authContext, workspaceDataSource, rolePermissionConfig, @@ -405,8 +436,9 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ if (args.selectedFieldsResult.relations) { await this.processNestedRelationsHelper.processNestedRelations({ - objectMetadataMaps, - parentObjectMetadataItem: objectMetadataItemWithFieldMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + parentObjectMetadataItem: flatObjectMetadata, parentObjectRecords: updatedRecords, relations: args.selectedFieldsResult.relations as Record< string, @@ -430,8 +462,9 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ async processQueryResult( queryResult: ObjectRecord, - _objectMetadataItemId: string, - _objectMetadataMaps: ObjectMetadataMaps, + _flatObjectMetadata: FlatObjectMetadata, + _flatObjectMetadataMaps: FlatEntityMaps, + _flatFieldMetadataMaps: FlatEntityMaps, _authContext: WorkspaceAuthContext, ): Promise { return queryResult; @@ -441,13 +474,13 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ args: CommonInput, queryRunnerContext: CommonExtendedQueryRunnerContext, ): Promise { - const { objectMetadataItemWithFieldMaps } = queryRunnerContext; + const { flatObjectMetadata } = queryRunnerContext; - assertMutationNotOnRemoteObject(objectMetadataItemWithFieldMaps); + assertMutationNotOnRemoteObject(flatObjectMetadata); - if (!isDefined(objectMetadataItemWithFieldMaps.duplicateCriteria)) { + if (!isDefined(flatObjectMetadata.duplicateCriteria)) { throw new CommonQueryRunnerException( - `Merge is only available for objects with duplicate criteria. Object '${objectMetadataItemWithFieldMaps.nameSingular}' does not have duplicate criteria defined.`, + `Merge is only available for objects with duplicate criteria. Object '${flatObjectMetadata.nameSingular}' does not have duplicate criteria defined.`, CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT, ); } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-restore-many-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-restore-many-query-runner.service.ts index 52045178e3b..444c76811e8 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-restore-many-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-restore-many-query-runner.service.ts @@ -22,8 +22,10 @@ import { } from 'src/engine/api/common/types/common-query-args.type'; import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return'; import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; +import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; @Injectable() export class CommonRestoreManyQueryRunnerService extends CommonBaseQueryRunnerService< @@ -41,26 +43,28 @@ export class CommonRestoreManyQueryRunnerService extends CommonBaseQueryRunnerSe authContext, rolePermissionConfig, workspaceDataSource, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatObjectMetadata, commonQueryParser, } = queryRunnerContext; const queryBuilder = repository.createQueryBuilder( - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, ); commonQueryParser.applyFilterToBuilder( queryBuilder, - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, args.filter, ); const columnsToReturn = buildColumnsToReturn({ select: args.selectedFieldsResult.select, relations: args.selectedFieldsResult.relations, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); const restoredObjectRecords = await queryBuilder @@ -73,10 +77,10 @@ export class CommonRestoreManyQueryRunnerService extends CommonBaseQueryRunnerSe if (isDefined(args.selectedFieldsResult.relations)) { await this.processNestedRelationsHelper.processNestedRelations({ - objectMetadataMaps, - parentObjectMetadataItem: objectMetadataItemWithFieldMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + parentObjectMetadataItem: flatObjectMetadata, parentObjectRecords: restoredRecords, - //TODO : Refacto-common - Typing to fix when switching processNestedRelationsHelper to Common relations: args.selectedFieldsResult.relations as Record< string, FindOptionsRelations @@ -96,27 +100,30 @@ export class CommonRestoreManyQueryRunnerService extends CommonBaseQueryRunnerSe args: CommonInput, queryRunnerContext: CommonBaseQueryRunnerContext, ): Promise> { - const { objectMetadataItemWithFieldMaps } = queryRunnerContext; + const { flatObjectMetadata, flatFieldMetadataMaps } = queryRunnerContext; return { ...args, filter: this.queryRunnerArgsFactory.overrideFilterByFieldMetadata( args.filter, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ), }; } async processQueryResult( queryResult: ObjectRecord[], - objectMetadataItemId: string, - objectMetadataMaps: ObjectMetadataMaps, + flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, authContext: WorkspaceAuthContext, ): Promise { return this.commonResultGettersService.processRecordArray( queryResult, - objectMetadataItemId, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext.workspace.id, ); } @@ -125,9 +132,9 @@ export class CommonRestoreManyQueryRunnerService extends CommonBaseQueryRunnerSe args: RestoreManyQueryArgs, queryRunnerContext: CommonBaseQueryRunnerContext, ) { - const { objectMetadataItemWithFieldMaps } = queryRunnerContext; + const { flatObjectMetadata } = queryRunnerContext; - assertMutationNotOnRemoteObject(objectMetadataItemWithFieldMaps); + assertMutationNotOnRemoteObject(flatObjectMetadata); if (!isDefined(args.filter)) { throw new CommonQueryRunnerException( 'Filter is required', diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-restore-one-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-restore-one-query-runner.service.ts index 61332332d0b..93eab1448d9 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-restore-one-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-restore-one-query-runner.service.ts @@ -20,8 +20,10 @@ import { RestoreOneQueryArgs, } from 'src/engine/api/common/types/common-query-args.type'; import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; +import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; @Injectable() export class CommonRestoreOneQueryRunnerService extends CommonBaseQueryRunnerService< @@ -67,14 +69,16 @@ export class CommonRestoreOneQueryRunnerService extends CommonBaseQueryRunnerSer async processQueryResult( queryResult: ObjectRecord, - objectMetadataItemId: string, - objectMetadataMaps: ObjectMetadataMaps, + flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, authContext: WorkspaceAuthContext, ): Promise { return this.commonResultGettersService.processRecord( queryResult, - objectMetadataItemId, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext.workspace.id, ); } @@ -83,9 +87,9 @@ export class CommonRestoreOneQueryRunnerService extends CommonBaseQueryRunnerSer args: CommonInput, queryRunnerContext: CommonBaseQueryRunnerContext, ): Promise { - const { objectMetadataItemWithFieldMaps } = queryRunnerContext; + const { flatObjectMetadata } = queryRunnerContext; - assertMutationNotOnRemoteObject(objectMetadataItemWithFieldMaps); + assertMutationNotOnRemoteObject(flatObjectMetadata); assertIsValidUuid(args.id); } } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-update-many-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-update-many-query-runner.service.ts index 60df7d4f670..736c255abb2 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-update-many-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-update-many-query-runner.service.ts @@ -22,8 +22,10 @@ import { } from 'src/engine/api/common/types/common-query-args.type'; import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return'; import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; +import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; @Injectable() export class CommonUpdateManyQueryRunnerService extends CommonBaseQueryRunnerService< @@ -41,26 +43,28 @@ export class CommonUpdateManyQueryRunnerService extends CommonBaseQueryRunnerSer authContext, rolePermissionConfig, workspaceDataSource, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatObjectMetadata, commonQueryParser, } = queryRunnerContext; const queryBuilder = repository.createQueryBuilder( - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, ); commonQueryParser.applyFilterToBuilder( queryBuilder, - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, args.filter, ); const columnsToReturn = buildColumnsToReturn({ select: args.selectedFieldsResult.select, relations: args.selectedFieldsResult.relations, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); const updatedObjectRecords = await queryBuilder @@ -73,10 +77,10 @@ export class CommonUpdateManyQueryRunnerService extends CommonBaseQueryRunnerSer if (isDefined(args.selectedFieldsResult.relations)) { await this.processNestedRelationsHelper.processNestedRelations({ - objectMetadataMaps, - parentObjectMetadataItem: objectMetadataItemWithFieldMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + parentObjectMetadataItem: flatObjectMetadata, parentObjectRecords: updatedRecords, - //TODO : Refacto-common - Typing to fix when switching processNestedRelationsHelper to Common relations: args.selectedFieldsResult.relations as Record< string, FindOptionsRelations @@ -96,20 +100,22 @@ export class CommonUpdateManyQueryRunnerService extends CommonBaseQueryRunnerSer args: CommonInput, queryRunnerContext: CommonBaseQueryRunnerContext, ): Promise> { - const { authContext, objectMetadataItemWithFieldMaps } = queryRunnerContext; + const { authContext, flatObjectMetadata, flatFieldMetadataMaps } = + queryRunnerContext; return { ...args, - filter: - this.queryRunnerArgsFactory.overrideFilterByFieldMetadata( - args.filter, - objectMetadataItemWithFieldMaps, - ) || {}, + filter: this.queryRunnerArgsFactory.overrideFilterByFieldMetadata( + args.filter, + flatObjectMetadata, + flatFieldMetadataMaps, + ), data: ( await this.dataArgProcessor.process({ partialRecordInputs: [args.data], authContext, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, shouldBackfillPositionIfUndefined: false, }) )[0], @@ -120,9 +126,9 @@ export class CommonUpdateManyQueryRunnerService extends CommonBaseQueryRunnerSer args: CommonInput, queryRunnerContext: CommonBaseQueryRunnerContext, ): Promise { - const { objectMetadataItemWithFieldMaps } = queryRunnerContext; + const { flatObjectMetadata } = queryRunnerContext; - assertMutationNotOnRemoteObject(objectMetadataItemWithFieldMaps); + assertMutationNotOnRemoteObject(flatObjectMetadata); if (!args.filter) { throw new CommonQueryRunnerException( 'Filter is required', @@ -135,14 +141,16 @@ export class CommonUpdateManyQueryRunnerService extends CommonBaseQueryRunnerSer async processQueryResult( queryResult: ObjectRecord[], - objectMetadataItemId: string, - objectMetadataMaps: ObjectMetadataMaps, + flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, authContext: WorkspaceAuthContext, ): Promise { return await this.commonResultGettersService.processRecordArray( queryResult, - objectMetadataItemId, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext.workspace.id, ); } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-update-one-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-update-one-query-runner.service.ts index c899bd641be..b564d0ac8c8 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-update-one-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-update-one-query-runner.service.ts @@ -19,8 +19,10 @@ import { UpdateOneQueryArgs, } from 'src/engine/api/common/types/common-query-args.type'; import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; +import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; @Injectable() export class CommonUpdateOneQueryRunnerService extends CommonBaseQueryRunnerService< @@ -60,7 +62,8 @@ export class CommonUpdateOneQueryRunnerService extends CommonBaseQueryRunnerServ args: CommonInput, queryRunnerContext: CommonBaseQueryRunnerContext, ): Promise> { - const { authContext, objectMetadataItemWithFieldMaps } = queryRunnerContext; + const { authContext, flatObjectMetadata, flatFieldMetadataMaps } = + queryRunnerContext; return { ...args, @@ -68,7 +71,8 @@ export class CommonUpdateOneQueryRunnerService extends CommonBaseQueryRunnerServ await this.dataArgProcessor.process({ partialRecordInputs: [args.data], authContext, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, shouldBackfillPositionIfUndefined: false, }) )[0], @@ -77,14 +81,16 @@ export class CommonUpdateOneQueryRunnerService extends CommonBaseQueryRunnerServ async processQueryResult( queryResult: ObjectRecord, - objectMetadataItemId: string, - objectMetadataMaps: ObjectMetadataMaps, + flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, authContext: WorkspaceAuthContext, ): Promise { return this.commonResultGettersService.processRecord( queryResult, - objectMetadataItemId, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext.workspace.id, ); } @@ -93,9 +99,9 @@ export class CommonUpdateOneQueryRunnerService extends CommonBaseQueryRunnerServ args: CommonInput, queryRunnerContext: CommonBaseQueryRunnerContext, ): Promise { - const { objectMetadataItemWithFieldMaps } = queryRunnerContext; + const { flatObjectMetadata } = queryRunnerContext; - assertMutationNotOnRemoteObject(objectMetadataItemWithFieldMaps); + assertMutationNotOnRemoteObject(flatObjectMetadata); assertIsValidUuid(args.id); } } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/utils/get-object-alias-for-group-by.util.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/utils/get-object-alias-for-group-by.util.ts index 36fa83fdd3c..f9372dfe359 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/utils/get-object-alias-for-group-by.util.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/utils/get-object-alias-for-group-by.util.ts @@ -1,7 +1,7 @@ -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; export const getObjectAlias = ( - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, ): string => { - return objectMetadataItemWithFieldMaps.nameSingular; + return flatObjectMetadata.nameSingular; }; diff --git a/packages/twenty-server/src/engine/api/common/common-result-getters/common-result-getters.service.ts b/packages/twenty-server/src/engine/api/common/common-result-getters/common-result-getters.service.ts index 1cb419b042e..1d598102025 100644 --- a/packages/twenty-server/src/engine/api/common/common-result-getters/common-result-getters.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-result-getters/common-result-getters.service.ts @@ -15,8 +15,12 @@ import { AttachmentQueryResultGetterHandler } from 'src/engine/api/graphql/works import { PersonQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/person-query-result-getter.handler'; import { WorkspaceMemberQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/workspace-member-query-result-getter.handler'; import { FileService } from 'src/engine/core-modules/file/services/file.service'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; -import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; +import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; // TODO: find a way to prevent conflict between handlers executing logic on object relations // And this factory that is also executing logic on object relations @@ -45,8 +49,9 @@ export class CommonResultGettersService { public async processRecordArray( recordArray: ObjectRecord[], - objectMetadataItemId: string, - objectMetadataMaps: ObjectMetadataMaps, + flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, workspaceId: string, ) { return await Promise.all( @@ -54,8 +59,9 @@ export class CommonResultGettersService { async (record: ObjectRecord) => await this.processRecord( record, - objectMetadataItemId, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, workspaceId, ), ), @@ -64,28 +70,26 @@ export class CommonResultGettersService { public async processRecord( record: ObjectRecord, - objectMetadataItemId: string, - objectMetadataMaps: ObjectMetadataMaps, + flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, workspaceId: string, ): Promise { - const objectMetadataMapItem = objectMetadataMaps.byId[objectMetadataItemId]; + const handler = this.getHandler(flatObjectMetadata.nameSingular); - if (!isDefined(objectMetadataMapItem)) { - throw new Error('Object metadata map item is not defined'); - } - - const handler = this.getHandler(objectMetadataMapItem.nameSingular); + const { fieldIdByName } = buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + flatObjectMetadata, + ); const relationFields = Object.keys(record) .map( (recordFieldName) => - objectMetadataMapItem.fieldsById[ - objectMetadataMapItem.fieldIdByName[recordFieldName] - ], + flatFieldMetadataMaps.byId[fieldIdByName[recordFieldName]], ) .filter(isDefined) .filter((fieldMetadata) => - isFieldMetadataEntityOfType(fieldMetadata, FieldMetadataType.RELATION), + isFlatFieldMetadataOfType(fieldMetadata, FieldMetadataType.RELATION), ); const relationFieldsProcessedMap = {} as Record< @@ -104,18 +108,26 @@ export class CommonResultGettersService { continue; } + const targetFlatObjectMetadata = + findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityId: relationField.relationTargetObjectMetadataId, + flatEntityMaps: flatObjectMetadataMaps, + }); + relationFieldsProcessedMap[relationField.name] = relationField.settings?.relationType === RelationType.ONE_TO_MANY ? await this.processRecordArray( record[relationField.name], - relationField.relationTargetObjectMetadataId, - objectMetadataMaps, + targetFlatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, workspaceId, ) : await this.processRecord( record[relationField.name], - relationField.relationTargetObjectMetadataId, - objectMetadataMaps, + targetFlatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, workspaceId, ); } diff --git a/packages/twenty-server/src/engine/api/common/types/common-base-query-runner-context.type.ts b/packages/twenty-server/src/engine/api/common/types/common-base-query-runner-context.type.ts index d6c5e1e5e5a..b265bf45355 100644 --- a/packages/twenty-server/src/engine/api/common/types/common-base-query-runner-context.type.ts +++ b/packages/twenty-server/src/engine/api/common/types/common-base-query-runner-context.type.ts @@ -1,9 +1,12 @@ import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; export type CommonBaseQueryRunnerContext = { authContext: AuthContext; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; - objectMetadataMaps: ObjectMetadataMaps; + flatObjectMetadata: FlatObjectMetadata; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; + objectIdByNameSingular: Record; }; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/__mocks__/mockPersonObjectMetadata.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/__mocks__/mockPersonObjectMetadata.ts index b2297d08c4e..0ab4cdf36ad 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/__mocks__/mockPersonObjectMetadata.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/__mocks__/mockPersonObjectMetadata.ts @@ -1,24 +1,151 @@ import { FieldMetadataType } from 'twenty-shared/types'; import { type WorkspaceEntityDuplicateCriteria } from 'src/engine/api/graphql/workspace-query-builder/types/workspace-entity-duplicate-criteria.type'; -import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock'; -import { getMockObjectMetadataItemWithFieldsMaps } from 'src/utils/__test__/get-object-metadata-item-with-fields-maps.mock'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; const workspaceId = '20202020-1c25-4d02-bf25-6aeccf7ea419'; +const objectMetadataId = 'person-object-id'; -export const mockPersonObjectMetadataWithFieldMaps = ( +const mockFieldMetadatas: FlatFieldMetadata[] = [ + { + id: 'name-id', + type: FieldMetadataType.FULL_NAME, + name: 'name', + label: 'Name', + defaultValue: { + lastName: "''", + firstName: "''", + }, + objectMetadataId, + isNullable: true, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), + universalIdentifier: 'name-id', + viewFieldIds: [], + viewFilterIds: [], + viewGroupIds: [], + kanbanAggregateOperationViewIds: [], + calendarViewIds: [], + applicationId: null, + } as unknown as FlatFieldMetadata, + { + id: 'emails-id', + type: FieldMetadataType.EMAILS, + name: 'emails', + label: 'Emails', + defaultValue: { + primaryEmail: "''", + additionalEmails: null, + }, + objectMetadataId, + isNullable: true, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), + universalIdentifier: 'emails-id', + viewFieldIds: [], + viewFilterIds: [], + viewGroupIds: [], + kanbanAggregateOperationViewIds: [], + calendarViewIds: [], + applicationId: null, + } as unknown as FlatFieldMetadata, + { + id: 'linkedinLink-id', + type: FieldMetadataType.LINKS, + name: 'linkedinLink', + label: 'Linkedin', + defaultValue: { + primaryLinkUrl: "''", + secondaryLinks: [], + primaryLinkLabel: "''", + }, + objectMetadataId, + isNullable: true, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), + universalIdentifier: 'linkedinLink-id', + viewFieldIds: [], + viewFilterIds: [], + viewGroupIds: [], + kanbanAggregateOperationViewIds: [], + calendarViewIds: [], + applicationId: null, + } as unknown as FlatFieldMetadata, + { + id: 'jobTitle-id', + type: FieldMetadataType.TEXT, + name: 'jobTitle', + label: 'Job Title', + defaultValue: "''", + objectMetadataId, + isNullable: false, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), + universalIdentifier: 'jobTitle-id', + viewFieldIds: [], + viewFilterIds: [], + viewGroupIds: [], + kanbanAggregateOperationViewIds: [], + calendarViewIds: [], + applicationId: null, + } as unknown as FlatFieldMetadata, +]; + +export const mockPersonFlatFieldMetadataMaps = + (): FlatEntityMaps => ({ + byId: mockFieldMetadatas.reduce( + (acc, field) => { + acc[field.id] = field; + + return acc; + }, + {} as Record, + ), + idByUniversalIdentifier: mockFieldMetadatas.reduce( + (acc, field) => { + acc[field.universalIdentifier] = field.id; + + return acc; + }, + {} as Record, + ), + universalIdentifiersByApplicationId: {}, + }); + +export const mockPersonFlatObjectMetadataMaps = ( duplicateCriteria: WorkspaceEntityDuplicateCriteria[], -) => - getMockObjectMetadataItemWithFieldsMaps({ - id: '', +): FlatEntityMaps => { + const flatObjectMetadata = mockPersonFlatObjectMetadata(duplicateCriteria); + + return { + byId: { + [flatObjectMetadata.id]: flatObjectMetadata, + }, + idByUniversalIdentifier: { + [flatObjectMetadata.universalIdentifier as string]: flatObjectMetadata.id, + }, + universalIdentifiersByApplicationId: {}, + }; +}; + +export const mockPersonFlatObjectMetadata = ( + duplicateCriteria: WorkspaceEntityDuplicateCriteria[], +): FlatObjectMetadata => + ({ + id: objectMetadataId, icon: 'Icon123', standardId: '', nameSingular: 'person', namePlural: 'people', labelSingular: 'Person', labelPlural: 'People', - description: 'A person', - targetTableName: 'DEPRECATED', + targetTableName: 'person', isCustom: false, isRemote: false, isActive: true, @@ -29,87 +156,16 @@ export const mockPersonObjectMetadataWithFieldMaps = ( labelIdentifierFieldMetadataId: '', imageIdentifierFieldMetadataId: '', workspaceId, - indexMetadatas: [], - fieldIdByName: { - name: 'name-id', - emails: 'emails-id', - linkedinLink: 'linkedinLink-id', - jobTitle: 'jobTitle-id', - }, - fieldIdByJoinColumnName: {}, - fieldsById: { - 'name-id': getMockFieldMetadataEntity({ - workspaceId, - objectMetadataId: '', - id: 'name-id', - type: FieldMetadataType.FULL_NAME, - name: 'name', - label: 'Name', - defaultValue: { - lastName: "''", - firstName: "''", - }, - description: "Contact's name", - isCustom: false, - isNullable: true, - isUnique: false, - isLabelSyncedWithName: true, - createdAt: new Date(), - updatedAt: new Date(), - }), - 'emails-id': getMockFieldMetadataEntity({ - workspaceId, - objectMetadataId: '', - id: 'emails-id', - type: FieldMetadataType.EMAILS, - name: 'emails', - label: 'Emails', - defaultValue: { - primaryEmail: "''", - additionalEmails: null, - }, - description: "Contact's Emails", - isCustom: false, - isNullable: true, - isLabelSyncedWithName: true, - createdAt: new Date(), - updatedAt: new Date(), - }), - 'linkedinLink-id': getMockFieldMetadataEntity({ - workspaceId, - objectMetadataId: '', - id: 'linkedinLink-id', - type: FieldMetadataType.LINKS, - name: 'linkedinLink', - label: 'Linkedin', - defaultValue: { - primaryLinkUrl: "''", - secondaryLinks: [], - primaryLinkLabel: "''", - }, - description: "Contact's Linkedin account", - isCustom: false, - isNullable: true, - isUnique: false, - isLabelSyncedWithName: true, - createdAt: new Date(), - updatedAt: new Date(), - }), - 'jobTitle-id': getMockFieldMetadataEntity({ - workspaceId, - objectMetadataId: '', - id: 'jobTitle-id', - type: FieldMetadataType.TEXT, - name: 'jobTitle', - label: 'Job Title', - defaultValue: "''", - description: "Contact's job title", - isCustom: false, - isNullable: false, - isUnique: false, - isLabelSyncedWithName: true, - createdAt: new Date(), - updatedAt: new Date(), - }), - }, - }); + universalIdentifier: objectMetadataId, + indexMetadataIds: [], + fieldMetadataIds: mockFieldMetadatas.map((field) => field.id), + viewIds: [], + applicationId: null, + isLabelSyncedWithName: false, + createdAt: new Date(), + updatedAt: new Date(), + shortcut: null, + description: null, + standardOverrides: null, + isUIReadOnly: false, + }) as FlatObjectMetadata; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser.ts index d421fdf2a2c..928acec19b7 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser.ts @@ -2,19 +2,25 @@ import { Brackets, NotBrackets, type WhereExpressionBuilder } from 'typeorm'; import { type ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { type WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder'; import { GraphqlQueryFilterFieldParser } from './graphql-query-filter-field.parser'; export class GraphqlQueryFilterConditionParser { - private objectMetadataMapItem: ObjectMetadataItemWithFieldMaps; + private flatObjectMetadata: FlatObjectMetadata; private queryFilterFieldParser: GraphqlQueryFilterFieldParser; - constructor(objectMetadataMapItem: ObjectMetadataItemWithFieldMaps) { - this.objectMetadataMapItem = objectMetadataMapItem; + constructor( + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, + ) { + this.flatObjectMetadata = flatObjectMetadata; this.queryFilterFieldParser = new GraphqlQueryFilterFieldParser( - this.objectMetadataMapItem, + this.flatObjectMetadata, + flatFieldMetadataMaps, ); } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-field.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-field.parser.ts index afe54f1b371..a595b0e5daf 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-field.parser.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-field.parser.ts @@ -1,5 +1,5 @@ import { compositeTypeDefinitions } from 'twenty-shared/types'; -import { capitalize } from 'twenty-shared/utils'; +import { capitalize, isDefined } from 'twenty-shared/utils'; import { type WhereExpressionBuilder } from 'typeorm'; import { @@ -7,18 +7,35 @@ import { GraphqlQueryRunnerExceptionCode, } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; import { computeWhereConditionParts } from 'src/engine/api/graphql/graphql-query-runner/utils/compute-where-condition-parts'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { type CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory'; const ARRAY_OPERATORS = ['in', 'contains', 'notContains']; export class GraphqlQueryFilterFieldParser { - private objectMetadataMapItem: ObjectMetadataItemWithFieldMaps; + private flatObjectMetadata: FlatObjectMetadata; + private flatFieldMetadataMaps: FlatEntityMaps; + private fieldIdByName: Record; + private fieldIdByJoinColumnName: Record; - constructor(objectMetadataMapItem: ObjectMetadataItemWithFieldMaps) { - this.objectMetadataMapItem = objectMetadataMapItem; + constructor( + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, + ) { + this.flatObjectMetadata = flatObjectMetadata; + this.flatFieldMetadataMaps = flatFieldMetadataMaps; + + const fieldMaps = buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + flatObjectMetadata, + ); + + this.fieldIdByName = fieldMaps.fieldIdByName; + this.fieldIdByJoinColumnName = fieldMaps.fieldIdByJoinColumnName; } public parse( @@ -30,13 +47,11 @@ export class GraphqlQueryFilterFieldParser { isFirst = false, ): void { const fieldMetadataId = - this.objectMetadataMapItem.fieldIdByName[`${key}`] || - this.objectMetadataMapItem.fieldIdByJoinColumnName[`${key}`]; + this.fieldIdByName[`${key}`] || this.fieldIdByJoinColumnName[`${key}`]; - const fieldMetadata = - this.objectMetadataMapItem.fieldsById[fieldMetadataId]; + const fieldMetadata = this.flatFieldMetadataMaps.byId[fieldMetadataId]; - if (!fieldMetadata) { + if (!isDefined(fieldMetadata)) { throw new Error(`Field metadata not found for field: ${key}`); } @@ -77,7 +92,7 @@ export class GraphqlQueryFilterFieldParser { private parseCompositeFieldForFilter( queryBuilder: WhereExpressionBuilder, - fieldMetadata: FieldMetadataEntity, + fieldMetadata: FlatFieldMetadata, objectNameSingular: string, // eslint-disable-next-line @typescript-eslint/no-explicit-any fieldValue: any, diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser.ts index 683bddff33a..1404a2bf80e 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser.ts @@ -32,12 +32,13 @@ import { type AggregationField, getAvailableAggregationsFromObjectFields, } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util'; -import { isFieldMetadataRelationOrMorphRelation } from 'src/engine/api/graphql/workspace-schema-builder/utils/is-field-metadata-relation-or-morph-relation.utils'; import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; +import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { formatColumnNameForRelationField } from 'src/engine/twenty-orm/utils/format-column-name-for-relation-field.util'; import { formatColumnNamesFromCompositeFieldAndSubfields } from 'src/engine/twenty-orm/utils/format-column-names-from-composite-field-and-subfield.util'; @@ -47,15 +48,28 @@ export type OrderByCondition = { }; export class GraphqlQueryOrderFieldParser { - private objectMetadataMapItem: ObjectMetadataItemWithFieldMaps; - private objectMetadataMaps?: ObjectMetadataMaps; + private flatObjectMetadata: FlatObjectMetadata; + private flatObjectMetadataMaps: FlatEntityMaps; + private flatFieldMetadataMaps: FlatEntityMaps; + private fieldIdByName: Record; + private fieldIdByJoinColumnName: Record; constructor( - objectMetadataMapItem: ObjectMetadataItemWithFieldMaps, - objectMetadataMaps?: ObjectMetadataMaps, + flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, ) { - this.objectMetadataMapItem = objectMetadataMapItem; - this.objectMetadataMaps = objectMetadataMaps; + this.flatObjectMetadata = flatObjectMetadata; + this.flatObjectMetadataMaps = flatObjectMetadataMaps; + this.flatFieldMetadataMaps = flatFieldMetadataMaps; + + const fieldMaps = buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + flatObjectMetadata, + ); + + this.fieldIdByName = fieldMaps.fieldIdByName; + this.fieldIdByJoinColumnName = fieldMaps.fieldIdByJoinColumnName; } parse( @@ -67,10 +81,10 @@ export class GraphqlQueryOrderFieldParser { (acc, item) => { Object.entries(item).forEach(([fieldName, orderByDirection]) => { const fieldMetadataId = - this.objectMetadataMapItem.fieldIdByName[fieldName] || - this.objectMetadataMapItem.fieldIdByJoinColumnName[fieldName]; + this.fieldIdByName[fieldName] || + this.fieldIdByJoinColumnName[fieldName]; const fieldMetadata = - this.objectMetadataMapItem.fieldsById[fieldMetadataId]; + this.flatFieldMetadataMaps.byId[fieldMetadataId]; if (!fieldMetadata || orderByDirection === undefined) { throw new GraphqlQueryRunnerException( @@ -92,9 +106,7 @@ export class GraphqlQueryOrderFieldParser { const orderByCasting = this.getOptionalOrderByCasting(fieldMetadata); - const columnName = isFieldMetadataRelationOrMorphRelation( - fieldMetadata, - ) + const columnName = isMorphOrRelationFlatFieldMetadata(fieldMetadata) ? formatColumnNameForRelationField( fieldMetadata.name, fieldMetadata.settings, @@ -124,17 +136,19 @@ export class GraphqlQueryOrderFieldParser { }): Record[] { let parsedOrderBy: Record[] = []; + const fields = this.flatObjectMetadata.fieldMetadataIds + .map((id) => this.flatFieldMetadataMaps.byId[id]) + .filter(isDefined); + const availableAggregations: Record = - getAvailableAggregationsFromObjectFields( - Object.values(this.objectMetadataMapItem.fieldsById), - ); + getAvailableAggregationsFromObjectFields(fields); for (const orderByArg of orderBy) { if (this.isAggregateOrderByArg(orderByArg)) { const parsedAggregateOrderBy = this.parseAggregateOrderByArg( availableAggregations, orderByArg, - this.objectMetadataMapItem, + this.flatObjectMetadata, ); parsedOrderBy.push(parsedAggregateOrderBy); @@ -148,10 +162,8 @@ export class GraphqlQueryOrderFieldParser { } const fieldName = Object.keys(orderByArg)[0]; - const fieldMetadataId = - this.objectMetadataMapItem.fieldIdByName[fieldName]; - const fieldMetadata = - this.objectMetadataMapItem.fieldsById[fieldMetadataId]; + const fieldMetadataId = this.fieldIdByName[fieldName]; + const fieldMetadata = this.flatFieldMetadataMaps.byId[fieldMetadataId]; if (!isDefined(fieldMetadata)) { throw new UserInputError(`Cannot orderBy unknown field: ${fieldName}.`); @@ -162,7 +174,7 @@ export class GraphqlQueryOrderFieldParser { this.parseObjectRecordOrderByForScalarField({ groupByFields, orderByArg, - objectMetadataItemWithFieldMaps: this.objectMetadataMapItem, + flatObjectMetadata: this.flatObjectMetadata, fieldMetadata, }); @@ -218,7 +230,7 @@ export class GraphqlQueryOrderFieldParser { this.parseObjectRecordOrderByForCompositeField({ groupByFields, orderByArg, - objectMetadataItemWithFieldMaps: this.objectMetadataMapItem, + flatObjectMetadata: this.flatObjectMetadata, fieldMetadata, }); @@ -237,7 +249,7 @@ export class GraphqlQueryOrderFieldParser { } getOptionalOrderByCasting( - fieldMetadata: Pick, + fieldMetadata: Pick, ): string { if ( fieldMetadata.type === FieldMetadataType.SELECT || @@ -379,7 +391,7 @@ export class GraphqlQueryOrderFieldParser { parseAggregateOrderByArg = ( availableAggregations: Record, orderByArg: AggregateOrderByWithGroupByField, - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, ): Record => { const aggregate = orderByArg.aggregate; @@ -399,7 +411,7 @@ export class GraphqlQueryOrderFieldParser { const aggregateExpression = ProcessAggregateHelper.getAggregateExpression( aggregateField, - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, ); if (!isDefined(aggregateExpression)) { @@ -419,13 +431,13 @@ export class GraphqlQueryOrderFieldParser { parseObjectRecordOrderByForScalarField = ({ groupByFields, orderByArg, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, fieldMetadata, }: { groupByFields: GroupByField[]; orderByArg: ObjectRecordOrderByForScalarField; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; - fieldMetadata: FieldMetadataEntity; + flatObjectMetadata: FlatObjectMetadata; + fieldMetadata: FlatFieldMetadata; }): Record | null => { const fieldIsInGroupBy = groupByFields.some( (groupByField) => groupByField.fieldMetadata.id === fieldMetadata.id, @@ -445,7 +457,7 @@ export class GraphqlQueryOrderFieldParser { } return { - [`"${objectMetadataItemWithFieldMaps.nameSingular}"."${fieldMetadata.name}"${orderByCasting}`]: + [`"${flatObjectMetadata.nameSingular}"."${fieldMetadata.name}"${orderByCasting}`]: convertOrderByToFindOptionsOrder(orderByDirection), }; }; @@ -453,13 +465,13 @@ export class GraphqlQueryOrderFieldParser { parseObjectRecordOrderByForCompositeField = ({ groupByFields, orderByArg, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, fieldMetadata, }: { groupByFields: GroupByField[]; orderByArg: ObjectRecordOrderByForCompositeField; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; - fieldMetadata: FieldMetadataEntity; + flatObjectMetadata: FlatObjectMetadata; + fieldMetadata: FlatFieldMetadata; }): Record | null => { const fieldName = Object.keys(orderByArg)[0]; const orderBySubField = orderByArg[fieldName]; @@ -491,7 +503,7 @@ export class GraphqlQueryOrderFieldParser { return parseCompositeFieldForOrder( fieldMetadata, orderBySubField, - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, ); }; @@ -556,7 +568,7 @@ export class GraphqlQueryOrderFieldParser { }: { groupByFields: GroupByField[]; orderByArg: ObjectRecordOrderByForRelationField; - fieldMetadata: FieldMetadataEntity; + fieldMetadata: FlatFieldMetadata; }): Record | null => { const { associatedGroupByField, @@ -565,7 +577,8 @@ export class GraphqlQueryOrderFieldParser { } = prepareForOrderByRelationFieldParsing({ orderByArg, fieldMetadata, - objectMetadataMaps: this.objectMetadataMaps, + flatObjectMetadataMaps: this.flatObjectMetadataMaps, + flatFieldMetadataMaps: this.flatFieldMetadataMaps, groupByFields, }); @@ -709,9 +722,9 @@ export class GraphqlQueryOrderFieldParser { | ObjectRecordOrderByForCompositeField | ObjectRecordOrderByWithGroupByDateField | ObjectRecordOrderByForRelationField, - fieldMetadata: FieldMetadataEntity, + fieldMetadata: FlatFieldMetadata, ): orderByArg is ObjectRecordOrderByForRelationField => { - if (!isFieldMetadataRelationOrMorphRelation(fieldMetadata)) { + if (!isMorphOrRelationFlatFieldMetadata(fieldMetadata)) { return false; } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/utils/parse-composite-field-for-order.util.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/utils/parse-composite-field-for-order.util.ts index c57468e4221..be7def8deff 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/utils/parse-composite-field-for-order.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/utils/parse-composite-field-for-order.util.ts @@ -6,11 +6,11 @@ import { capitalize } from 'twenty-shared/utils'; import { type OrderByCondition } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser'; import { convertOrderByToFindOptionsOrder } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/utils/convert-order-by-to-find-options-order'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; import { type CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory'; export const parseCompositeFieldForOrder = ( - fieldMetadata: FieldMetadataEntity, + fieldMetadata: FlatFieldMetadata, // eslint-disable-next-line @typescript-eslint/no-explicit-any value: any, objectNameSingular: string, diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/utils/prepare-for-order-by-relation-field-parsing.util.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/utils/prepare-for-order-by-relation-field-parsing.util.ts index 75eb0d3c133..70e9538e0db 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/utils/prepare-for-order-by-relation-field-parsing.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/utils/prepare-for-order-by-relation-field-parsing.util.ts @@ -8,26 +8,24 @@ import { type GroupByRelationField, } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/types/group-by-field.types'; import { isGroupByRelationField } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/is-group-by-relation-field.util'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; export const prepareForOrderByRelationFieldParsing = ({ orderByArg, fieldMetadata, - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, groupByFields, }: { orderByArg: ObjectRecordOrderByForRelationField; - fieldMetadata: FieldMetadataEntity; - objectMetadataMaps?: ObjectMetadataMaps; + fieldMetadata: FlatFieldMetadata; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; groupByFields: GroupByField[]; }) => { - if (!objectMetadataMaps) { - throw new UserInputError( - `Object metadata maps not found for field ${fieldMetadata.name}`, - ); - } - const relationFieldName = Object.keys(orderByArg)[0]; const nestedFieldOrderByObject = orderByArg[relationFieldName]; @@ -57,17 +55,22 @@ export const prepareForOrderByRelationFieldParsing = ({ ); } - const targetObjectMetadataItem = - objectMetadataMaps?.byId[fieldMetadata.relationTargetObjectMetadataId]; + const targetObjectMetadata = + flatObjectMetadataMaps.byId[fieldMetadata.relationTargetObjectMetadataId]; - if (!isDefined(targetObjectMetadataItem)) { + if (!isDefined(targetObjectMetadata)) { throw new UserInputError( `Target object metadata item not found for field ${fieldMetadata.name}`, ); } - const nestedFieldMetadataId = - targetObjectMetadataItem?.fieldIdByName[nestedFieldName]; + const { fieldIdByName: targetFieldIdByName } = + buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + targetObjectMetadata, + ); + + const nestedFieldMetadataId = targetFieldIdByName[nestedFieldName]; if (!isDefined(nestedFieldMetadataId)) { throw new UserInputError( @@ -75,12 +78,11 @@ export const prepareForOrderByRelationFieldParsing = ({ ); } - const nestedFieldMetadata = - targetObjectMetadataItem?.fieldsById[nestedFieldMetadataId]; + const nestedFieldMetadata = flatFieldMetadataMaps.byId[nestedFieldMetadataId]; if (!isDefined(nestedFieldMetadata) || !isDefined(nestedFieldMetadataId)) { throw new UserInputError( - `Nested field "${nestedFieldName}" not found in target object "${targetObjectMetadataItem.nameSingular}"`, + `Nested field "${nestedFieldName}" not found in target object "${targetObjectMetadata.nameSingular}"`, ); } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-aggregate.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-aggregate.parser.ts index 6a57bc46582..f3fb4c620f1 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-aggregate.parser.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-aggregate.parser.ts @@ -1,21 +1,28 @@ +import { isDefined } from 'twenty-shared/utils'; + import { type GraphqlQuerySelectedFieldsResult } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser'; import { type AggregationField, getAvailableAggregationsFromObjectFields, } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; export class GraphqlQuerySelectedFieldsAggregateParser { parse( // eslint-disable-next-line @typescript-eslint/no-explicit-any graphqlSelectedFields: Partial>, - objectMetadataMapItem: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, accumulator: GraphqlQuerySelectedFieldsResult, ): void { + const fields = flatObjectMetadata.fieldMetadataIds + .map((id) => flatFieldMetadataMaps.byId[id]) + .filter(isDefined); + const availableAggregations: Record = - getAvailableAggregationsFromObjectFields( - Object.values(objectMetadataMapItem.fieldsById), - ); + getAvailableAggregationsFromObjectFields(fields); for (const selectedField of Object.keys(graphqlSelectedFields)) { const selectedAggregation = availableAggregations[selectedField]; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-relation.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-relation.parser.ts index fa0e1cd1120..2b2b14b91e4 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-relation.parser.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-relation.parser.ts @@ -1,20 +1,28 @@ +import { isDefined } from 'twenty-shared/utils'; + import { GraphqlQuerySelectedFieldsParser, type GraphqlQuerySelectedFieldsResult, } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser'; -import { getTargetObjectMetadataOrThrow } from 'src/engine/api/graphql/graphql-query-runner/utils/get-target-object-metadata.util'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; export class GraphqlQuerySelectedFieldsRelationParser { - private objectMetadataMaps: ObjectMetadataMaps; + private flatObjectMetadataMaps: FlatEntityMaps; + private flatFieldMetadataMaps: FlatEntityMaps; - constructor(objectMetadataMaps: ObjectMetadataMaps) { - this.objectMetadataMaps = objectMetadataMaps; + constructor( + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, + ) { + this.flatObjectMetadataMaps = flatObjectMetadataMaps; + this.flatFieldMetadataMaps = flatFieldMetadataMaps; } parseRelationField( - fieldMetadata: FieldMetadataEntity, + fieldMetadata: FlatFieldMetadata, fieldKey: string, // eslint-disable-next-line @typescript-eslint/no-explicit-any fieldValue: any, @@ -26,18 +34,24 @@ export class GraphqlQuerySelectedFieldsRelationParser { accumulator.relations[fieldKey] = true; - const targetObjectMetadata = getTargetObjectMetadataOrThrow( - fieldMetadata, - this.objectMetadataMaps, - ); + if (!isDefined(fieldMetadata.relationTargetObjectMetadataId)) { + throw new Error( + `Relation target object metadata id not found for field ${fieldMetadata.name}`, + ); + } + + const targetObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityId: fieldMetadata.relationTargetObjectMetadataId, + flatEntityMaps: this.flatObjectMetadataMaps, + }); const fieldParser = new GraphqlQuerySelectedFieldsParser( - this.objectMetadataMaps, + this.flatObjectMetadataMaps, + this.flatFieldMetadataMaps, ); const relationAccumulator = fieldParser.parse( fieldValue, targetObjectMetadata, - this.objectMetadataMaps, ); accumulator.select[fieldKey] = { diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser.ts index f832666b165..4f566194371 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser.ts @@ -6,12 +6,13 @@ import { capitalize, isDefined } from 'twenty-shared/utils'; import { GraphqlQuerySelectedFieldsAggregateParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-aggregate.parser'; import { GraphqlQuerySelectedFieldsRelationParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-relation.parser'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { type CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory'; -import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; export type GraphqlQuerySelectedFieldsResult = { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -25,18 +26,27 @@ export type GraphqlQuerySelectedFieldsResult = { export class GraphqlQuerySelectedFieldsParser { private graphqlQuerySelectedFieldsRelationParser: GraphqlQuerySelectedFieldsRelationParser; private aggregateParser: GraphqlQuerySelectedFieldsAggregateParser; + private flatObjectMetadataMaps: FlatEntityMaps; + private flatFieldMetadataMaps: FlatEntityMaps; - constructor(objectMetadataMaps: ObjectMetadataMaps) { + constructor( + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, + ) { + this.flatObjectMetadataMaps = flatObjectMetadataMaps; + this.flatFieldMetadataMaps = flatFieldMetadataMaps; this.graphqlQuerySelectedFieldsRelationParser = - new GraphqlQuerySelectedFieldsRelationParser(objectMetadataMaps); + new GraphqlQuerySelectedFieldsRelationParser( + flatObjectMetadataMaps, + flatFieldMetadataMaps, + ); this.aggregateParser = new GraphqlQuerySelectedFieldsAggregateParser(); } parse( // eslint-disable-next-line @typescript-eslint/no-explicit-any graphqlSelectedFields: Partial>, - objectMetadataMapItem: ObjectMetadataItemWithFieldMaps, - objectMetadataMaps: ObjectMetadataMaps, + flatObjectMetadata: FlatObjectMetadata, ): GraphqlQuerySelectedFieldsResult { const accumulator: GraphqlQuerySelectedFieldsResult = { select: {}, @@ -47,9 +57,8 @@ export class GraphqlQuerySelectedFieldsParser { if (this.isRootConnection(graphqlSelectedFields)) { this.parseConnectionField( graphqlSelectedFields, - objectMetadataMapItem, + flatObjectMetadata, accumulator, - objectMetadataMaps, ); return accumulator; @@ -57,15 +66,15 @@ export class GraphqlQuerySelectedFieldsParser { this.aggregateParser.parse( graphqlSelectedFields, - objectMetadataMapItem, + flatObjectMetadata, + this.flatFieldMetadataMaps, accumulator, ); this.parseRecordFields( graphqlSelectedFields, - objectMetadataMapItem, + flatObjectMetadata, accumulator, - objectMetadataMaps, ); return accumulator; @@ -74,15 +83,17 @@ export class GraphqlQuerySelectedFieldsParser { private parseRecordFields( // eslint-disable-next-line @typescript-eslint/no-explicit-any graphqlSelectedFields: Partial>, - objectMetadataMapItem: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, accumulator: GraphqlQuerySelectedFieldsResult, - objectMetadataMaps: ObjectMetadataMaps, ): void { - for (const fieldMetadata of Object.values( - objectMetadataMapItem.fieldsById, - )) { + for (const fieldMetadataId of flatObjectMetadata.fieldMetadataIds) { + const fieldMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityId: fieldMetadataId, + flatEntityMaps: this.flatFieldMetadataMaps, + }); + if ( - isFieldMetadataEntityOfType(fieldMetadata, FieldMetadataType.RELATION) + isFlatFieldMetadataOfType(fieldMetadata, FieldMetadataType.RELATION) ) { const joinColumnName = fieldMetadata.settings?.joinColumnName; @@ -111,13 +122,15 @@ export class GraphqlQuerySelectedFieldsParser { } if ( - isFieldMetadataEntityOfType( + isFlatFieldMetadataOfType( fieldMetadata, FieldMetadataType.MORPH_RELATION, ) ) { const targetObjectMetadata = - objectMetadataMaps.byId[fieldMetadata.relationTargetObjectMetadataId]; + this.flatObjectMetadataMaps.byId[ + fieldMetadata.relationTargetObjectMetadataId + ]; if ( !fieldMetadata.settings?.relationType || @@ -182,24 +195,19 @@ export class GraphqlQuerySelectedFieldsParser { private parseConnectionField( // eslint-disable-next-line @typescript-eslint/no-explicit-any graphqlSelectedFields: Partial>, - objectMetadataMapItem: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, accumulator: GraphqlQuerySelectedFieldsResult, - objectMetadataMaps: ObjectMetadataMaps, ): void { this.aggregateParser.parse( graphqlSelectedFields, - objectMetadataMapItem, + flatObjectMetadata, + this.flatFieldMetadataMaps, accumulator, ); const node = graphqlSelectedFields.edges.node; - this.parseRecordFields( - node, - objectMetadataMapItem, - accumulator, - objectMetadataMaps, - ); + this.parseRecordFields(node, flatObjectMetadata, accumulator); } private isRootConnection( @@ -210,7 +218,7 @@ export class GraphqlQuerySelectedFieldsParser { } private parseCompositeField( - fieldMetadata: FieldMetadataEntity, + fieldMetadata: FlatFieldMetadata, // eslint-disable-next-line @typescript-eslint/no-explicit-any fieldValue: any, // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts index 804f95ec6e8..62d70c022e7 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts @@ -6,10 +6,6 @@ import { type ObjectRecordOrderBy, } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; -import { - GraphqlQueryRunnerException, - GraphqlQueryRunnerExceptionCode, -} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; import { GraphqlQueryFilterConditionParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser'; import { GraphqlQueryOrderFieldParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser'; import { @@ -17,30 +13,35 @@ import { type GraphqlQuerySelectedFieldsResult, } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser'; import { type GroupByField } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/types/group-by-field.types'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; -import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { type WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder'; export class GraphqlQueryParser { - private objectMetadataMapItem: ObjectMetadataItemWithFieldMaps; - private objectMetadataMaps: ObjectMetadataMaps; + private flatObjectMetadata: FlatObjectMetadata; + private flatObjectMetadataMaps: FlatEntityMaps; + private flatFieldMetadataMaps: FlatEntityMaps; private filterConditionParser: GraphqlQueryFilterConditionParser; private orderFieldParser: GraphqlQueryOrderFieldParser; constructor( - objectMetadataMapItem: ObjectMetadataItemWithFieldMaps, - objectMetadataMaps: ObjectMetadataMaps, + flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, ) { - this.objectMetadataMapItem = objectMetadataMapItem; - this.objectMetadataMaps = objectMetadataMaps; + this.flatObjectMetadata = flatObjectMetadata; + this.flatObjectMetadataMaps = flatObjectMetadataMaps; + this.flatFieldMetadataMaps = flatFieldMetadataMaps; this.filterConditionParser = new GraphqlQueryFilterConditionParser( - this.objectMetadataMapItem, + this.flatObjectMetadata, + this.flatFieldMetadataMaps, ); this.orderFieldParser = new GraphqlQueryOrderFieldParser( - this.objectMetadataMapItem, - this.objectMetadataMaps, + this.flatObjectMetadata, + this.flatObjectMetadataMaps, + this.flatFieldMetadataMaps, ); } @@ -143,26 +144,14 @@ export class GraphqlQueryParser { // eslint-disable-next-line @typescript-eslint/no-explicit-any graphqlSelectedFields: Partial>, ): GraphqlQuerySelectedFieldsResult { - const objectMetadataMapItem = getObjectMetadataMapItemByNameSingular( - this.objectMetadataMaps, - this.objectMetadataMapItem.nameSingular, - ); - - if (!objectMetadataMapItem) { - throw new GraphqlQueryRunnerException( - `Could not find object metadata for ${this.objectMetadataMapItem.nameSingular}`, - GraphqlQueryRunnerExceptionCode.OBJECT_METADATA_NOT_FOUND, - ); - } - const selectedFieldsParser = new GraphqlQuerySelectedFieldsParser( - this.objectMetadataMaps, + this.flatObjectMetadataMaps, + this.flatFieldMetadataMaps, ); return selectedFieldsParser.parse( graphqlSelectedFields, - this.objectMetadataMapItem, - this.objectMetadataMaps, + this.flatObjectMetadata, ); } } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/resolvers/types/group-by-field.types.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/resolvers/types/group-by-field.types.ts index 944953e6c4d..6d09f12c61a 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/resolvers/types/group-by-field.types.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/resolvers/types/group-by-field.types.ts @@ -1,21 +1,21 @@ import { type ObjectRecordGroupByDateGranularity } from 'twenty-shared/types'; import { type FirstDayOfTheWeek } from 'twenty-shared/utils'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; export type GroupByRegularField = { - fieldMetadata: FieldMetadataEntity; + fieldMetadata: FlatFieldMetadata; subFieldName?: string; }; export type GroupByDateField = { - fieldMetadata: FieldMetadataEntity; + fieldMetadata: FlatFieldMetadata; subFieldName?: string; dateGranularity: ObjectRecordGroupByDateGranularity; weekStartDay?: FirstDayOfTheWeek; }; export type GroupByRelationField = { - fieldMetadata: FieldMetadataEntity; - nestedFieldMetadata: FieldMetadataEntity; + fieldMetadata: FlatFieldMetadata; + nestedFieldMetadata: FlatFieldMetadata; nestedSubFieldName?: string; dateGranularity?: ObjectRecordGroupByDateGranularity; weekStartDay?: FirstDayOfTheWeek; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/format-result-with-group-by-dimension-values.util.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/format-result-with-group-by-dimension-values.util.ts index c5259f5edad..50b7445f708 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/format-result-with-group-by-dimension-values.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/format-result-with-group-by-dimension-values.util.ts @@ -7,8 +7,9 @@ import { isDefined } from 'twenty-shared/utils'; import { type CommonGroupByOutputItem } from 'src/engine/api/common/types/common-group-by-output-item.type'; import { type GroupByDefinition } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/types/group-by-definition.type'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; export const formatResultWithGroupByDimensionValues = async ({ @@ -16,8 +17,9 @@ export const formatResultWithGroupByDimensionValues = async ({ groupByDefinitions, aggregateFieldNames, recordsResult, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, processRecord, }: { groupsResult: Record[]; @@ -25,8 +27,9 @@ export const formatResultWithGroupByDimensionValues = async ({ aggregateFieldNames: string[]; processRecord?: (record: ObjectRecord) => Promise; recordsResult?: Array>; - objectMetadataItemWithFieldMaps?: ObjectMetadataItemWithFieldMaps; - objectMetadataMaps?: ObjectMetadataMaps; + flatObjectMetadata?: FlatObjectMetadata; + flatObjectMetadataMaps?: FlatEntityMaps; + flatFieldMetadataMaps?: FlatEntityMaps; }): Promise => { const formattedResult: CommonGroupByOutputItem[] = []; @@ -34,8 +37,9 @@ export const formatResultWithGroupByDimensionValues = async ({ if (isDefined(recordsResult)) { if ( - !isDefined(objectMetadataItemWithFieldMaps) || - !isDefined(objectMetadataMaps) + !isDefined(flatObjectMetadata) || + !isDefined(flatObjectMetadataMaps) || + !isDefined(flatFieldMetadataMaps) ) { throw new Error('Metadata are required to format result'); } @@ -55,8 +59,9 @@ export const formatResultWithGroupByDimensionValues = async ({ await processRecord( formatResult( record, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, ), ), ), diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/parse-group-by-args.util.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/parse-group-by-args.util.ts index 65e0c623527..6acbb14dd56 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/parse-group-by-args.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/parse-group-by-args.util.ts @@ -1,5 +1,5 @@ -import { isDefined } from 'class-validator'; import { FieldMetadataType } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; import { type GroupByResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; @@ -7,19 +7,28 @@ import { type GroupByField } from 'src/engine/api/graphql/graphql-query-runner/g import { isGroupByDateFieldDefinition } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/is-group-by-date-field-definition.util'; import { parseGroupByRelationField } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/parse-group-by-relation-field.util'; import { validateSingleKeyForGroupByOrThrow } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/validate-single-key-for-group-by-or-throw.util'; -import { isFieldMetadataRelationOrMorphRelation } from 'src/engine/api/graphql/workspace-schema-builder/utils/is-field-metadata-relation-or-morph-relation.utils'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; +import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; export const parseGroupByArgs = ( args: GroupByResolverArgs, - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, - objectMetadataMaps: ObjectMetadataMaps, + flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, ): GroupByField[] => { const groupByFieldNames = args.groupBy; const groupByFields: GroupByField[] = []; + const { fieldIdByName, fieldIdByJoinColumnName } = + buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + flatObjectMetadata, + ); + for (const fieldNames of groupByFieldNames) { validateSingleKeyForGroupByOrThrow({ groupByKeys: Object.keys(fieldNames), @@ -29,17 +38,17 @@ export const parseGroupByArgs = ( for (const fieldName of Object.keys(fieldNames)) { const fieldMetadataId = - objectMetadataItemWithFieldMaps.fieldIdByName[fieldName] || - objectMetadataItemWithFieldMaps.fieldIdByJoinColumnName[fieldName]; - const fieldMetadata = - objectMetadataItemWithFieldMaps.fieldsById[fieldMetadataId]; + fieldIdByName[fieldName] || fieldIdByJoinColumnName[fieldName]; + const fieldMetadata = fieldMetadataId + ? flatFieldMetadataMaps.byId[fieldMetadataId] + : undefined; if (!isDefined(fieldMetadata) || !isDefined(fieldMetadataId)) { throw new Error(`Unidentified field in groupBy: ${fieldName}`); } const isGroupByRelationField = - isFieldMetadataRelationOrMorphRelation(fieldMetadata) && + isMorphOrRelationFlatFieldMetadata(fieldMetadata) && typeof fieldNames[fieldName] === 'object' && fieldNames[fieldName] !== null && !isGroupByDateFieldDefinition(fieldNames[fieldName]); @@ -50,7 +59,8 @@ export const parseGroupByArgs = ( fieldNames, fieldName, fieldMetadata, - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, groupByFields, }); diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/parse-group-by-relation-field.util.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/parse-group-by-relation-field.util.ts index bc1ed4326ba..832f1d72b7f 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/parse-group-by-relation-field.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/parse-group-by-relation-field.util.ts @@ -1,6 +1,6 @@ import { UserInputError } from 'apollo-server-core'; -import { isDefined } from 'class-validator'; import { FieldMetadataType } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; import { GraphqlQueryRunnerException, @@ -11,31 +11,41 @@ import { type DateFieldGroupByDefinition } from 'src/engine/api/graphql/graphql- import { type GroupByField } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/types/group-by-field.types'; import { isGroupByDateFieldDefinition } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/is-group-by-date-field-definition.util'; import { validateSingleKeyForGroupByOrThrow } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/validate-single-key-for-group-by-or-throw.util'; -import { getTargetObjectMetadataOrThrow } from 'src/engine/api/graphql/graphql-query-runner/utils/get-target-object-metadata.util'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; const getNestedFieldMetadataDetails = ({ fieldNames, fieldName, fieldMetadata, - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }: { fieldNames: Record; fieldName: string; - fieldMetadata: FieldMetadataEntity; - objectMetadataMaps: ObjectMetadataMaps; + fieldMetadata: FlatFieldMetadata; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; }) => { const nestedFieldGroupByDefinitions = fieldNames[fieldName] as | Record | Record | Record; - const targetObjectMetadata = getTargetObjectMetadataOrThrow( - fieldMetadata, - objectMetadataMaps, - ); + if (!isDefined(fieldMetadata.relationTargetObjectMetadataId)) { + throw new UserInputError( + `Relation target object metadata id not found for field ${fieldMetadata.name}`, + ); + } + + const targetObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityId: fieldMetadata.relationTargetObjectMetadataId, + flatEntityMaps: flatObjectMetadataMaps, + }); const nestedFieldNames = Object.keys(nestedFieldGroupByDefinitions); @@ -46,10 +56,13 @@ const getNestedFieldMetadataDetails = ({ }); const nestedFieldName = nestedFieldNames[0]; - const nestedFieldMetadataId = - targetObjectMetadata.fieldIdByName[nestedFieldName]; - const nestedFieldMetadata = - targetObjectMetadata.fieldsById[nestedFieldMetadataId]; + const { fieldIdByName } = buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + targetObjectMetadata, + ); + + const nestedFieldMetadataId = fieldIdByName[nestedFieldName]; + const nestedFieldMetadata = flatFieldMetadataMaps.byId[nestedFieldMetadataId]; if (!isDefined(nestedFieldMetadata) || !isDefined(nestedFieldMetadataId)) { throw new GraphqlQueryRunnerException( @@ -83,8 +96,8 @@ const handleNestedCompositeField = ({ }: { nestedFieldGroupByDefinition: CompositeFieldGroupByDefinition; nestedFieldName: string; - fieldMetadata: FieldMetadataEntity; - nestedFieldMetadata: FieldMetadataEntity; + fieldMetadata: FlatFieldMetadata; + nestedFieldMetadata: FlatFieldMetadata; groupByFields: GroupByField[]; }) => { if ( @@ -127,13 +140,15 @@ export const parseGroupByRelationField = ({ fieldNames, fieldName, fieldMetadata, - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, groupByFields, }: { fieldNames: Record; fieldName: string; - fieldMetadata: FieldMetadataEntity; - objectMetadataMaps: ObjectMetadataMaps; + fieldMetadata: FlatFieldMetadata; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; groupByFields: GroupByField[]; }) => { const { nestedFieldGroupByDefinition, nestedFieldMetadata, nestedFieldName } = @@ -141,7 +156,8 @@ export const parseGroupByRelationField = ({ fieldNames, fieldName, fieldMetadata, - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); // Handle date fields in nested relations @@ -150,8 +166,7 @@ export const parseGroupByRelationField = ({ nestedFieldMetadata.type === FieldMetadataType.DATE_TIME) && isGroupByDateFieldDefinition(nestedFieldGroupByDefinition) ) { - const dateFieldDefinition = - nestedFieldGroupByDefinition as DateFieldGroupByDefinition; + const dateFieldDefinition = nestedFieldGroupByDefinition; groupByFields.push({ fieldMetadata, diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/services/group-by-with-records.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/services/group-by-with-records.service.ts index 312f914aeb9..3cff2bfa5f2 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/services/group-by-with-records.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/services/group-by-with-records.service.ts @@ -18,8 +18,9 @@ import { formatResultWithGroupByDimensionValues } from 'src/engine/api/graphql/g import { getGroupLimit } from 'src/engine/api/graphql/graphql-query-runner/group-by/utils/get-group-limit.util'; import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper'; import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select'; -import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { type WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder'; import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; @@ -66,16 +67,18 @@ export class GroupByWithRecordsService { authContext, workspaceDataSource, rolePermissionConfig, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, repository, } = queryRunnerContext; const columnsToSelect = buildColumnsToSelect({ select: selectedFieldsResult.select, relations: selectedFieldsResult.relations, - objectMetadataItemWithFieldMaps, - objectMetadataMaps: objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); const queryBuilderWithPartitionBy = this.addPartitionByToQueryBuilder({ @@ -85,16 +88,18 @@ export class GroupByWithRecordsService { groupByDefinitions, repository, orderByForRecords, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); const recordsResult = await queryBuilderWithPartitionBy.getRawMany(); if (!isEmpty(selectedFieldsResult.relations)) { await this.processNestedRelationsHelper.processNestedRelations({ - objectMetadataMaps, - parentObjectMetadataItem: objectMetadataItemWithFieldMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + parentObjectMetadataItem: flatObjectMetadata, parentObjectRecords: recordsResult.flatMap((group) => group.records), parentObjectRecordsAggregatedValues: {}, relations: selectedFieldsResult.relations, @@ -112,13 +117,15 @@ export class GroupByWithRecordsService { recordsResult, groupByDefinitions, aggregateFieldNames: Object.keys(selectedFieldsResult.aggregate), - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, processRecord: (record: ObjectRecord) => this.commonResultGettersService.processRecord( record, - objectMetadataItemWithFieldMaps.id, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext.workspace.id, ), }); @@ -131,8 +138,9 @@ export class GroupByWithRecordsService { groupByDefinitions, repository, orderByForRecords, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }: { queryBuilderForSubQuery: WorkspaceSelectQueryBuilder; columnsToSelect: Record; @@ -140,8 +148,9 @@ export class GroupByWithRecordsService { groupByDefinitions: GroupByDefinition[]; repository: WorkspaceRepository; orderByForRecords: ObjectRecordOrderBy; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; - objectMetadataMaps: ObjectMetadataMaps; + flatObjectMetadata: FlatObjectMetadata; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; }): WorkspaceSelectQueryBuilder { const groupByExpressions = groupByDefinitions .map((def) => def.expression) @@ -157,7 +166,7 @@ export class GroupByWithRecordsService { queryBuilderForSubQuery, ); - const objectAlias = getObjectAlias(objectMetadataItemWithFieldMaps); + const objectAlias = getObjectAlias(flatObjectMetadata); const recordSelectWithAlias = Object.keys(columnsToSelect) .map((col) => `"${objectAlias}"."${col}" as "${SUB_QUERY_PREFIX}${col}"`) @@ -175,14 +184,15 @@ export class GroupByWithRecordsService { if (!isEmpty(orderByForRecords)) { const graphqlQueryParser = new GraphqlQueryParser( - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, ); graphqlQueryParser.applyOrderToBuilder( subQuery, orderByForRecords, - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, ); } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper.ts index 8e1fcaadee9..35f825a5051 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper.ts @@ -16,21 +16,29 @@ import { import { encodeCursor } from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util'; import { getTargetObjectMetadataOrThrow } from 'src/engine/api/graphql/graphql-query-runner/utils/get-target-object-metadata.util'; import { type AggregationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { isFieldMetadataTypeMorphRelation } from 'src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-morph-relation.util'; -import { isFieldMetadataTypeRelation } from 'src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-relation.util'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; -import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { type CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory'; import { isPlainObject } from 'src/utils/is-plain-object'; // TODO: Refacto-common - Rename CommonRecordsToGraphqlConnectionHelper export class ObjectRecordsToGraphqlConnectionHelper { - private objectMetadataMaps: ObjectMetadataMaps; + private flatObjectMetadataMaps: FlatEntityMaps; + private flatFieldMetadataMaps: FlatEntityMaps; + private objectIdByNameSingular: Record; - constructor(objectMetadataMaps: ObjectMetadataMaps) { - this.objectMetadataMaps = objectMetadataMaps; + constructor( + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, + objectIdByNameSingular: Record, + ) { + this.flatObjectMetadataMaps = flatObjectMetadataMaps; + this.flatFieldMetadataMaps = flatFieldMetadataMaps; + this.objectIdByNameSingular = objectIdByNameSingular; } public createConnection({ @@ -154,22 +162,22 @@ export class ObjectRecordsToGraphqlConnectionHelper { ); } - const objectMetadata = getObjectMetadataMapItemByNameSingular( - this.objectMetadataMaps, - objectName, - ); + const objectMetadataId = this.objectIdByNameSingular[objectName]; - if (!objectMetadata) { - throw new GraphqlQueryRunnerException( - `Object metadata not found for ${objectName}`, - GraphqlQueryRunnerExceptionCode.OBJECT_METADATA_NOT_FOUND, - ); - } + const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityId: objectMetadataId, + flatEntityMaps: this.flatObjectMetadataMaps, + }); // eslint-disable-next-line @typescript-eslint/no-explicit-any const processedObjectRecord: Record = {}; - for (const fieldMetadata of Object.values(objectMetadata.fieldsById)) { + for (const fieldId of flatObjectMetadata.fieldMetadataIds) { + const fieldMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityId: fieldId, + flatEntityMaps: this.flatFieldMetadataMaps, + }); + if (isCompositeFieldMetadataType(fieldMetadata.type)) { const objectValue = objectRecord[fieldMetadata.name]; @@ -183,75 +191,13 @@ export class ObjectRecordsToGraphqlConnectionHelper { continue; } - if (isFieldMetadataTypeRelation(fieldMetadata)) { - const fieldMetadataNameWithId = `${fieldMetadata.name}Id`; - - if (isDefined(objectRecord[fieldMetadataNameWithId])) { - processedObjectRecord[fieldMetadataNameWithId] = - objectRecord[fieldMetadataNameWithId]; - } - - const objectValue = objectRecord[fieldMetadata.name]; - - if (!isDefined(objectValue)) { - continue; - } - - if (Array.isArray(objectValue)) { - const targetObjectMetadata = getTargetObjectMetadataOrThrow( - fieldMetadata, - this.objectMetadataMaps, - ); - - processedObjectRecord[fieldMetadata.name] = this.createConnection({ - objectRecords: objectValue, - parentObjectRecord: objectRecord, - objectRecordsAggregatedValues: - objectRecordsAggregatedValues[fieldMetadata.name], - selectedAggregatedFields: - selectedAggregatedFields[fieldMetadata.name], - objectName: targetObjectMetadata.nameSingular, - take, - totalCount: - objectRecordsAggregatedValues[fieldMetadata.name]?.totalCount ?? - objectValue.length, - order, - hasNextPage: false, - hasPreviousPage: false, - depth: depth + 1, - }); - } else if (isPlainObject(objectValue)) { - const targetObjectMetadata = getTargetObjectMetadataOrThrow( - fieldMetadata, - this.objectMetadataMaps, - ); - - processedObjectRecord[fieldMetadata.name] = this.processRecord({ - objectRecord: objectValue, - objectRecordsAggregatedValues: - objectRecordsAggregatedValues[fieldMetadata.name], - selectedAggregatedFields: - selectedAggregatedFields[fieldMetadata.name], - objectName: targetObjectMetadata.nameSingular, - take, - totalCount, - order, - depth: depth + 1, - }); - } - continue; - } - - if (isFieldMetadataTypeMorphRelation(fieldMetadata)) { + if (isMorphOrRelationFlatFieldMetadata(fieldMetadata)) { const targetObjectMetadata = - this.objectMetadataMaps.byId[ + this.flatObjectMetadataMaps.byId[ fieldMetadata.relationTargetObjectMetadataId ]; - if ( - !fieldMetadata.settings?.relationType || - !isDefined(targetObjectMetadata) - ) { + if (!isDefined(targetObjectMetadata)) { continue; } @@ -287,9 +233,9 @@ export class ObjectRecordsToGraphqlConnectionHelper { depth: depth + 1, }); } else if (isPlainObject(objectValue)) { - const targetObjectMetadata = getTargetObjectMetadataOrThrow( + const targetObjectMetadataOrThrow = getTargetObjectMetadataOrThrow( fieldMetadata, - this.objectMetadataMaps, + this.flatObjectMetadataMaps, ); processedObjectRecord[fieldMetadata.name] = this.processRecord({ @@ -298,7 +244,7 @@ export class ObjectRecordsToGraphqlConnectionHelper { objectRecordsAggregatedValues[fieldMetadata.name], selectedAggregatedFields: selectedAggregatedFields[fieldMetadata.name], - objectName: targetObjectMetadata.nameSingular, + objectName: targetObjectMetadataOrThrow.nameSingular, take, totalCount, order, @@ -324,7 +270,7 @@ export class ObjectRecordsToGraphqlConnectionHelper { } private processCompositeField( - fieldMetadata: FieldMetadataEntity, + fieldMetadata: FlatFieldMetadata, // eslint-disable-next-line @typescript-eslint/no-explicit-any fieldValue: any, // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper.ts index 152d75eb3ac..7b22692a4c9 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper.ts @@ -11,13 +11,14 @@ import { } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; import { ProcessAggregateHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper'; import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select'; -import { getFieldMetadataFromGraphQLField } from 'src/engine/api/graphql/graphql-query-runner/utils/get-field-metadata-from-graphql-field.util'; import { getTargetObjectMetadataOrThrow } from 'src/engine/api/graphql/graphql-query-runner/utils/get-target-object-metadata.util'; import { type AggregationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util'; import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; -import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util'; +import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; +import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; +import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { type WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; import { type WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder'; import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; @@ -28,7 +29,8 @@ export class ProcessNestedRelationsV2Helper { constructor() {} public async processNestedRelations({ - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, parentObjectMetadataItem, parentObjectRecords, parentObjectRecordsAggregatedValues = {}, @@ -40,8 +42,9 @@ export class ProcessNestedRelationsV2Helper { rolePermissionConfig, selectedFields, }: { - objectMetadataMaps: ObjectMetadataMaps; - parentObjectMetadataItem: ObjectMetadataItemWithFieldMaps; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; + parentObjectMetadataItem: FlatObjectMetadata; parentObjectRecords: T[]; // eslint-disable-next-line @typescript-eslint/no-explicit-any parentObjectRecordsAggregatedValues?: Record; @@ -57,7 +60,8 @@ export class ProcessNestedRelationsV2Helper { const processRelationTasks = Object.entries(relations).map( ([sourceFieldName, nestedRelations]) => this.processRelation({ - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, parentObjectMetadataItem, parentObjectRecords, parentObjectRecordsAggregatedValues, @@ -79,7 +83,8 @@ export class ProcessNestedRelationsV2Helper { } private async processRelation({ - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, parentObjectMetadataItem, parentObjectRecords, parentObjectRecordsAggregatedValues, @@ -92,8 +97,9 @@ export class ProcessNestedRelationsV2Helper { rolePermissionConfig, selectedFields, }: { - objectMetadataMaps: ObjectMetadataMaps; - parentObjectMetadataItem: ObjectMetadataItemWithFieldMaps; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; + parentObjectMetadataItem: FlatObjectMetadata; parentObjectRecords: T[]; // eslint-disable-next-line @typescript-eslint/no-explicit-any parentObjectRecordsAggregatedValues: Record; @@ -106,12 +112,20 @@ export class ProcessNestedRelationsV2Helper { rolePermissionConfig?: RolePermissionConfig; selectedFields: Record; }): Promise { - const sourceFieldMetadata = getFieldMetadataFromGraphQLField({ - objectMetadataItem: parentObjectMetadataItem, - graphQLField: sourceFieldName, - objectMetadataMaps, + const { fieldIdByName } = buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + parentObjectMetadataItem, + ); + + const sourceFieldMetadata = findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: fieldIdByName[sourceFieldName], + flatEntityMaps: flatFieldMetadataMaps, }); + if (!sourceFieldMetadata) { + return; + } + if ( !isFieldMetadataEntityOfType( sourceFieldMetadata, @@ -136,7 +150,8 @@ export class ProcessNestedRelationsV2Helper { const relationType = sourceFieldMetadata.settings?.relationType; const { targetRelationName, targetObjectMetadata, targetRelation } = this.getTargetObjectMetadata({ - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, parentObjectMetadataItem, sourceFieldName, }); @@ -155,8 +170,9 @@ export class ProcessNestedRelationsV2Helper { const columnsToSelect = buildColumnsToSelect({ select: selectedFields, relations: nestedRelations, - objectMetadataItemWithFieldMaps: targetObjectMetadata, - objectMetadataMaps, + flatObjectMetadata: targetObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); targetObjectQueryBuilder = targetObjectQueryBuilder.setFindOptions({ @@ -208,23 +224,11 @@ export class ProcessNestedRelationsV2Helper { relationType, }); - const targetObjectMetadataItemWithFieldsMaps = - getObjectMetadataMapItemByNameSingular( - objectMetadataMaps, - targetObjectMetadata.nameSingular, - ); - - if (!targetObjectMetadataItemWithFieldsMaps) { - throw new GraphqlQueryRunnerException( - `Object ${targetObjectMetadata.nameSingular} not found`, - GraphqlQueryRunnerExceptionCode.OBJECT_METADATA_NOT_FOUND, - ); - } - if (Object.keys(nestedRelations).length > 0) { await this.processNestedRelations({ - objectMetadataMaps, - parentObjectMetadataItem: targetObjectMetadataItemWithFieldsMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + parentObjectMetadataItem: targetObjectMetadata, parentObjectRecords: relationResults as ObjectRecord[], parentObjectRecordsAggregatedValues: relationAggregatedFieldsResult, relations: nestedRelations as Record< @@ -242,23 +246,36 @@ export class ProcessNestedRelationsV2Helper { } private getTargetObjectMetadata({ - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, parentObjectMetadataItem, sourceFieldName, }: { - objectMetadataMaps: ObjectMetadataMaps; - parentObjectMetadataItem: ObjectMetadataItemWithFieldMaps; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; + parentObjectMetadataItem: FlatObjectMetadata; sourceFieldName: string; }) { - const targetFieldMetadata = getFieldMetadataFromGraphQLField({ - objectMetadataItem: parentObjectMetadataItem, - graphQLField: sourceFieldName, - objectMetadataMaps, + const { fieldIdByName } = buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + parentObjectMetadataItem, + ); + + const targetFieldMetadata = findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: fieldIdByName[sourceFieldName], + flatEntityMaps: flatFieldMetadataMaps, }); + if (!targetFieldMetadata) { + throw new GraphqlQueryRunnerException( + `Field ${sourceFieldName} not found on object ${parentObjectMetadataItem.nameSingular}`, + GraphqlQueryRunnerExceptionCode.FIELD_NOT_FOUND, + ); + } + const targetObjectMetadata = getTargetObjectMetadataOrThrow( targetFieldMetadata, - objectMetadataMaps, + flatObjectMetadataMaps, ); if ( @@ -272,14 +289,11 @@ export class ProcessNestedRelationsV2Helper { } const targetRelation = - objectMetadataMaps.byId[ - targetFieldMetadata.relationTargetObjectMetadataId - ]?.fieldsById[targetFieldMetadata.relationTargetFieldMetadataId]; + flatFieldMetadataMaps.byId[ + targetFieldMetadata.relationTargetFieldMetadataId + ]; - const targetRelationName = - objectMetadataMaps.byId[ - targetFieldMetadata.relationTargetObjectMetadataId - ]?.fieldsById[targetFieldMetadata.relationTargetFieldMetadataId]?.name; + const targetRelationName = targetRelation?.name; return { targetRelationName, targetObjectMetadata, targetRelation }; } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper.ts index bcc5c5df796..a048a06a3f5 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper.ts @@ -6,8 +6,9 @@ import { type FindOptionsRelations, type ObjectLiteral } from 'typeorm'; import { ProcessNestedRelationsV2Helper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper'; import { type AggregationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util'; import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { type WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; import { RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; @@ -18,7 +19,8 @@ export class ProcessNestedRelationsHelper { ) {} public async processNestedRelations({ - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, parentObjectMetadataItem, parentObjectRecords, parentObjectRecordsAggregatedValues = {}, @@ -30,8 +32,9 @@ export class ProcessNestedRelationsHelper { rolePermissionConfig, selectedFields, }: { - objectMetadataMaps: ObjectMetadataMaps; - parentObjectMetadataItem: ObjectMetadataItemWithFieldMaps; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; + parentObjectMetadataItem: FlatObjectMetadata; parentObjectRecords: T[]; // eslint-disable-next-line @typescript-eslint/no-explicit-any parentObjectRecordsAggregatedValues?: Record; @@ -45,7 +48,8 @@ export class ProcessNestedRelationsHelper { selectedFields: Record; }): Promise { return this.processNestedRelationsV2Helper.processNestedRelations({ - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, parentObjectMetadataItem, parentObjectRecords, parentObjectRecordsAggregatedValues, diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/__tests__/build-columns-to-select.spec.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/__tests__/build-columns-to-select.spec.ts index 0cfa2649094..96002fb0571 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/__tests__/build-columns-to-select.spec.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/__tests__/build-columns-to-select.spec.ts @@ -3,115 +3,185 @@ import { FieldMetadataType } from 'twenty-shared/types'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; describe('buildColumnsToSelect', () => { - const mockObjectMetadataItemWithFieldMaps: any = { - id: 'a55d8cad-4c3d-4c8a-82b5-539a36de5605', - standardId: '20202020-e674-48e5-a542-72570eee7213', - dataSourceId: 'd161a12f-1daa-4a0b-887c-96a48a3d9ecf', - nameSingular: 'person', - namePlural: 'people', - labelSingular: 'Person', - labelPlural: 'People', - description: 'A person', - icon: 'IconUser', - standardOverrides: null, - targetTableName: 'DEPRECATED', - isCustom: false, - isRemote: false, - isActive: true, - isSystem: false, - isAuditLogged: true, - isSearchable: true, - duplicateCriteria: [ - ['nameFirstName', 'nameLastName'], - ['linkedinLinkPrimaryLinkUrl'], - ['emailsPrimaryEmail'], - ], - shortcut: 'P', - labelIdentifierFieldMetadataId: '92414583-c6a6-4c98-bae7-6ce318bd3423', - imageIdentifierFieldMetadataId: '6bfcecc4-1866-4254-ba6b-6f22246819bb', - isLabelSyncedWithName: false, - workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419', - createdAt: new Date('2025-07-10T10:58:54.536Z'), - updatedAt: new Date('2025-07-10T10:58:54.536Z'), - indexMetadatas: [], - fieldsById: { - '92414583-c6a6-4c98-bae7-6ce318bd3423': { - id: '92414583-c6a6-4c98-bae7-6ce318bd3423', - type: FieldMetadataType.FULL_NAME, - name: 'name', - label: 'Name', - defaultValue: { - lastName: "''", - firstName: "''", - }, - description: "Contact's name", - icon: 'IconUser', - standardOverrides: null, - options: undefined, - settings: undefined, - isCustom: false, - isActive: true, - isSystem: false, - isNullable: true, - isUnique: false, - workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419', - isLabelSyncedWithName: true, - relationTargetFieldMetadataId: undefined, - relationTargetObjectMetadataId: undefined, - objectMetadataId: 'a55d8cad-4c3d-4c8a-82b5-539a36de5605', - createdAt: new Date('2025-07-10T10:58:54.536Z'), - updatedAt: new Date('2025-07-10T10:58:54.536Z'), - }, - '30dc1370-bb1a-42e3-abbc-a40edf1c6796': { - id: '30dc1370-bb1a-42e3-abbc-a40edf1c6796', - type: FieldMetadataType.RELATION, - name: 'company', - label: 'Company', - defaultValue: null, - description: "Contact's company", - icon: 'IconBuildingSkyscraper', - standardOverrides: null, - options: undefined, - settings: { - relationType: RelationType.MANY_TO_ONE, - joinColumnName: 'companyId', - }, - isCustom: false, - isActive: true, - isSystem: false, - isNullable: true, - isUnique: false, - workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419', - isLabelSyncedWithName: true, - relationTargetFieldMetadataId: '83cd9e7f-dfbc-4b93-a0a7-4c04b76d2009', - relationTargetObjectMetadataId: '9af20778-2f2c-4e22-ae83-2e77e479b57c', - objectMetadataId: 'a55d8cad-4c3d-4c8a-82b5-539a36de5605', - createdAt: new Date('2025-07-10T10:58:54.536Z'), - updatedAt: new Date('2025-07-10T10:58:54.536Z'), - }, - }, - fieldIdByName: { - name: '92414583-c6a6-4c98-bae7-6ce318bd3423', - company: '30dc1370-bb1a-42e3-abbc-a40edf1c6796', - }, - fieldIdByJoinColumnName: { - companyId: '30dc1370-bb1a-42e3-abbc-a40edf1c6796', - }, - }; + const workspaceId = '20202020-1c25-4d02-bf25-6aeccf7ea419'; + const personObjectId = 'a55d8cad-4c3d-4c8a-82b5-539a36de5605'; + const companyObjectId = '9af20778-2f2c-4e22-ae83-2e77e479b57c'; - const mockObjectMetadataMaps = { - byId: { - '9af20778-2f2c-4e22-ae83-2e77e479b57c': { - id: '9af20778-2f2c-4e22-ae83-2e77e479b57c', - nameSingular: 'company', - namePlural: 'companies', - }, + const nameFieldId = '92414583-c6a6-4c98-bae7-6ce318bd3423'; + const companyFieldId = '30dc1370-bb1a-42e3-abbc-a40edf1c6796'; + + const createMockField = ( + overrides: Partial & { + id: string; + name: string; + type: FieldMetadataType; + objectMetadataId: string; }, - }; + ): FlatFieldMetadata => + ({ + workspaceId, + isNullable: true, + isLabelSyncedWithName: true, + createdAt: new Date('2025-07-10T10:58:54.536Z'), + updatedAt: new Date('2025-07-10T10:58:54.536Z'), + universalIdentifier: overrides.id, + viewFieldIds: [], + viewFilterIds: [], + viewGroupIds: [], + kanbanAggregateOperationViewIds: [], + calendarViewIds: [], + applicationId: null, + label: overrides.name, + isCustom: false, + isActive: true, + isSystem: false, + isUnique: false, + ...overrides, + }) as FlatFieldMetadata; + + const nameField = createMockField({ + id: nameFieldId, + type: FieldMetadataType.FULL_NAME, + name: 'name', + label: 'Name', + objectMetadataId: personObjectId, + defaultValue: { + lastName: "''", + firstName: "''", + }, + description: "Contact's name", + icon: 'IconUser', + }); + + const companyRelationField = createMockField({ + id: companyFieldId, + type: FieldMetadataType.RELATION, + name: 'company', + label: 'Company', + objectMetadataId: personObjectId, + defaultValue: null, + description: "Contact's company", + icon: 'IconBuildingSkyscraper', + settings: { + relationType: RelationType.MANY_TO_ONE, + joinColumnName: 'companyId', + }, + relationTargetFieldMetadataId: '83cd9e7f-dfbc-4b93-a0a7-4c04b76d2009', + relationTargetObjectMetadataId: companyObjectId, + }); + + const buildMockFlatObjectMetadata = ( + fieldIds: string[], + ): FlatObjectMetadata => + ({ + id: personObjectId, + standardId: '20202020-e674-48e5-a542-72570eee7213', + nameSingular: 'person', + namePlural: 'people', + labelSingular: 'Person', + labelPlural: 'People', + description: 'A person', + icon: 'IconUser', + targetTableName: 'DEPRECATED', + isCustom: false, + isRemote: false, + isActive: true, + isSystem: false, + isAuditLogged: true, + isSearchable: true, + duplicateCriteria: [ + ['nameFirstName', 'nameLastName'], + ['linkedinLinkPrimaryLinkUrl'], + ['emailsPrimaryEmail'], + ], + shortcut: 'P', + labelIdentifierFieldMetadataId: nameFieldId, + imageIdentifierFieldMetadataId: '6bfcecc4-1866-4254-ba6b-6f22246819bb', + isLabelSyncedWithName: false, + workspaceId, + createdAt: new Date('2025-07-10T10:58:54.536Z'), + updatedAt: new Date('2025-07-10T10:58:54.536Z'), + universalIdentifier: personObjectId, + fieldMetadataIds: fieldIds, + indexMetadataIds: [], + viewIds: [], + applicationId: null, + }) as unknown as FlatObjectMetadata; + + const buildFlatFieldMetadataMaps = ( + fields: FlatFieldMetadata[], + ): FlatEntityMaps => ({ + byId: fields.reduce( + (acc, field) => { + acc[field.id] = field; + + return acc; + }, + {} as Record, + ), + idByUniversalIdentifier: fields.reduce( + (acc, field) => { + acc[field.universalIdentifier] = field.id; + + return acc; + }, + {} as Record, + ), + universalIdentifiersByApplicationId: {}, + }); + + const buildFlatObjectMetadataMaps = ( + objects: FlatObjectMetadata[], + ): FlatEntityMaps => ({ + byId: objects.reduce( + (acc, obj) => { + acc[obj.id] = obj; + + return acc; + }, + {} as Record, + ), + idByUniversalIdentifier: objects.reduce( + (acc, obj) => { + if (obj.universalIdentifier) { + acc[obj.universalIdentifier] = obj.id; + } + + return acc; + }, + {} as Record, + ), + universalIdentifiersByApplicationId: {}, + }); + + const companyObjectMetadata: FlatObjectMetadata = { + id: companyObjectId, + nameSingular: 'company', + namePlural: 'companies', + fieldMetadataIds: [], + indexMetadataIds: [], + viewIds: [], + } as unknown as FlatObjectMetadata; it('should build columns to select with relation fields', () => { + const flatObjectMetadata = buildMockFlatObjectMetadata([ + nameFieldId, + companyFieldId, + ]); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps([ + nameField, + companyRelationField, + ]); + const flatObjectMetadataMaps = buildFlatObjectMetadataMaps([ + flatObjectMetadata, + companyObjectMetadata, + ]); + const select = { nameFirstName: true, company: { @@ -127,8 +197,9 @@ describe('buildColumnsToSelect', () => { const result = buildColumnsToSelect({ select, relations, - objectMetadataItemWithFieldMaps: mockObjectMetadataItemWithFieldMaps, - objectMetadataMaps: mockObjectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); expect(result).toEqual({ @@ -139,6 +210,19 @@ describe('buildColumnsToSelect', () => { }); it('should build columns to select without relation fields', () => { + const flatObjectMetadata = buildMockFlatObjectMetadata([ + nameFieldId, + companyFieldId, + ]); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps([ + nameField, + companyRelationField, + ]); + const flatObjectMetadataMaps = buildFlatObjectMetadataMaps([ + flatObjectMetadata, + companyObjectMetadata, + ]); + const select = { nameFirstName: true, nameLastName: true, @@ -149,8 +233,9 @@ describe('buildColumnsToSelect', () => { const result = buildColumnsToSelect({ select, relations, - objectMetadataItemWithFieldMaps: mockObjectMetadataItemWithFieldMaps, - objectMetadataMaps: mockObjectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); expect(result).toEqual({ @@ -161,6 +246,19 @@ describe('buildColumnsToSelect', () => { }); it('should filter out non-boolean values from select', () => { + const flatObjectMetadata = buildMockFlatObjectMetadata([ + nameFieldId, + companyFieldId, + ]); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps([ + nameField, + companyRelationField, + ]); + const flatObjectMetadataMaps = buildFlatObjectMetadataMaps([ + flatObjectMetadata, + companyObjectMetadata, + ]); + const select = { nameFirstName: true, nameLastName: false, @@ -172,8 +270,9 @@ describe('buildColumnsToSelect', () => { const result = buildColumnsToSelect({ select, relations, - objectMetadataItemWithFieldMaps: mockObjectMetadataItemWithFieldMaps, - objectMetadataMaps: mockObjectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); expect(result).toEqual({ @@ -183,6 +282,19 @@ describe('buildColumnsToSelect', () => { }); it('should handle relation field that is not a relation type', () => { + const flatObjectMetadata = buildMockFlatObjectMetadata([ + nameFieldId, + companyFieldId, + ]); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps([ + nameField, + companyRelationField, + ]); + const flatObjectMetadataMaps = buildFlatObjectMetadataMaps([ + flatObjectMetadata, + companyObjectMetadata, + ]); + const select = { nameFirstName: true, }; @@ -194,8 +306,9 @@ describe('buildColumnsToSelect', () => { const result = buildColumnsToSelect({ select, relations, - objectMetadataItemWithFieldMaps: mockObjectMetadataItemWithFieldMaps, - objectMetadataMaps: mockObjectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); expect(result).toEqual({ @@ -205,21 +318,32 @@ describe('buildColumnsToSelect', () => { }); it('should handle relation field that is not MANY_TO_ONE', () => { - const mockObjectMetadataWithOneToMany: any = { - ...mockObjectMetadataItemWithFieldMaps, - fieldsById: { - ...mockObjectMetadataItemWithFieldMaps.fieldsById, - '30dc1370-bb1a-42e3-abbc-a40edf1c6796': { - ...mockObjectMetadataItemWithFieldMaps.fieldsById[ - '30dc1370-bb1a-42e3-abbc-a40edf1c6796' - ], - settings: { - relationType: RelationType.ONE_TO_MANY, - joinColumnName: 'companyId', - }, - }, + const oneToManyCompanyField = createMockField({ + id: companyFieldId, + type: FieldMetadataType.RELATION, + name: 'company', + label: 'Company', + objectMetadataId: personObjectId, + defaultValue: null, + settings: { + relationType: RelationType.ONE_TO_MANY, + joinColumnName: 'companyId', }, - }; + relationTargetObjectMetadataId: companyObjectId, + }); + + const flatObjectMetadata = buildMockFlatObjectMetadata([ + nameFieldId, + companyFieldId, + ]); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps([ + nameField, + oneToManyCompanyField, + ]); + const flatObjectMetadataMaps = buildFlatObjectMetadataMaps([ + flatObjectMetadata, + companyObjectMetadata, + ]); const select = { nameFirstName: true, @@ -232,8 +356,9 @@ describe('buildColumnsToSelect', () => { const result = buildColumnsToSelect({ select, relations, - objectMetadataItemWithFieldMaps: mockObjectMetadataWithOneToMany, - objectMetadataMaps: mockObjectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); expect(result).toEqual({ @@ -243,21 +368,32 @@ describe('buildColumnsToSelect', () => { }); it('should handle relation field without joinColumnName', () => { - const mockObjectMetadataWithoutJoinColumn: any = { - ...mockObjectMetadataItemWithFieldMaps, - fieldsById: { - ...mockObjectMetadataItemWithFieldMaps.fieldsById, - '30dc1370-bb1a-42e3-abbc-a40edf1c6796': { - ...mockObjectMetadataItemWithFieldMaps.fieldsById[ - '30dc1370-bb1a-42e3-abbc-a40edf1c6796' - ], - settings: { - relationType: RelationType.MANY_TO_ONE, - joinColumnName: null, - }, - }, + const noJoinColumnCompanyField = createMockField({ + id: companyFieldId, + type: FieldMetadataType.RELATION, + name: 'company', + label: 'Company', + objectMetadataId: personObjectId, + defaultValue: null, + settings: { + relationType: RelationType.MANY_TO_ONE, + joinColumnName: null, }, - }; + relationTargetObjectMetadataId: companyObjectId, + }); + + const flatObjectMetadata = buildMockFlatObjectMetadata([ + nameFieldId, + companyFieldId, + ]); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps([ + nameField, + noJoinColumnCompanyField, + ]); + const flatObjectMetadataMaps = buildFlatObjectMetadataMaps([ + flatObjectMetadata, + companyObjectMetadata, + ]); const select = { nameFirstName: true, @@ -270,8 +406,9 @@ describe('buildColumnsToSelect', () => { const result = buildColumnsToSelect({ select, relations, - objectMetadataItemWithFieldMaps: mockObjectMetadataWithoutJoinColumn, - objectMetadataMaps: mockObjectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); expect(result).toEqual({ diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return.ts index c4a7ab3a4d8..c647e449cf2 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return.ts @@ -1,24 +1,28 @@ import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; export const buildColumnsToReturn = ({ select, relations, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }: { select: Record; relations: Record; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; - objectMetadataMaps: ObjectMetadataMaps; + flatObjectMetadata: FlatObjectMetadata; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; }): string[] => { return Object.entries( buildColumnsToSelect({ select, relations, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }), ) .filter(([_columnName, value]) => value === true) diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select.ts index ad025dcd7e9..261d2bc6218 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select.ts @@ -1,32 +1,32 @@ +import { FieldMetadataType } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; -import { isFieldMetadataTypeMorphRelation } from 'src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-morph-relation.util'; -import { isFieldMetadataTypeRelation } from 'src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-relation.util'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; + export const buildColumnsToSelect = ({ select, relations, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }: { select: Record; relations: Record; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; - objectMetadataMaps: { - byId: Partial< - Record< - string, - Pick - > - >; - }; + flatObjectMetadata: FlatObjectMetadata; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; }) => { const requiredRelationColumns = getRequiredRelationColumns( relations, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, ); const fieldsToSelect: Record = Object.entries(select) @@ -44,20 +44,19 @@ export const buildColumnsToSelect = ({ const getRequiredRelationColumns = ( relations: Record, - objectMetadataItem: Pick, - objectMetadataMaps: { - byId: Partial< - Record< - string, - Pick - > - >; - }, + flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, ): string[] => { const requiredColumns: string[] = []; - for (const fieldMetadata of Object.values(objectMetadataItem.fieldsById)) { - if (isFieldMetadataTypeRelation(fieldMetadata)) { + for (const fieldId of flatObjectMetadata.fieldMetadataIds) { + const fieldMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityId: fieldId, + flatEntityMaps: flatFieldMetadataMaps, + }); + + if (isFlatFieldMetadataOfType(fieldMetadata, FieldMetadataType.RELATION)) { const relationValue = relations[fieldMetadata.name]; if ( @@ -71,9 +70,14 @@ const getRequiredRelationColumns = ( requiredColumns.push(fieldMetadata.settings.joinColumnName); } - if (isFieldMetadataTypeMorphRelation(fieldMetadata)) { - const targetObjectMetadata = - objectMetadataMaps.byId[fieldMetadata.relationTargetObjectMetadataId]; + if ( + isFlatFieldMetadataOfType(fieldMetadata, FieldMetadataType.MORPH_RELATION) + ) { + const targetObjectMetadata = fieldMetadata.relationTargetObjectMetadataId + ? flatObjectMetadataMaps.byId[ + fieldMetadata.relationTargetObjectMetadataId + ] + : undefined; if ( !fieldMetadata.settings?.relationType || diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/get-field-metadata-from-graphql-field.util.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/get-field-metadata-from-graphql-field.util.ts index 48819f701ed..1c6bfb479e4 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/get-field-metadata-from-graphql-field.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/get-field-metadata-from-graphql-field.util.ts @@ -2,56 +2,71 @@ import { FieldMetadataType } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { getTargetObjectMetadataOrThrow } from 'src/engine/api/graphql/graphql-query-runner/utils/get-target-object-metadata.util'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { isFieldMetadataTypeMorphRelation } from 'src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-morph-relation.util'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; +import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; type GetFieldMetadataFromGraphQLFieldArgs = { - objectMetadataItem: ObjectMetadataItemWithFieldMaps; + flatObjectMetadata: FlatObjectMetadata; graphQLField: string; - objectMetadataMaps: ObjectMetadataMaps; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; }; export function getFieldMetadataFromGraphQLField({ - objectMetadataItem, + flatObjectMetadata, graphQLField, - objectMetadataMaps, -}: GetFieldMetadataFromGraphQLFieldArgs) { - const sourceFieldMetadataId = objectMetadataItem.fieldIdByName[graphQLField]; - let sourceFieldMetadata = - objectMetadataItem.fieldsById[sourceFieldMetadataId]; + flatObjectMetadataMaps, + flatFieldMetadataMaps, +}: GetFieldMetadataFromGraphQLFieldArgs): FlatFieldMetadata | undefined { + const { fieldIdByName } = buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + flatObjectMetadata, + ); + + const sourceFieldMetadataId = fieldIdByName[graphQLField]; + let sourceFieldMetadata = sourceFieldMetadataId + ? flatFieldMetadataMaps.byId[sourceFieldMetadataId] + : undefined; // If empty, it could be a morph relation if (!isDefined(sourceFieldMetadata)) { - const morphRelationsWithTargetObjectMetadata = Object.values( - objectMetadataItem.fieldsById, - ) - .filter( - (fieldMetadata) => - fieldMetadata.type === FieldMetadataType.MORPH_RELATION, + const morphRelationsWithTargetObjectMetadata = + getFlatFieldsFromFlatObjectMetadata( + flatObjectMetadata, + flatFieldMetadataMaps, ) - .map((fieldMetadata) => { - const targetObjectMetadata = getTargetObjectMetadataOrThrow( - fieldMetadata, - objectMetadataMaps, - ); + .filter( + (fieldMetadata) => + fieldMetadata.type === FieldMetadataType.MORPH_RELATION, + ) + .map((fieldMetadata) => { + const targetObjectMetadata = getTargetObjectMetadataOrThrow( + fieldMetadata, + flatObjectMetadataMaps, + ); - return { - fieldMetadata, - targetObjectMetadata, - }; - }); + return { + fieldMetadata, + targetObjectMetadata, + }; + }); const possibleGraphQLFieldNames: { graphQLField: string; - fieldMetadata: FieldMetadataEntity; - targetObjectMetadata: ObjectMetadataItemWithFieldMaps; + fieldMetadata: FlatFieldMetadata; + targetObjectMetadata: FlatObjectMetadata; }[] = []; morphRelationsWithTargetObjectMetadata.map((morphRelation) => { if ( - !isFieldMetadataTypeMorphRelation(morphRelation.fieldMetadata) || + !isFlatFieldMetadataOfType( + morphRelation.fieldMetadata, + FieldMetadataType.MORPH_RELATION, + ) || !morphRelation.fieldMetadata.settings?.relationType ) { return; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/get-target-object-metadata.util.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/get-target-object-metadata.util.ts index 9efd28327ba..72a4ab6bf66 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/get-target-object-metadata.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/get-target-object-metadata.util.ts @@ -2,13 +2,14 @@ import { GraphqlQueryRunnerException, GraphqlQueryRunnerExceptionCode, } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; export const getTargetObjectMetadataOrThrow = ( - fieldMetadata: FieldMetadataEntity, - objectMetadataMaps: ObjectMetadataMaps, -) => { + fieldMetadata: FlatFieldMetadata, + flatObjectMetadataMaps: FlatEntityMaps, +): FlatObjectMetadata => { if (!fieldMetadata.relationTargetObjectMetadataId) { throw new GraphqlQueryRunnerException( `Relation target object metadata id not found for field ${fieldMetadata.name}`, @@ -17,7 +18,7 @@ export const getTargetObjectMetadataOrThrow = ( } const targetObjectMetadata = - objectMetadataMaps.byId[fieldMetadata.relationTargetObjectMetadataId]; + flatObjectMetadataMaps.byId[fieldMetadata.relationTargetObjectMetadataId]; if (!targetObjectMetadata) { throw new GraphqlQueryRunnerException( diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface.ts deleted file mode 100644 index 6ea833fc9a8..00000000000 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { type GraphQLResolveInfo } from 'graphql'; - -import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; - -export interface WorkspaceQueryRunnerOptions { - authContext: AuthContext; - objectMetadataMaps: ObjectMetadataMaps; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; - info: GraphQLResolveInfo; -} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/handle-duplicate-key-error.util.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/handle-duplicate-key-error.util.ts index dae563cee80..12349f31676 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/handle-duplicate-key-error.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/handle-duplicate-key-error.util.ts @@ -1,9 +1,7 @@ import { msg } from '@lingui/core/macro'; -import { isDefined } from 'twenty-shared/utils'; import { type QueryFailedError } from 'typeorm'; -import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { TwentyORMException, TwentyORMExceptionCode, @@ -14,64 +12,16 @@ interface PostgreSQLError extends QueryFailedError { } export const handleDuplicateKeyError = ( - error: PostgreSQLError, - objectMetadata: ObjectMetadataItemWithFieldMaps, + _error: PostgreSQLError, + _objectMetadata: FlatObjectMetadata, ) => { - const indexNameMatch = error.message.match(/"([^"]+)"/); - - const duplicatedValues = error?.detail?.match(/=\(([^)]+)\)/)?.[1]; - - if (indexNameMatch) { - const indexName = indexNameMatch[1]; - - const deletedAtFieldMetadataId = objectMetadata.fieldIdByName['deletedAt']; - - const affectedColumns = objectMetadata.indexMetadatas - .find((index) => index.name === indexName) - ?.indexFieldMetadatas?.filter( - (field) => field.fieldMetadataId !== deletedAtFieldMetadataId, - ) - .map((indexField) => { - const fieldMetadata = - objectMetadata.fieldsById[indexField.fieldMetadataId]; - - return fieldMetadata?.label; - }); - - if (!isDefined(affectedColumns)) { - throw new TwentyORMException( - `A duplicate entry was detected`, - TwentyORMExceptionCode.DUPLICATE_ENTRY_DETECTED, - { - userFriendlyMessage: msg`This record already exists. Please check your data and try again.`, - }, - ); - } - - const columnNames = affectedColumns.join(', '); - - if (affectedColumns?.length === 1) { - const fieldName = columnNames.toLowerCase(); - - throw new UserInputError( - `Duplicate ${columnNames} ${duplicatedValues ? `with value ${duplicatedValues}` : ''}. Please set a unique one.`, - { - userFriendlyMessage: duplicatedValues - ? msg`This ${fieldName} with value ${duplicatedValues} is already taken. Please choose a different value.` - : msg`This ${fieldName} is already taken. Please choose a different value.`, - isExpected: true, - }, - ); - } - - const fieldNames = columnNames.toLowerCase(); - - throw new TwentyORMException( - `A duplicate entry was detected. The combination of ${columnNames} must be unique.`, - TwentyORMExceptionCode.DUPLICATE_ENTRY_DETECTED, - { - userFriendlyMessage: msg`This combination of ${fieldNames} already exists. Please use different values.`, - }, - ); - } + // Since we no longer have indexMetadatas in FlatObjectMetadata, + // we provide a generic error message + throw new TwentyORMException( + `A duplicate entry was detected`, + TwentyORMExceptionCode.DUPLICATE_ENTRY_DETECTED, + { + userFriendlyMessage: msg`This record already exists. Please check your data and try again.`, + }, + ); }; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-many-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-many-resolver.factory.ts index d02a8d8d094..bd1296f080b 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-many-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-many-resolver.factory.ts @@ -41,14 +41,15 @@ export class CreateManyResolverFactory const typeORMObjectRecordsParser = new ObjectRecordsToGraphqlConnectionHelper( - internalContext.objectMetadataMaps, + internalContext.flatObjectMetadataMaps, + internalContext.flatFieldMetadataMaps, + internalContext.objectIdByNameSingular, ); return records.map((record: ObjectRecord) => typeORMObjectRecordsParser.processRecord({ objectRecord: record, - objectName: - internalContext.objectMetadataItemWithFieldMaps.nameSingular, + objectName: internalContext.flatObjectMetadata.nameSingular, take: 1, totalCount: 1, }), diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-one-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-one-resolver.factory.ts index ecee4e26ab0..6550acccc6e 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-one-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-one-resolver.factory.ts @@ -40,13 +40,14 @@ export class CreateOneResolverFactory const typeORMObjectRecordsParser = new ObjectRecordsToGraphqlConnectionHelper( - internalContext.objectMetadataMaps, + internalContext.flatObjectMetadataMaps, + internalContext.flatFieldMetadataMaps, + internalContext.objectIdByNameSingular, ); return typeORMObjectRecordsParser.processRecord({ objectRecord: record, - objectName: - internalContext.objectMetadataItemWithFieldMaps.nameSingular, + objectName: internalContext.flatObjectMetadata.nameSingular, take: 1, totalCount: 1, }); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-many-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-many-resolver.factory.ts index febdd06de2b..608ccf58133 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-many-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-many-resolver.factory.ts @@ -41,14 +41,15 @@ export class DeleteManyResolverFactory const typeORMObjectRecordsParser = new ObjectRecordsToGraphqlConnectionHelper( - internalContext.objectMetadataMaps, + internalContext.flatObjectMetadataMaps, + internalContext.flatFieldMetadataMaps, + internalContext.objectIdByNameSingular, ); return records.map((record: ObjectRecord) => typeORMObjectRecordsParser.processRecord({ objectRecord: record, - objectName: - internalContext.objectMetadataItemWithFieldMaps.nameSingular, + objectName: internalContext.flatObjectMetadata.nameSingular, take: 1, totalCount: 1, }), diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-one-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-one-resolver.factory.ts index 6d829828478..1bc0ae1acf8 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-one-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-one-resolver.factory.ts @@ -40,13 +40,14 @@ export class DeleteOneResolverFactory const typeORMObjectRecordsParser = new ObjectRecordsToGraphqlConnectionHelper( - internalContext.objectMetadataMaps, + internalContext.flatObjectMetadataMaps, + internalContext.flatFieldMetadataMaps, + internalContext.objectIdByNameSingular, ); return typeORMObjectRecordsParser.processRecord({ objectRecord: record, - objectName: - internalContext.objectMetadataItemWithFieldMaps.nameSingular, + objectName: internalContext.flatObjectMetadata.nameSingular, take: 1, totalCount: 1, }); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory.ts index 1a48865f823..46f57ecfc2e 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory.ts @@ -41,14 +41,15 @@ export class DestroyManyResolverFactory const typeORMObjectRecordsParser = new ObjectRecordsToGraphqlConnectionHelper( - internalContext.objectMetadataMaps, + internalContext.flatObjectMetadataMaps, + internalContext.flatFieldMetadataMaps, + internalContext.objectIdByNameSingular, ); return records.map((record: ObjectRecord) => typeORMObjectRecordsParser.processRecord({ objectRecord: record, - objectName: - internalContext.objectMetadataItemWithFieldMaps.nameSingular, + objectName: internalContext.flatObjectMetadata.nameSingular, take: 1, totalCount: 1, }), diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-one-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-one-resolver.factory.ts index 86a33573cc4..d7afd06d553 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-one-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-one-resolver.factory.ts @@ -40,13 +40,14 @@ export class DestroyOneResolverFactory const typeORMObjectRecordsParser = new ObjectRecordsToGraphqlConnectionHelper( - internalContext.objectMetadataMaps, + internalContext.flatObjectMetadataMaps, + internalContext.flatFieldMetadataMaps, + internalContext.objectIdByNameSingular, ); return typeORMObjectRecordsParser.processRecord({ objectRecord: record, - objectName: - internalContext.objectMetadataItemWithFieldMaps.nameSingular, + objectName: internalContext.flatObjectMetadata.nameSingular, take: 1, totalCount: 1, }); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-duplicates-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-duplicates-resolver.factory.ts index b964ae4ba4c..15f6192899f 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-duplicates-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-duplicates-resolver.factory.ts @@ -42,14 +42,15 @@ export class FindDuplicatesResolverFactory const typeORMObjectRecordsParser = new ObjectRecordsToGraphqlConnectionHelper( - internalContext.objectMetadataMaps, + internalContext.flatObjectMetadataMaps, + internalContext.flatFieldMetadataMaps, + internalContext.objectIdByNameSingular, ); return paginatedDuplicates.map((duplicate) => typeORMObjectRecordsParser.createConnection({ objectRecords: duplicate.records, - objectName: - internalContext.objectMetadataItemWithFieldMaps.nameSingular, + objectName: internalContext.flatObjectMetadata.nameSingular, take: duplicate.records.length, totalCount: duplicate.totalCount, order: [{ id: OrderByDirection.AscNullsFirst }], diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-many-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-many-resolver.factory.ts index 3c71ed3efb5..f9b173ace9a 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-many-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-many-resolver.factory.ts @@ -47,15 +47,16 @@ export class FindManyResolverFactory const typeORMObjectRecordsParser = new ObjectRecordsToGraphqlConnectionHelper( - internalContext.objectMetadataMaps, + internalContext.flatObjectMetadataMaps, + internalContext.flatFieldMetadataMaps, + internalContext.objectIdByNameSingular, ); return typeORMObjectRecordsParser.createConnection({ objectRecords: records, objectRecordsAggregatedValues: aggregatedValues, selectedAggregatedFields: selectedFieldsResult.aggregate, - objectName: - internalContext.objectMetadataItemWithFieldMaps.nameSingular, + objectName: internalContext.flatObjectMetadata.nameSingular, take: args.first ?? args.last ?? QUERY_MAX_RECORDS, totalCount, order: args.orderBy, diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-one-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-one-resolver.factory.ts index 6e82e7389e0..fe8160001c5 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-one-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-one-resolver.factory.ts @@ -40,13 +40,14 @@ export class FindOneResolverFactory const typeORMObjectRecordsParser = new ObjectRecordsToGraphqlConnectionHelper( - internalContext.objectMetadataMaps, + internalContext.flatObjectMetadataMaps, + internalContext.flatFieldMetadataMaps, + internalContext.objectIdByNameSingular, ); return typeORMObjectRecordsParser.processRecord({ objectRecord: record, - objectName: - internalContext.objectMetadataItemWithFieldMaps.nameSingular, + objectName: internalContext.flatObjectMetadata.nameSingular, take: 1, totalCount: 1, }); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/group-by-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/group-by-resolver.factory.ts index 5710f187b05..946b216c24f 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/group-by-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/group-by-resolver.factory.ts @@ -40,7 +40,9 @@ export class GroupByResolverFactory try { const typeORMObjectRecordsParser = new ObjectRecordsToGraphqlConnectionHelper( - internalContext.objectMetadataMaps, + internalContext.flatObjectMetadataMaps, + internalContext.flatFieldMetadataMaps, + internalContext.objectIdByNameSingular, ); const results = await this.commonGroupByQueryRunnerService.execute( @@ -51,8 +53,7 @@ export class GroupByResolverFactory const formattedResults = results.map((group) => { const formattedRecords = typeORMObjectRecordsParser.createConnection({ objectRecords: group.records ?? [], - objectName: - internalContext.objectMetadataItemWithFieldMaps.nameSingular, + objectName: internalContext.flatObjectMetadata.nameSingular, objectRecordsAggregatedValues: {}, selectedAggregatedFields: {}, take: group.records?.length || 0, diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/merge-many-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/merge-many-resolver.factory.ts index dd2adec1810..86345942d5f 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/merge-many-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/merge-many-resolver.factory.ts @@ -40,13 +40,14 @@ export class MergeManyResolverFactory const typeORMObjectRecordsParser = new ObjectRecordsToGraphqlConnectionHelper( - internalContext.objectMetadataMaps, + internalContext.flatObjectMetadataMaps, + internalContext.flatFieldMetadataMaps, + internalContext.objectIdByNameSingular, ); return typeORMObjectRecordsParser.processRecord({ objectRecord: record, - objectName: - internalContext.objectMetadataItemWithFieldMaps.nameSingular, + objectName: internalContext.flatObjectMetadata.nameSingular, take: 1, totalCount: 1, }); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/restore-many-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/restore-many-resolver.factory.ts index 6c5b732674d..444fee1a9b6 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/restore-many-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/restore-many-resolver.factory.ts @@ -41,14 +41,15 @@ export class RestoreManyResolverFactory const typeORMObjectRecordsParser = new ObjectRecordsToGraphqlConnectionHelper( - internalContext.objectMetadataMaps, + internalContext.flatObjectMetadataMaps, + internalContext.flatFieldMetadataMaps, + internalContext.objectIdByNameSingular, ); return records.map((record: ObjectRecord) => typeORMObjectRecordsParser.processRecord({ objectRecord: record, - objectName: - internalContext.objectMetadataItemWithFieldMaps.nameSingular, + objectName: internalContext.flatObjectMetadata.nameSingular, take: 1, totalCount: 1, }), diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/restore-one-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/restore-one-resolver.factory.ts index ef59ff6d515..3bf4b686c01 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/restore-one-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/restore-one-resolver.factory.ts @@ -40,13 +40,14 @@ export class RestoreOneResolverFactory const typeORMObjectRecordsParser = new ObjectRecordsToGraphqlConnectionHelper( - internalContext.objectMetadataMaps, + internalContext.flatObjectMetadataMaps, + internalContext.flatFieldMetadataMaps, + internalContext.objectIdByNameSingular, ); return typeORMObjectRecordsParser.processRecord({ objectRecord: record, - objectName: - internalContext.objectMetadataItemWithFieldMaps.nameSingular, + objectName: internalContext.flatObjectMetadata.nameSingular, take: 1, totalCount: 1, }); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory.ts index 8a0c477244e..90a08688260 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory.ts @@ -41,14 +41,15 @@ export class UpdateManyResolverFactory const typeORMObjectRecordsParser = new ObjectRecordsToGraphqlConnectionHelper( - internalContext.objectMetadataMaps, + internalContext.flatObjectMetadataMaps, + internalContext.flatFieldMetadataMaps, + internalContext.objectIdByNameSingular, ); return records.map((record: ObjectRecord) => typeORMObjectRecordsParser.processRecord({ objectRecord: record, - objectName: - internalContext.objectMetadataItemWithFieldMaps.nameSingular, + objectName: internalContext.flatObjectMetadata.nameSingular, take: 1, totalCount: 1, }), diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-one-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-one-resolver.factory.ts index fd75c558ece..c31aef9150f 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-one-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-one-resolver.factory.ts @@ -40,13 +40,14 @@ export class UpdateOneResolverFactory const typeORMObjectRecordsParser = new ObjectRecordsToGraphqlConnectionHelper( - internalContext.objectMetadataMaps, + internalContext.flatObjectMetadataMaps, + internalContext.flatFieldMetadataMaps, + internalContext.objectIdByNameSingular, ); return typeORMObjectRecordsParser.processRecord({ objectRecord: record, - objectName: - internalContext.objectMetadataItemWithFieldMaps.nameSingular, + objectName: internalContext.flatObjectMetadata.nameSingular, take: 1, totalCount: 1, }); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/workspace-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/workspace-resolver.factory.ts index 15722ca67c5..0db00351d5a 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/workspace-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/workspace-resolver.factory.ts @@ -17,11 +17,10 @@ import { AuthExceptionCode, } from 'src/engine/core-modules/auth/auth.exception'; import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; -import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { getResolverName } from 'src/engine/utils/get-resolver-name.util'; -import { standardObjectMetadataDefinitions } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects'; -import { shouldExcludeFromWorkspaceApi } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/should-exclude-from-workspace-api.util'; import { CreateManyResolverFactory } from './factories/create-many-resolver.factory'; import { CreateOneResolverFactory } from './factories/create-one-resolver.factory'; @@ -57,12 +56,13 @@ export class WorkspaceResolverFactory { private readonly mergeManyResolverFactory: MergeManyResolverFactory, private readonly groupByResolverFactory: GroupByResolverFactory, private readonly workspaceResolverBuilderService: WorkspaceResolverBuilderService, - private readonly featureFlagService: FeatureFlagService, ) {} async create( authContext: AuthContext, - objectMetadataMaps: ObjectMetadataMaps, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, + objectIdByNameSingular: Record, workspaceResolverBuilderMethods: WorkspaceResolverBuilderMethods, ): Promise { const factories = new Map< @@ -99,30 +99,17 @@ export class WorkspaceResolverFactory { ); } - const workspaceFeatureFlagsMap = - await this.featureFlagService.getWorkspaceFeatureFlagsMap(workspaceId); - - for (const objectMetadata of Object.values(objectMetadataMaps.byId).filter( - isDefined, - )) { - if ( - shouldExcludeFromWorkspaceApi( - objectMetadata, - standardObjectMetadataDefinitions, - workspaceFeatureFlagsMap, - ) - ) { - continue; - } - + for (const flatObjectMetadata of Object.values( + flatObjectMetadataMaps.byId, + ).filter(isDefined)) { // Generate query resolvers for (const methodName of workspaceResolverBuilderMethods.queries) { - const resolverName = getResolverName(objectMetadata, methodName); + const resolverName = getResolverName(flatObjectMetadata, methodName); const resolverFactory = factories.get(methodName); if (!resolverFactory) { this.logger.error(`Unknown query resolver type: ${methodName}`, { - objectMetadata, + flatObjectMetadata, methodName, resolverName, }); @@ -132,27 +119,29 @@ export class WorkspaceResolverFactory { if ( this.workspaceResolverBuilderService.shouldBuildResolver( - objectMetadata, + flatObjectMetadata, methodName, ) ) { // @ts-expect-error legacy noImplicitAny resolvers.Query[resolverName] = resolverFactory.create({ authContext, - objectMetadataMaps, - objectMetadataItemWithFieldMaps: objectMetadata, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, }); } } // Generate mutation resolvers for (const methodName of workspaceResolverBuilderMethods.mutations) { - const resolverName = getResolverName(objectMetadata, methodName); + const resolverName = getResolverName(flatObjectMetadata, methodName); const resolverFactory = factories.get(methodName); if (!resolverFactory) { this.logger.error(`Unknown mutation resolver type: ${methodName}`, { - objectMetadata, + flatObjectMetadata, methodName, resolverName, }); @@ -162,15 +151,17 @@ export class WorkspaceResolverFactory { if ( this.workspaceResolverBuilderService.shouldBuildResolver( - objectMetadata, + flatObjectMetadata, methodName, ) ) { // @ts-expect-error legacy noImplicitAny resolvers.Mutation[resolverName] = resolverFactory.create({ authContext, - objectMetadataMaps, - objectMetadataItemWithFieldMaps: objectMetadata, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, }); } } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/enum-types/enum-field-metadata-gql-enum-type.generator.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/enum-types/enum-field-metadata-gql-enum-type.generator.ts index a11a78ff1e3..70ca1bc31ed 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/enum-types/enum-field-metadata-gql-enum-type.generator.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/enum-types/enum-field-metadata-gql-enum-type.generator.ts @@ -9,9 +9,9 @@ import { type FieldMetadataComplexOption, type FieldMetadataDefaultOption, } from 'src/engine/metadata-modules/field-metadata/dtos/options.input'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { transformEnumValue } from 'src/engine/utils/transform-enum-value'; import { pascalCase } from 'src/utils/pascal-case'; @@ -23,25 +23,28 @@ export class EnumFieldMetadataGqlEnumTypeGenerator { constructor(private readonly gqlTypesStorage: GqlTypesStorage) {} - public buildAndStore(objectMetadata: ObjectMetadataEntity) { - for (const fieldMetadata of objectMetadata.fields) { + public buildAndStore( + flatObjectMetadata: FlatObjectMetadata, + fields: FlatFieldMetadata[], + ) { + for (const fieldMetadata of fields) { if (!isEnumFieldMetadataType(fieldMetadata.type)) { continue; } this.gqlTypesStorage.addGqlType( computeEnumFieldGqlTypeKey( - objectMetadata.nameSingular, + flatObjectMetadata.nameSingular, fieldMetadata.name, ), - this.generateEnum(objectMetadata.nameSingular, fieldMetadata), + this.generateEnum(flatObjectMetadata.nameSingular, fieldMetadata), ); } } private generateEnum( objectName: string, - fieldMetadata: FieldMetadataEntity, + fieldMetadata: FlatFieldMetadata, ): GraphQLEnumType { // FixMe: It's a hack until Typescript get fixed on union types for reduce function // https://github.com/microsoft/TypeScript/issues/36390 diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/gql-type.generator.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/gql-type.generator.ts index e03557963ec..97dbd9086e1 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/gql-type.generator.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/gql-type.generator.ts @@ -4,6 +4,7 @@ import { type CompositeType, compositeTypeDefinitions, } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; import { CompositeFieldMetadataGqlEnumTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/enum-types/composite-field-metadata-gql-enum-type.generator'; import { EnumFieldMetadataGqlEnumTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/enum-types/enum-field-metadata-gql-enum-type.generator'; @@ -19,9 +20,10 @@ import { ObjectMetadataGqlObjectTypeGenerator } from 'src/engine/api/graphql/wor import { ObjectMetadataWithRelationsGqlObjectTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/object-metadata-with-relations-gql-object-type.generator'; import { MutationTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/root-types/mutation-type.generator'; import { QueryTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/root-types/query-type.generator'; -import { objectContainsMorphRelationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/object-contains-morph-relation-field.util'; -import { objectContainsRelationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/object-contains-relation-field'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { type SchemaGenerationContext } from 'src/engine/api/graphql/workspace-schema-builder/types/schema-generation-context.type'; +import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util'; +import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { isMorphOrRelationFieldMetadataType } from 'src/engine/utils/is-morph-or-relation-field-metadata-type.util'; @Injectable() export class GqlTypeGenerator { @@ -44,14 +46,14 @@ export class GqlTypeGenerator { private readonly mutationTypeGenerator: MutationTypeGenerator, ) {} - async buildAndStore(objectMetadataCollection: ObjectMetadataEntity[]) { + async buildAndStore(context: SchemaGenerationContext) { const compositeTypeCollection = [...compositeTypeDefinitions.values()]; this.buildAndStoreCompositeFieldMetadataGqlTypes(compositeTypeCollection); this.buildAndStoreDateFieldMetadataGroupByGqlTypes(); - this.buildAndStoreObjectMetadataGqlTypes(objectMetadataCollection); - await this.queryTypeGenerator.buildAndStore(objectMetadataCollection); - this.mutationTypeGenerator.buildAndStore(objectMetadataCollection); + this.buildAndStoreObjectMetadataGqlTypes(context); + await this.queryTypeGenerator.buildAndStore(context); + this.mutationTypeGenerator.buildAndStore(context); } private buildAndStoreCompositeFieldMetadataGqlTypes( @@ -75,36 +77,67 @@ export class GqlTypeGenerator { } private buildAndStoreObjectMetadataGqlTypes( - dynamicObjectMetadataCollection: ObjectMetadataEntity[], + context: SchemaGenerationContext, ) { + const { flatObjectMetadataMaps, flatFieldMetadataMaps } = context; + const objectMetadataCollection = Object.values( + flatObjectMetadataMaps.byId, + ).filter(isDefined); + this.logger.log( - `Generating metadata objects: [${dynamicObjectMetadataCollection + `Generating metadata objects: [${objectMetadataCollection .map((object) => object.nameSingular) .join(', ')}]`, ); - for (const objectMetadata of dynamicObjectMetadataCollection) { - this.enumFieldMetadataGqlEnumTypeGenerator.buildAndStore(objectMetadata); - this.objectMetadataGqlObjectTypeGenerator.buildAndStore(objectMetadata); - this.edgeGqlObjectTypeGenerator.buildAndStore(objectMetadata); - this.connectionGqlObjectTypeGenerator.buildAndStore(objectMetadata); - this.groupByConnectionGqlObjectTypeGenerator.buildAndStore( - objectMetadata, - ); - this.relationConnectGqlInputTypeGenerator.buildAndStore(objectMetadata); - this.objectMetadataGqlInputTypeGenerator.buildAndStore( - objectMetadata, - dynamicObjectMetadataCollection, + for (const flatObjectMetadata of objectMetadataCollection) { + const fields = getFlatFieldsFromFlatObjectMetadata( + flatObjectMetadata, + flatFieldMetadataMaps, ); - if ( - objectContainsRelationField(objectMetadata) || - objectContainsMorphRelationField(objectMetadata) - ) + this.enumFieldMetadataGqlEnumTypeGenerator.buildAndStore( + flatObjectMetadata, + fields, + ); + this.objectMetadataGqlObjectTypeGenerator.buildAndStore( + flatObjectMetadata, + fields, + ); + this.edgeGqlObjectTypeGenerator.buildAndStore(flatObjectMetadata); + this.connectionGqlObjectTypeGenerator.buildAndStore( + flatObjectMetadata, + fields, + ); + this.groupByConnectionGqlObjectTypeGenerator.buildAndStore( + flatObjectMetadata, + ); + this.relationConnectGqlInputTypeGenerator.buildAndStore( + flatObjectMetadata, + fields, + context, + ); + this.objectMetadataGqlInputTypeGenerator.buildAndStore( + flatObjectMetadata, + fields, + context, + ); + + if (this.objectContainsRelationOrMorphField(fields)) { this.objectMetadataWithRelationsGqlObjectTypeGenerator.buildAndStore( - objectMetadata, - dynamicObjectMetadataCollection, + flatObjectMetadata, + fields, + context, ); + } } } + + private objectContainsRelationOrMorphField( + fields: FlatFieldMetadata[], + ): boolean { + return fields.some((field) => + isMorphOrRelationFieldMetadataType(field.type), + ); + } } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/create-input/object-metadata-create-gql-input-type.generator.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/create-input/object-metadata-create-gql-input-type.generator.ts index 6e6b638e29e..b257f26b9e3 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/create-input/object-metadata-create-gql-input-type.generator.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/create-input/object-metadata-create-gql-input-type.generator.ts @@ -16,15 +16,16 @@ import { TypeOptions, } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service'; import { GqlTypesStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/gql-types.storage'; +import { type SchemaGenerationContext } from 'src/engine/api/graphql/workspace-schema-builder/types/schema-generation-context.type'; import { computeFieldInputTypeOptions } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-field-input-type-options.util'; import { computeCompositeFieldInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-composite-field-input-type-key.util'; import { computeEnumFieldGqlTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-enum-field-gql-type-key.util'; import { computeObjectMetadataInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-object-metadata-input-type.util'; -import { isFieldMetadataRelationOrMorphRelation } from 'src/engine/api/graphql/workspace-schema-builder/utils/is-field-metadata-relation-or-morph-relation.utils'; -import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; import { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { pascalCase } from 'src/utils/pascal-case'; @Injectable() @@ -38,15 +39,20 @@ export class ObjectMetadataCreateGqlInputTypeGenerator { private readonly typeMapperService: TypeMapperService, ) {} - public buildAndStore(objectMetadata: ObjectMetadataEntity) { + public buildAndStore( + flatObjectMetadata: FlatObjectMetadata, + fields: FlatFieldMetadata[], + _context: SchemaGenerationContext, + ) { const inputType = new GraphQLInputObjectType({ - name: `${pascalCase(objectMetadata.nameSingular)}${GqlInputTypeDefinitionKind.Create.toString()}Input`, - description: objectMetadata.description, - fields: () => this.generateFields(objectMetadata), + name: `${pascalCase(flatObjectMetadata.nameSingular)}${GqlInputTypeDefinitionKind.Create.toString()}Input`, + description: flatObjectMetadata.description, + fields: () => + this.generateFields(flatObjectMetadata.nameSingular, fields), }) as GraphQLInputObjectType; const key = computeObjectMetadataInputTypeKey( - objectMetadata.nameSingular, + flatObjectMetadata.nameSingular, GqlInputTypeDefinitionKind.Create, ); @@ -54,11 +60,12 @@ export class ObjectMetadataCreateGqlInputTypeGenerator { } private generateFields( - objectMetadata: ObjectMetadataEntity, + objectNameSingular: string, + fields: FlatFieldMetadata[], ): GraphQLInputFieldConfigMap { const allGeneratedFields: GraphQLInputFieldConfigMap = {}; - for (const fieldMetadata of objectMetadata.fields) { + for (const fieldMetadata of fields) { const typeOptions = computeFieldInputTypeOptions( fieldMetadata, GqlInputTypeDefinitionKind.Create, @@ -66,24 +73,26 @@ export class ObjectMetadataCreateGqlInputTypeGenerator { let generatedFields; - if (isFieldMetadataRelationOrMorphRelation(fieldMetadata)) { + if (isMorphOrRelationFlatFieldMetadata(fieldMetadata)) { + const relationFieldMetadata = fieldMetadata; + generatedFields = { ...this.relationFieldMetadataGqlInputTypeGenerator.generateSimpleRelationFieldCreateOrUpdateInputType( { - fieldMetadata, + fieldMetadata: relationFieldMetadata, typeOptions, }, ), ...this.relationFieldMetadataGqlInputTypeGenerator.generateConnectRelationFieldInputType( { - fieldMetadata, + fieldMetadata: relationFieldMetadata, typeOptions, }, ), }; } else if (isEnumFieldMetadataType(fieldMetadata.type)) { generatedFields = this.generateEnumFieldCreateInputType( - objectMetadata, + objectNameSingular, fieldMetadata, typeOptions, ); @@ -106,12 +115,12 @@ export class ObjectMetadataCreateGqlInputTypeGenerator { } private generateEnumFieldCreateInputType( - objectMetadata: ObjectMetadataEntity, - fieldMetadata: FieldMetadataEntity, + objectNameSingular: string, + fieldMetadata: FlatFieldMetadata, typeOptions: TypeOptions, ) { const key = computeEnumFieldGqlTypeKey( - objectMetadata.nameSingular, + objectNameSingular, fieldMetadata.name, ); @@ -141,7 +150,7 @@ export class ObjectMetadataCreateGqlInputTypeGenerator { } private generateCompositeFieldCreateInputType( - fieldMetadata: FieldMetadataEntity, + fieldMetadata: FlatFieldMetadata, typeOptions: TypeOptions, ) { const key = computeCompositeFieldInputTypeKey( @@ -175,7 +184,7 @@ export class ObjectMetadataCreateGqlInputTypeGenerator { } private generateAtomicFieldCreateInputType( - fieldMetadata: FieldMetadataEntity, + fieldMetadata: FlatFieldMetadata, typeOptions: TypeOptions, ) { const type = this.typeMapperService.mapToScalarType( diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/filter-input/object-metadata-filter-gql-input-type.generator.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/filter-input/object-metadata-filter-gql-input-type.generator.ts index bd41575b98c..65248d0580e 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/filter-input/object-metadata-filter-gql-input-type.generator.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/filter-input/object-metadata-filter-gql-input-type.generator.ts @@ -21,11 +21,11 @@ import { computeCompositeFieldInputTypeKey } from 'src/engine/api/graphql/worksp import { computeEnumFieldGqlTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-enum-field-gql-type-key.util'; import { computeObjectMetadataInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-object-metadata-input-type.util'; import { createGqlEnumFilterType } from 'src/engine/api/graphql/workspace-schema-builder/utils/create-gql-enum-filter-type.util'; -import { isFieldMetadataRelationOrMorphRelation } from 'src/engine/api/graphql/workspace-schema-builder/utils/is-field-metadata-relation-or-morph-relation.utils'; -import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; import { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { pascalCase } from 'src/utils/pascal-case'; @Injectable() @@ -39,15 +39,19 @@ export class ObjectMetadataFilterGqlInputTypeGenerator { private readonly typeMapperService: TypeMapperService, ) {} - public buildAndStore(objectMetadata: ObjectMetadataEntity) { + public buildAndStore( + flatObjectMetadata: FlatObjectMetadata, + fields: FlatFieldMetadata[], + ) { const inputType = new GraphQLInputObjectType({ - name: `${pascalCase(objectMetadata.nameSingular)}${GqlInputTypeDefinitionKind.Filter.toString()}Input`, - description: objectMetadata.description, - fields: () => this.generateFields(objectMetadata, inputType), + name: `${pascalCase(flatObjectMetadata.nameSingular)}${GqlInputTypeDefinitionKind.Filter.toString()}Input`, + description: flatObjectMetadata.description, + fields: () => + this.generateFields(flatObjectMetadata.nameSingular, fields, inputType), }) as GraphQLInputObjectType; const key = computeObjectMetadataInputTypeKey( - objectMetadata.nameSingular, + flatObjectMetadata.nameSingular, GqlInputTypeDefinitionKind.Filter, ); @@ -55,7 +59,8 @@ export class ObjectMetadataFilterGqlInputTypeGenerator { } private generateFields( - objectMetadata: ObjectMetadataEntity, + objectNameSingular: string, + fields: FlatFieldMetadata[], inputType: GraphQLInputObjectType, ): GraphQLInputFieldConfigMap { const andOrType = this.typeMapperService.applyTypeOptions(inputType, { @@ -65,17 +70,20 @@ export class ObjectMetadataFilterGqlInputTypeGenerator { }); const allGeneratedFields: GraphQLInputFieldConfigMap = {}; - for (const fieldMetadata of objectMetadata.fields) { - fieldMetadata.isNullable = true; + for (const fieldMetadata of fields) { + const modifiedFieldMetadata = { + ...fieldMetadata, + isNullable: true, + }; const typeOptions = computeFieldInputTypeOptions( - fieldMetadata, + modifiedFieldMetadata, GqlInputTypeDefinitionKind.Filter, ); let generatedFields; - if (isFieldMetadataRelationOrMorphRelation(fieldMetadata)) { + if (isMorphOrRelationFlatFieldMetadata(fieldMetadata)) { generatedFields = this.relationFieldMetadataGqlInputTypeGenerator.generateSimpleRelationFieldFilterInputType( { @@ -85,7 +93,7 @@ export class ObjectMetadataFilterGqlInputTypeGenerator { ); } else if (isEnumFieldMetadataType(fieldMetadata.type)) { generatedFields = this.generateEnumFieldFilterInputType( - objectMetadata, + objectNameSingular, fieldMetadata, typeOptions, ); @@ -120,12 +128,12 @@ export class ObjectMetadataFilterGqlInputTypeGenerator { } private generateEnumFieldFilterInputType( - objectMetadata: ObjectMetadataEntity, - fieldMetadata: FieldMetadataEntity, + objectNameSingular: string, + fieldMetadata: FlatFieldMetadata, typeOptions: TypeOptions, ) { const key = computeEnumFieldGqlTypeKey( - objectMetadata.nameSingular, + objectNameSingular, fieldMetadata.name, ); @@ -152,7 +160,7 @@ export class ObjectMetadataFilterGqlInputTypeGenerator { } private generateCompositeFieldFilterInputType( - fieldMetadata: FieldMetadataEntity, + fieldMetadata: FlatFieldMetadata, typeOptions: TypeOptions, ) { const key = computeCompositeFieldInputTypeKey( @@ -181,7 +189,7 @@ export class ObjectMetadataFilterGqlInputTypeGenerator { } private generateAtomicFieldFilterInputType( - fieldMetadata: FieldMetadataEntity, + fieldMetadata: FlatFieldMetadata, typeOptions: TypeOptions, ) { const type = this.typeMapperService.mapToFilterType( diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/group-by-input/object-metadata-group-by-gql-input-type.generator.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/group-by-input/object-metadata-group-by-gql-input-type.generator.ts index 1529ce0c760..c4171ed49a6 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/group-by-input/object-metadata-group-by-gql-input-type.generator.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/group-by-input/object-metadata-group-by-gql-input-type.generator.ts @@ -16,14 +16,13 @@ import { GROUP_BY_DATE_GRANULARITY_INPUT_KEY } from 'src/engine/api/graphql/work import { RelationFieldMetadataGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/relation-field-metadata-gql-type.generator'; import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service'; import { GqlTypesStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/gql-types.storage'; +import { type SchemaGenerationContext } from 'src/engine/api/graphql/workspace-schema-builder/types/schema-generation-context.type'; import { computeCompositeFieldInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-composite-field-input-type-key.util'; import { computeObjectMetadataInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-object-metadata-input-type.util'; -import { isFieldMetadataRelationOrMorphRelation } from 'src/engine/api/graphql/workspace-schema-builder/utils/is-field-metadata-relation-or-morph-relation.utils'; -import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; -import { generateObjectMetadataMaps } from 'src/engine/metadata-modules/utils/generate-object-metadata-maps.util'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { pascalCase } from 'src/utils/pascal-case'; @Injectable() @@ -31,8 +30,6 @@ export class ObjectMetadataGroupByGqlInputTypeGenerator { private readonly logger = new Logger( ObjectMetadataGroupByGqlInputTypeGenerator.name, ); - private objectMetadataCollection: ObjectMetadataEntity[] = []; - private objectMetadataMaps: ObjectMetadataMaps | undefined; constructor( private readonly gqlTypesStorage: GqlTypesStorage, @@ -41,24 +38,18 @@ export class ObjectMetadataGroupByGqlInputTypeGenerator { ) {} public buildAndStore( - objectMetadata: ObjectMetadataEntity, - objectMetadataCollection?: ObjectMetadataEntity[], + flatObjectMetadata: FlatObjectMetadata, + fields: FlatFieldMetadata[], + context: SchemaGenerationContext, ) { - if (isDefined(objectMetadataCollection)) { - this.objectMetadataCollection = objectMetadataCollection; - this.objectMetadataMaps = generateObjectMetadataMaps( - objectMetadataCollection, - ); - } - const inputType = new GraphQLInputObjectType({ - name: `${pascalCase(objectMetadata.nameSingular)}${GqlInputTypeDefinitionKind.GroupBy.toString()}Input`, - description: objectMetadata.description, - fields: () => this.generateFields(objectMetadata), + name: `${pascalCase(flatObjectMetadata.nameSingular)}${GqlInputTypeDefinitionKind.GroupBy.toString()}Input`, + description: flatObjectMetadata.description, + fields: () => this.generateFields(fields, context), }) as GraphQLInputObjectType; const key = computeObjectMetadataInputTypeKey( - objectMetadata.nameSingular, + flatObjectMetadata.nameSingular, GqlInputTypeDefinitionKind.GroupBy, ); @@ -66,17 +57,16 @@ export class ObjectMetadataGroupByGqlInputTypeGenerator { } private generateFields( - objectMetadata: ObjectMetadataEntity, + fields: FlatFieldMetadata[], + context: SchemaGenerationContext, ): GraphQLInputFieldConfigMap { const allGeneratedFields: GraphQLInputFieldConfigMap = {}; - for (const fieldMetadata of objectMetadata.fields) { - const generatedField = isFieldMetadataRelationOrMorphRelation( - fieldMetadata, - ) + for (const fieldMetadata of fields) { + const generatedField = isMorphOrRelationFlatFieldMetadata(fieldMetadata) ? this.relationFieldMetadataGqlInputTypeGenerator.generateSimpleRelationFieldGroupByInputType( fieldMetadata, - this.objectMetadataMaps, + context, ) : this.generateField(fieldMetadata); @@ -86,7 +76,7 @@ export class ObjectMetadataGroupByGqlInputTypeGenerator { return allGeneratedFields; } - private generateField(fieldMetadata: FieldMetadataEntity) { + private generateField(fieldMetadata: FlatFieldMetadata) { if (isCompositeFieldMetadataType(fieldMetadata.type)) return this.generateCompositeFieldGroupByInputType(fieldMetadata); @@ -124,7 +114,7 @@ export class ObjectMetadataGroupByGqlInputTypeGenerator { } private generateCompositeFieldGroupByInputType( - fieldMetadata: FieldMetadataEntity, + fieldMetadata: FlatFieldMetadata, ) { const key = computeCompositeFieldInputTypeKey( fieldMetadata.type, diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/object-metadata-gql-input-type.generator.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/object-metadata-gql-input-type.generator.ts index 4b2c5f6df4b..765d1a81507 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/object-metadata-gql-input-type.generator.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/object-metadata-gql-input-type.generator.ts @@ -6,7 +6,9 @@ import { ObjectMetadataGroupByGqlInputTypeGenerator } from 'src/engine/api/graph import { ObjectMetadataOrderByGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/order-by-input/object-metadata-order-by-gql-input-type.generator'; import { ObjectMetadataOrderByWithGroupByGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/order-by-input/object-metadata-order-by-with-group-by-gql-input-type.generator'; import { ObjectMetadataUpdateGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/update-input/object-metadata-update-gql-input-type.generator'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { type SchemaGenerationContext } from 'src/engine/api/graphql/workspace-schema-builder/types/schema-generation-context.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; @Injectable() export class ObjectMetadataGqlInputTypeGenerator { @@ -20,28 +22,37 @@ export class ObjectMetadataGqlInputTypeGenerator { ) {} public buildAndStore( - objectMetadata: ObjectMetadataEntity, - objectMetadataCollection?: ObjectMetadataEntity[], + flatObjectMetadata: FlatObjectMetadata, + fields: FlatFieldMetadata[], + context: SchemaGenerationContext, ) { this.objectMetadataCreateGqlInputTypeGenerator.buildAndStore( - objectMetadata, + flatObjectMetadata, + fields, + context, ); this.objectMetadataUpdateGqlInputTypeGenerator.buildAndStore( - objectMetadata, + flatObjectMetadata, + fields, + context, ); this.objectMetadataFilterGqlInputTypeGenerator.buildAndStore( - objectMetadata, + flatObjectMetadata, + fields, ); this.objectMetadataOrderByGqlInputTypeGenerator.buildAndStore({ - objectMetadata, + flatObjectMetadata, + fields, }); this.objectMetadataOrderByWithGroupByGqlInputTypeGenerator.buildAndStore({ - objectMetadata, - objectMetadataCollection, + flatObjectMetadata, + fields, + context, }); this.objectMetadataGroupByGqlInputTypeGenerator.buildAndStore( - objectMetadata, - objectMetadataCollection, + flatObjectMetadata, + fields, + context, ); } } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/order-by-input/object-metadata-order-by-base.generator.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/order-by-input/object-metadata-order-by-base.generator.ts index 23d8bd6433e..51267b540fd 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/order-by-input/object-metadata-order-by-base.generator.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/order-by-input/object-metadata-order-by-base.generator.ts @@ -12,13 +12,12 @@ import { TypeOptions, } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service'; import { GqlTypesStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/gql-types.storage'; +import { type SchemaGenerationContext } from 'src/engine/api/graphql/workspace-schema-builder/types/schema-generation-context.type'; import { computeFieldInputTypeOptions } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-field-input-type-options.util'; import { computeCompositeFieldInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-composite-field-input-type-key.util'; -import { isFieldMetadataRelationOrMorphRelation } from 'src/engine/api/graphql/workspace-schema-builder/utils/is-field-metadata-relation-or-morph-relation.utils'; -import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util'; @Injectable() export class ObjectMetadataOrderByBaseGenerator { @@ -29,35 +28,38 @@ export class ObjectMetadataOrderByBaseGenerator { ) {} public generateFields({ - objectMetadata, + fields, logger, orderByDateGranularity, - objectMetadataMaps, + context, }: { - objectMetadata: ObjectMetadataEntity; + fields: FlatFieldMetadata[]; logger: Logger; orderByDateGranularity?: boolean; - objectMetadataMaps?: ObjectMetadataMaps; + context?: SchemaGenerationContext; }): GraphQLInputFieldConfigMap { const allGeneratedFields: GraphQLInputFieldConfigMap = {}; - for (const fieldMetadata of objectMetadata.fields) { - fieldMetadata.isNullable = true; + for (const fieldMetadata of fields) { + const modifiedFieldMetadata = { + ...fieldMetadata, + isNullable: true, + }; const typeOptions = computeFieldInputTypeOptions( - fieldMetadata, + modifiedFieldMetadata, GqlInputTypeDefinitionKind.OrderBy, ); let generatedFields; - if (isFieldMetadataRelationOrMorphRelation(fieldMetadata)) { + if (isMorphOrRelationFlatFieldMetadata(fieldMetadata)) { generatedFields = this.relationFieldMetadataGqlInputTypeGenerator.generateSimpleRelationFieldOrderByInputType( { fieldMetadata, typeOptions, - objectMetadataMaps, + context, }, ); } else if (isCompositeFieldMetadataType(fieldMetadata.type)) { @@ -82,7 +84,7 @@ export class ObjectMetadataOrderByBaseGenerator { } private generateCompositeFieldOrderByInputType( - fieldMetadata: FieldMetadataEntity, + fieldMetadata: FlatFieldMetadata, typeOptions: TypeOptions, logger: Logger, ) { @@ -117,7 +119,7 @@ export class ObjectMetadataOrderByBaseGenerator { logger, orderByDateGranularity, }: { - fieldMetadata: FieldMetadataEntity; + fieldMetadata: FlatFieldMetadata; typeOptions: TypeOptions; logger: Logger; orderByDateGranularity?: boolean; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/order-by-input/object-metadata-order-by-gql-input-type.generator.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/order-by-input/object-metadata-order-by-gql-input-type.generator.ts index dcadf0d3633..17aaba5d6e7 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/order-by-input/object-metadata-order-by-gql-input-type.generator.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/order-by-input/object-metadata-order-by-gql-input-type.generator.ts @@ -6,7 +6,8 @@ import { GqlInputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-sch import { ObjectMetadataOrderByBaseGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/order-by-input/object-metadata-order-by-base.generator'; import { GqlTypesStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/gql-types.storage'; import { computeObjectMetadataInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-object-metadata-input-type.util'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { pascalCase } from 'src/utils/pascal-case'; @Injectable() @@ -20,22 +21,24 @@ export class ObjectMetadataOrderByGqlInputTypeGenerator { ) {} public buildAndStore({ - objectMetadata, + flatObjectMetadata, + fields, }: { - objectMetadata: ObjectMetadataEntity; + flatObjectMetadata: FlatObjectMetadata; + fields: FlatFieldMetadata[]; }) { const inputType = new GraphQLInputObjectType({ - name: `${pascalCase(objectMetadata.nameSingular)}${GqlInputTypeDefinitionKind.OrderBy.toString()}Input`, - description: objectMetadata.description, + name: `${pascalCase(flatObjectMetadata.nameSingular)}${GqlInputTypeDefinitionKind.OrderBy.toString()}Input`, + description: flatObjectMetadata.description, fields: () => this.objectMetadataOrderByBaseGenerator.generateFields({ - objectMetadata, + fields, logger: this.logger, }), }) as GraphQLInputObjectType; const key = computeObjectMetadataInputTypeKey( - objectMetadata.nameSingular, + flatObjectMetadata.nameSingular, GqlInputTypeDefinitionKind.OrderBy, ); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/order-by-input/object-metadata-order-by-with-group-by-gql-input-type.generator.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/order-by-input/object-metadata-order-by-with-group-by-gql-input-type.generator.ts index c477428f1c1..3169796f938 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/order-by-input/object-metadata-order-by-with-group-by-gql-input-type.generator.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/order-by-input/object-metadata-order-by-with-group-by-gql-input-type.generator.ts @@ -15,16 +15,15 @@ import { TypeOptions, } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service'; import { GqlTypesStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/gql-types.storage'; +import { type SchemaGenerationContext } from 'src/engine/api/graphql/workspace-schema-builder/types/schema-generation-context.type'; import { computeFieldInputTypeOptions } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-field-input-type-options.util'; import { computeCompositeFieldInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-composite-field-input-type-key.util'; import { computeObjectMetadataInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-object-metadata-input-type.util'; import { getAvailableAggregationsFromObjectFields } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util'; -import { isFieldMetadataRelationOrMorphRelation } from 'src/engine/api/graphql/workspace-schema-builder/utils/is-field-metadata-relation-or-morph-relation.utils'; -import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; -import { generateObjectMetadataMaps } from 'src/engine/metadata-modules/utils/generate-object-metadata-maps.util'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { pascalCase } from 'src/utils/pascal-case'; @Injectable() @@ -39,29 +38,24 @@ export class ObjectMetadataOrderByWithGroupByGqlInputTypeGenerator { private readonly typeMapperService: TypeMapperService, ) {} - private objectMetadataMaps?: ObjectMetadataMaps; - public buildAndStore({ - objectMetadata, - objectMetadataCollection, + flatObjectMetadata, + fields, + context, }: { - objectMetadata: ObjectMetadataEntity; - objectMetadataCollection?: ObjectMetadataEntity[]; + flatObjectMetadata: FlatObjectMetadata; + fields: FlatFieldMetadata[]; + context: SchemaGenerationContext; }) { - if (isDefined(objectMetadataCollection)) { - this.objectMetadataMaps = generateObjectMetadataMaps( - objectMetadataCollection, - ); - } - const inputType = new GraphQLInputObjectType({ - name: `${pascalCase(objectMetadata.nameSingular)}${GqlInputTypeDefinitionKind.OrderByWithGroupBy.toString()}Input`, - description: objectMetadata.description, - fields: () => this.generateFields(objectMetadata), + name: `${pascalCase(flatObjectMetadata.nameSingular)}${GqlInputTypeDefinitionKind.OrderByWithGroupBy.toString()}Input`, + description: flatObjectMetadata.description, + fields: () => + this.generateFields(flatObjectMetadata.nameSingular, fields, context), }) as GraphQLInputObjectType; const key = computeObjectMetadataInputTypeKey( - objectMetadata.nameSingular, + flatObjectMetadata.nameSingular, GqlInputTypeDefinitionKind.OrderByWithGroupBy, ); @@ -69,36 +63,47 @@ export class ObjectMetadataOrderByWithGroupByGqlInputTypeGenerator { } private generateFields( - objectMetadata: ObjectMetadataEntity, + objectNameSingular: string, + fields: FlatFieldMetadata[], + context: SchemaGenerationContext, ): GraphQLInputFieldConfigMap { return { - ...this.generateOrderByOnAggregateFields(objectMetadata), - ...this.generateOrderByOnDimensionValuesFields(objectMetadata), + ...this.generateOrderByOnAggregateFields( + objectNameSingular, + fields, + context, + ), + ...this.generateOrderByOnDimensionValuesFields(fields, context), }; } private generateOrderByOnAggregateFields( - objectMetadata: ObjectMetadataEntity, + objectNameSingular: string, + fields: FlatFieldMetadata[], + context: SchemaGenerationContext, ): GraphQLInputFieldConfigMap { const allAggregatedFields: GraphQLInputFieldConfigMap = {}; - for (const fieldMetadata of objectMetadata.fields) { - fieldMetadata.isNullable = true; + for (const fieldMetadata of fields) { + const modifiedFieldMetadata = { + ...fieldMetadata, + isNullable: true, + }; const typeOptions = computeFieldInputTypeOptions( - fieldMetadata, + modifiedFieldMetadata, GqlInputTypeDefinitionKind.OrderBy, ); let generatedFields; - if (isFieldMetadataRelationOrMorphRelation(fieldMetadata)) { + if (isMorphOrRelationFlatFieldMetadata(fieldMetadata)) { generatedFields = this.relationFieldMetadataGqlInputTypeGenerator.generateSimpleRelationFieldOrderByInputType( { fieldMetadata, typeOptions, - objectMetadataMaps: this.objectMetadataMaps, + context, }, ); } else if (isCompositeFieldMetadataType(fieldMetadata.type)) { @@ -117,7 +122,7 @@ export class ObjectMetadataOrderByWithGroupByGqlInputTypeGenerator { } const aggregateInputType = new GraphQLInputObjectType({ - name: `${pascalCase(objectMetadata.nameSingular)}${GqlInputTypeDefinitionKind.OrderByWithGroupBy.toString()}AggregateInput`, + name: `${pascalCase(objectNameSingular)}${GqlInputTypeDefinitionKind.OrderByWithGroupBy.toString()}AggregateInput`, description: 'Aggregate-based ordering', fields: () => allAggregatedFields, }) as GraphQLInputObjectType; @@ -131,18 +136,19 @@ export class ObjectMetadataOrderByWithGroupByGqlInputTypeGenerator { } private generateOrderByOnDimensionValuesFields( - objectMetadata: ObjectMetadataEntity, + fields: FlatFieldMetadata[], + context: SchemaGenerationContext, ) { return this.objectMetadataOrderByBaseGenerator.generateFields({ - objectMetadata, + fields, logger: this.logger, orderByDateGranularity: true, - objectMetadataMaps: this.objectMetadataMaps, + context, }); } private generateCompositeFieldOrderByInputType( - fieldMetadata: FieldMetadataEntity, + fieldMetadata: FlatFieldMetadata, typeOptions: TypeOptions, ) { const key = computeCompositeFieldInputTypeKey( @@ -169,7 +175,7 @@ export class ObjectMetadataOrderByWithGroupByGqlInputTypeGenerator { } private generateAtomicFieldOrderByInputType( - fieldMetadata: FieldMetadataEntity, + fieldMetadata: FlatFieldMetadata, typeOptions: TypeOptions, ) { const orderByType = this.typeMapperService.mapToOrderByType( @@ -193,7 +199,7 @@ export class ObjectMetadataOrderByWithGroupByGqlInputTypeGenerator { } private generateAggregateFieldOrderByInputType( - fieldMetadata: FieldMetadataEntity, + fieldMetadata: FlatFieldMetadata, ) { const aggregations = getAvailableAggregationsFromObjectFields([ fieldMetadata, diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/relation-connect-gql-input-type.generator.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/relation-connect-gql-input-type.generator.ts index 5e568d983d9..b3eb6a407b4 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/relation-connect-gql-input-type.generator.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/relation-connect-gql-input-type.generator.ts @@ -8,15 +8,16 @@ import { GraphQLString, } from 'graphql'; import { RELATION_NESTED_QUERY_KEYWORDS } from 'twenty-shared/constants'; -import { getUniqueConstraintsFields } from 'twenty-shared/utils'; import { compositeTypeDefinitions } from 'twenty-shared/types'; +import { getUniqueConstraintsFields, isDefined } from 'twenty-shared/utils'; import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service'; import { GqlTypesStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/gql-types.storage'; +import { type SchemaGenerationContext } from 'src/engine/api/graphql/workspace-schema-builder/types/schema-generation-context.type'; import { computeRelationConnectInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-relation-connect-input-type-key.util'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { pascalCase } from 'src/utils/pascal-case'; @Injectable() @@ -26,44 +27,76 @@ export class RelationConnectGqlInputTypeGenerator { private readonly gqlTypesStorage: GqlTypesStorage, ) {} - public buildAndStore(objectMetadata: ObjectMetadataEntity) { - const relationConnectGqlInputType = - this.generateRelationConnectInputType(objectMetadata); - const key = computeRelationConnectInputTypeKey(objectMetadata.id); + public buildAndStore( + flatObjectMetadata: FlatObjectMetadata, + fields: FlatFieldMetadata[], + context: SchemaGenerationContext, + ) { + const relationConnectGqlInputType = this.generateRelationConnectInputType( + flatObjectMetadata, + fields, + context, + ); + const key = computeRelationConnectInputTypeKey(flatObjectMetadata.id); this.gqlTypesStorage.addGqlType(key, relationConnectGqlInputType); } private generateRelationConnectInputType( - objectMetadata: ObjectMetadataEntity, + flatObjectMetadata: FlatObjectMetadata, + fields: FlatFieldMetadata[], + context: SchemaGenerationContext, ): GraphQLInputObjectType { return new GraphQLInputObjectType({ - name: `${pascalCase(objectMetadata.nameSingular)}RelationInput`, + name: `${pascalCase(flatObjectMetadata.nameSingular)}RelationInput`, fields: () => ({ [RELATION_NESTED_QUERY_KEYWORDS.CONNECT]: { type: new GraphQLInputObjectType({ - name: `${pascalCase(objectMetadata.nameSingular)}ConnectInput`, - fields: this.generateRelationWhereInputType(objectMetadata), + name: `${pascalCase(flatObjectMetadata.nameSingular)}ConnectInput`, + fields: this.generateRelationWhereInputType( + flatObjectMetadata, + fields, + context, + ), }), - description: `Connect to a ${objectMetadata.nameSingular} record`, + description: `Connect to a ${flatObjectMetadata.nameSingular} record`, }, [RELATION_NESTED_QUERY_KEYWORDS.DISCONNECT]: { type: GraphQLBoolean, - description: `Disconnect from a ${objectMetadata.nameSingular} record`, + description: `Disconnect from a ${flatObjectMetadata.nameSingular} record`, }, }), }); } private generateRelationWhereInputType( - objectMetadata: ObjectMetadataEntity, + flatObjectMetadata: FlatObjectMetadata, + fields: FlatFieldMetadata[], + context: SchemaGenerationContext, ): Record { - const uniqueConstraints = getUniqueConstraintsFields< - FieldMetadataEntity, - ObjectMetadataEntity - >(objectMetadata); + const { flatIndexMaps } = context; - const fields: Record< + const indexMetadatas = flatObjectMetadata.indexMetadataIds + .map((indexId) => flatIndexMaps.byId[indexId]) + .filter(isDefined) + .map((flatIndex) => ({ + id: flatIndex.id, + isUnique: flatIndex.isUnique, + indexFieldMetadatas: flatIndex.flatIndexFieldMetadatas, + })); + + const objectWithIndexes = { + id: flatObjectMetadata.id, + indexMetadatas, + fields, + }; + + const uniqueConstraints = getUniqueConstraintsFields< + FlatFieldMetadata, + typeof objectWithIndexes + >(objectWithIndexes); + + const inputFields: Record< string, { type: GraphQLInputType; description: string } > = {}; @@ -101,11 +134,11 @@ export class RelationConnectGqlInputTypeGenerator { }); const compositeInputType = new GraphQLInputObjectType({ - name: `${pascalCase(objectMetadata.nameSingular)}${pascalCase(field.name)}WhereInput`, + name: `${pascalCase(flatObjectMetadata.nameSingular)}${pascalCase(field.name)}WhereInput`, fields: () => compositeFields, }); - fields[field.name] = { + inputFields[field.name] = { type: compositeInputType, description: `Connect by ${field.label || field.name}`, }; @@ -116,7 +149,7 @@ export class RelationConnectGqlInputTypeGenerator { { settings: field.settings, isIdField: field.name === 'id' }, ); - fields[field.name] = { + inputFields[field.name] = { type: scalarType || GraphQLString, description: `Connect by ${field.label || field.name}`, }; @@ -127,15 +160,15 @@ export class RelationConnectGqlInputTypeGenerator { return { [RELATION_NESTED_QUERY_KEYWORDS.CONNECT_WHERE]: { type: new GraphQLInputObjectType({ - name: `${pascalCase(objectMetadata.nameSingular)}WhereUniqueInput`, - fields: () => fields, + name: `${pascalCase(flatObjectMetadata.nameSingular)}WhereUniqueInput`, + fields: () => inputFields, }), - description: `Find a ${objectMetadata.nameSingular} record based on its unique fields: ${this.formatConstraints(uniqueConstraints)}`, + description: `Find a ${flatObjectMetadata.nameSingular} record based on its unique fields: ${this.formatConstraints(uniqueConstraints)}`, }, }; } - private formatConstraints(constraints: FieldMetadataEntity[][]) { + private formatConstraints(constraints: FlatFieldMetadata[][]) { return constraints .map((constraint) => constraint.map((field) => field.name).join(' and ')) .join(' or '); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/relation-field-metadata-gql-type.generator.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/relation-field-metadata-gql-type.generator.ts index 57b17b0ac3d..381d218079b 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/relation-field-metadata-gql-type.generator.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/relation-field-metadata-gql-type.generator.ts @@ -14,11 +14,11 @@ import { TypeOptions, } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service'; import { GqlTypesStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/gql-types.storage'; +import { type SchemaGenerationContext } from 'src/engine/api/graphql/workspace-schema-builder/types/schema-generation-context.type'; import { computeObjectMetadataInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-object-metadata-input-type.util'; import { computeRelationConnectInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-relation-connect-input-type-key.util'; import { extractGraphQLRelationFieldNames } from 'src/engine/api/graphql/workspace-schema-builder/utils/extract-graphql-relation-field-names.util'; -import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; @Injectable() export class RelationFieldMetadataGqlInputTypeGenerator { @@ -35,7 +35,7 @@ export class RelationFieldMetadataGqlInputTypeGenerator { fieldMetadata, typeOptions, }: { - fieldMetadata: FieldMetadataEntity< + fieldMetadata: FlatFieldMetadata< FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION >; typeOptions: TypeOptions; @@ -77,7 +77,7 @@ export class RelationFieldMetadataGqlInputTypeGenerator { fieldMetadata, typeOptions, }: { - fieldMetadata: FieldMetadataEntity< + fieldMetadata: FlatFieldMetadata< FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION >; typeOptions: TypeOptions; @@ -118,13 +118,13 @@ export class RelationFieldMetadataGqlInputTypeGenerator { public generateSimpleRelationFieldOrderByInputType({ fieldMetadata, typeOptions, - objectMetadataMaps, + context, }: { - fieldMetadata: FieldMetadataEntity< + fieldMetadata: FlatFieldMetadata< FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION >; typeOptions: TypeOptions; - objectMetadataMaps?: ObjectMetadataMaps; + context?: SchemaGenerationContext; }) { if (fieldMetadata.settings?.relationType === RelationType.ONE_TO_MANY) return {}; @@ -158,10 +158,12 @@ export class RelationFieldMetadataGqlInputTypeGenerator { if ( isDefined(fieldMetadata.relationTargetObjectMetadataId) && - isDefined(objectMetadataMaps) + isDefined(context) ) { const targetObjectMetadata = - objectMetadataMaps.byId[fieldMetadata.relationTargetObjectMetadataId]; + context.flatObjectMetadataMaps.byId[ + fieldMetadata.relationTargetObjectMetadataId + ]; if (isDefined(targetObjectMetadata)) { const targetOrderByInputTypeKey = computeObjectMetadataInputTypeKey( @@ -189,10 +191,10 @@ export class RelationFieldMetadataGqlInputTypeGenerator { } public generateSimpleRelationFieldGroupByInputType( - fieldMetadata: FieldMetadataEntity< + fieldMetadata: FlatFieldMetadata< FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION >, - objectMetadataMaps?: ObjectMetadataMaps, + context?: SchemaGenerationContext, ): GraphQLInputFieldConfigMap { if (fieldMetadata.settings?.relationType === RelationType.ONE_TO_MANY) return {}; @@ -204,10 +206,12 @@ export class RelationFieldMetadataGqlInputTypeGenerator { if ( isDefined(fieldMetadata.relationTargetObjectMetadataId) && - isDefined(objectMetadataMaps) + isDefined(context) ) { const targetObjectMetadata = - objectMetadataMaps.byId[fieldMetadata.relationTargetObjectMetadataId]; + context.flatObjectMetadataMaps.byId[ + fieldMetadata.relationTargetObjectMetadataId + ]; if (isDefined(targetObjectMetadata)) { const targetGroupByInputTypeKey = computeObjectMetadataInputTypeKey( @@ -238,7 +242,7 @@ export class RelationFieldMetadataGqlInputTypeGenerator { fieldMetadata, typeOptions, }: { - fieldMetadata: FieldMetadataEntity< + fieldMetadata: FlatFieldMetadata< FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION >; typeOptions: TypeOptions; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/update-input/object-metadata-update-gql-input-type.generator.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/update-input/object-metadata-update-gql-input-type.generator.ts index 35bb08e441b..3fe76af2702 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/update-input/object-metadata-update-gql-input-type.generator.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/update-input/object-metadata-update-gql-input-type.generator.ts @@ -15,14 +15,16 @@ import { GqlInputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-sch import { RelationFieldMetadataGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/relation-field-metadata-gql-type.generator'; import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service'; import { GqlTypesStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/gql-types.storage'; +import { type SchemaGenerationContext } from 'src/engine/api/graphql/workspace-schema-builder/types/schema-generation-context.type'; import { computeFieldInputTypeOptions } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-field-input-type-options.util'; import { computeCompositeFieldInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-composite-field-input-type-key.util'; import { computeEnumFieldGqlTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-enum-field-gql-type-key.util'; import { computeObjectMetadataInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-object-metadata-input-type.util'; -import { isFieldMetadataRelationOrMorphRelation } from 'src/engine/api/graphql/workspace-schema-builder/utils/is-field-metadata-relation-or-morph-relation.utils'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; import { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { pascalCase } from 'src/utils/pascal-case'; @Injectable() @@ -36,15 +38,20 @@ export class ObjectMetadataUpdateGqlInputTypeGenerator { private readonly typeMapperService: TypeMapperService, ) {} - public buildAndStore(objectMetadata: ObjectMetadataEntity) { + public buildAndStore( + flatObjectMetadata: FlatObjectMetadata, + fields: FlatFieldMetadata[], + _context: SchemaGenerationContext, + ) { const inputType = new GraphQLInputObjectType({ - name: `${pascalCase(objectMetadata.nameSingular)}${GqlInputTypeDefinitionKind.Update.toString()}Input`, - description: objectMetadata.description, - fields: () => this.generateFields(objectMetadata), + name: `${pascalCase(flatObjectMetadata.nameSingular)}${GqlInputTypeDefinitionKind.Update.toString()}Input`, + description: flatObjectMetadata.description, + fields: () => + this.generateFields(flatObjectMetadata.nameSingular, fields), }) as GraphQLInputObjectType; const key = computeObjectMetadataInputTypeKey( - objectMetadata.nameSingular, + flatObjectMetadata.nameSingular, GqlInputTypeDefinitionKind.Update, ); @@ -52,21 +59,25 @@ export class ObjectMetadataUpdateGqlInputTypeGenerator { } private generateFields( - objectMetadata: ObjectMetadataEntity, + objectNameSingular: string, + fields: FlatFieldMetadata[], ): GraphQLInputFieldConfigMap { const allGeneratedFields: GraphQLInputFieldConfigMap = {}; - for (const fieldMetadata of objectMetadata.fields) { - fieldMetadata.isNullable = true; + for (const fieldMetadata of fields) { + const modifiedFieldMetadata = { + ...fieldMetadata, + isNullable: true, + }; const typeOptions = computeFieldInputTypeOptions( - fieldMetadata, + modifiedFieldMetadata, GqlInputTypeDefinitionKind.Update, ); let generatedFields; - if (isFieldMetadataRelationOrMorphRelation(fieldMetadata)) { + if (isMorphOrRelationFlatFieldMetadata(fieldMetadata)) { generatedFields = { ...this.relationFieldMetadataGqlInputTypeGenerator.generateSimpleRelationFieldCreateOrUpdateInputType( { @@ -90,7 +101,7 @@ export class ObjectMetadataUpdateGqlInputTypeGenerator { if (isEnumFieldMetadataType(fieldMetadata.type)) { const key = computeEnumFieldGqlTypeKey( - objectMetadata.nameSingular, + objectNameSingular, fieldMetadata.name, ); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/aggregation-type.generator.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/aggregation-type.generator.ts index ad9617e42e6..8ef1600f758 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/aggregation-type.generator.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/aggregation-type.generator.ts @@ -4,18 +4,17 @@ import { type AggregationField, getAvailableAggregationsFromObjectFields, } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; type AggregationGraphQLType = Pick; @Injectable() export class AggregationObjectTypeGenerator { public generate( - objectMetadata: ObjectMetadataEntity, + flatFields: FlatFieldMetadata[], ): Record { - const availableAggregations = getAvailableAggregationsFromObjectFields( - objectMetadata.fields, - ); + const availableAggregations = + getAvailableAggregationsFromObjectFields(flatFields); return Object.entries(availableAggregations).reduce< Record diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/connection-gql-object-type.generator.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/connection-gql-object-type.generator.ts index 690772e8927..bf6cd3fdc16 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/connection-gql-object-type.generator.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/connection-gql-object-type.generator.ts @@ -10,7 +10,8 @@ import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-build import { GqlTypesStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/gql-types.storage'; import { GraphQLOutputTypeFieldConfigMap } from 'src/engine/api/graphql/workspace-schema-builder/types/graphql-field-config-map.types'; import { computeObjectMetadataObjectTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-object-metadata-object-type-key.util'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { pascalCase } from 'src/utils/pascal-case'; @Injectable() @@ -23,51 +24,52 @@ export class ConnectionGqlObjectTypeGenerator { private readonly gqlTypesStorage: GqlTypesStorage, ) {} - public buildAndStore(objectMetadata: ObjectMetadataEntity) { + public buildAndStore( + flatObjectMetadata: FlatObjectMetadata, + flatFields: FlatFieldMetadata[], + ) { const kind = ObjectTypeDefinitionKind.Connection; const key = computeObjectMetadataObjectTypeKey( - objectMetadata.nameSingular, + flatObjectMetadata.nameSingular, kind, ); this.gqlTypesStorage.addGqlType( key, new GraphQLObjectType({ - name: `${pascalCase(objectMetadata.nameSingular)}${kind.toString()}`, - description: objectMetadata.description, - fields: () => this.generateFields(objectMetadata), + name: `${pascalCase(flatObjectMetadata.nameSingular)}${kind.toString()}`, + description: flatObjectMetadata.description, + fields: () => this.generateFields(flatObjectMetadata, flatFields), }), ); } private generateFields( - objectMetadata: ObjectMetadataEntity, + flatObjectMetadata: FlatObjectMetadata, + flatFields: FlatFieldMetadata[], ): GraphQLOutputTypeFieldConfigMap { const fields: GraphQLOutputTypeFieldConfigMap = {}; const aggregatedFields = - this.aggregationObjectTypeGenerator.generate(objectMetadata); + this.aggregationObjectTypeGenerator.generate(flatFields); Object.assign(fields, aggregatedFields); const edgeType = this.gqlTypesStorage.getGqlTypeByKey( computeObjectMetadataObjectTypeKey( - objectMetadata.nameSingular, + flatObjectMetadata.nameSingular, ObjectTypeDefinitionKind.Edge, ), ); if (!isDefined(edgeType) || isInputObjectType(edgeType)) { this.logger.error( - `Edge type for ${objectMetadata.nameSingular} was not found. Please, check if you have defined it.`, - { - objectMetadata, - }, + `Edge type for ${flatObjectMetadata.nameSingular} was not found. Please, check if you have defined it.`, ); throw new Error( - `Edge type for ${objectMetadata.nameSingular} was not found. Please, check if you have defined it.`, + `Edge type for ${flatObjectMetadata.nameSingular} was not found. Please, check if you have defined it.`, ); } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/edge-gql-object-type.generator.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/edge-gql-object-type.generator.ts index aa9f1f418c9..9392adde925 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/edge-gql-object-type.generator.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/edge-gql-object-type.generator.ts @@ -9,7 +9,7 @@ import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-build import { GqlTypesStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/gql-types.storage'; import { GraphQLOutputTypeFieldConfigMap } from 'src/engine/api/graphql/workspace-schema-builder/types/graphql-field-config-map.types'; import { computeObjectMetadataObjectTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-object-metadata-object-type-key.util'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { pascalCase } from 'src/utils/pascal-case'; @Injectable() @@ -21,30 +21,30 @@ export class EdgeGqlObjectTypeGenerator { private readonly gqlTypesStorage: GqlTypesStorage, ) {} - public buildAndStore(objectMetadata: ObjectMetadataEntity) { + public buildAndStore(flatObjectMetadata: FlatObjectMetadata) { const kind = ObjectTypeDefinitionKind.Edge; const key = computeObjectMetadataObjectTypeKey( - objectMetadata.nameSingular, + flatObjectMetadata.nameSingular, kind, ); this.gqlTypesStorage.addGqlType( key, new GraphQLObjectType({ - name: `${pascalCase(objectMetadata.nameSingular)}${kind.toString()}`, - description: objectMetadata.description, - fields: () => this.generateFields(objectMetadata), + name: `${pascalCase(flatObjectMetadata.nameSingular)}${kind.toString()}`, + description: flatObjectMetadata.description, + fields: () => this.generateFields(flatObjectMetadata.nameSingular), }), ); } private generateFields( - objectMetadata: ObjectMetadataEntity, + objectNameSingular: string, ): GraphQLOutputTypeFieldConfigMap { const fields: GraphQLOutputTypeFieldConfigMap = {}; const key = computeObjectMetadataObjectTypeKey( - objectMetadata.nameSingular, + objectNameSingular, ObjectTypeDefinitionKind.Plain, ); @@ -52,14 +52,11 @@ export class EdgeGqlObjectTypeGenerator { if (!isDefined(objectType) || isInputObjectType(objectType)) { this.logger.error( - `Node type for ${objectMetadata.nameSingular} was not found. Please, check if you have defined it.`, - { - objectMetadata, - }, + `Node type for ${objectNameSingular} was not found. Please, check if you have defined it.`, ); throw new Error( - `Node type for ${objectMetadata.nameSingular} was not found. Please, check if you have defined it.`, + `Node type for ${objectNameSingular} was not found. Please, check if you have defined it.`, ); } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/group-by-connection-gql-object-type.generator.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/group-by-connection-gql-object-type.generator.ts index 5a04ca46769..f70315776c5 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/group-by-connection-gql-object-type.generator.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/group-by-connection-gql-object-type.generator.ts @@ -9,7 +9,7 @@ import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-build import { GqlTypesStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/gql-types.storage'; import { GraphQLOutputTypeFieldConfigMap } from 'src/engine/api/graphql/workspace-schema-builder/types/graphql-field-config-map.types'; import { computeObjectMetadataObjectTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-object-metadata-object-type-key.util'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { pascalCase } from 'src/utils/pascal-case'; @Injectable() @@ -23,46 +23,43 @@ export class GroupByConnectionGqlObjectTypeGenerator { private readonly gqlTypesStorage: GqlTypesStorage, ) {} - public buildAndStore(objectMetadata: ObjectMetadataEntity) { + public buildAndStore(flatObjectMetadata: FlatObjectMetadata) { const kind = ObjectTypeDefinitionKind.GroupByConnection; const key = computeObjectMetadataObjectTypeKey( - objectMetadata.nameSingular, + flatObjectMetadata.nameSingular, kind, ); this.gqlTypesStorage.addGqlType( key, new GraphQLObjectType({ - name: `${pascalCase(objectMetadata.nameSingular)}${kind.toString()}`, - description: objectMetadata.description, - fields: () => this.generateFields(objectMetadata), + name: `${pascalCase(flatObjectMetadata.nameSingular)}${kind.toString()}`, + description: flatObjectMetadata.description, + fields: () => this.generateFields(flatObjectMetadata.nameSingular), }), ); } private generateFields( - objectMetadata: ObjectMetadataEntity, + objectNameSingular: string, ): GraphQLOutputTypeFieldConfigMap { const fields: GraphQLOutputTypeFieldConfigMap = {}; const connection = this.gqlTypesStorage.getGqlTypeByKey( computeObjectMetadataObjectTypeKey( - objectMetadata.nameSingular, + objectNameSingular, ObjectTypeDefinitionKind.Connection, ), ); if (!isDefined(connection) || !isObjectType(connection)) { this.logger.error( - `Connection type for ${objectMetadata.nameSingular} was not found.`, - { - objectMetadata, - }, + `Connection type for ${objectNameSingular} was not found.`, ); throw new Error( - `Connection type for ${objectMetadata.nameSingular} was not found.`, + `Connection type for ${objectNameSingular} was not found.`, ); } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/object-metadata-gql-object-type.generator.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/object-metadata-gql-object-type.generator.ts index 834d2ca87ec..be61a9db536 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/object-metadata-gql-object-type.generator.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/object-metadata-gql-object-type.generator.ts @@ -16,11 +16,11 @@ import { GraphQLOutputTypeFieldConfigMap } from 'src/engine/api/graphql/workspac import { computeCompositeFieldObjectTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-composite-field-object-type-key.util'; import { computeEnumFieldGqlTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-enum-field-gql-type-key.util'; import { computeObjectMetadataObjectTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-object-metadata-object-type-key.util'; -import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; import { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { isMorphOrRelationFieldMetadataType } from 'src/engine/utils/is-morph-or-relation-field-metadata-type.util'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { pascalCase } from 'src/utils/pascal-case'; @Injectable() @@ -35,29 +35,33 @@ export class ObjectMetadataGqlObjectTypeGenerator { private readonly typeMapperService: TypeMapperService, ) {} - public buildAndStore(objectMetadata: ObjectMetadataEntity) { + public buildAndStore( + flatObjectMetadata: FlatObjectMetadata, + fields: FlatFieldMetadata[], + ) { const kind = ObjectTypeDefinitionKind.Plain; const key = computeObjectMetadataObjectTypeKey( - objectMetadata.nameSingular, + flatObjectMetadata.nameSingular, ObjectTypeDefinitionKind.Plain, ); this.gqlTypesStorage.addGqlType( key, new GraphQLObjectType({ - name: `${pascalCase(objectMetadata.nameSingular)}${kind.toString()}`, - description: objectMetadata.description, - fields: this.generateFields(objectMetadata), + name: `${pascalCase(flatObjectMetadata.nameSingular)}${kind.toString()}`, + description: flatObjectMetadata.description, + fields: this.generateFields(flatObjectMetadata.nameSingular, fields), }), ); } private generateFields( - objectMetadata: ObjectMetadataEntity, + objectNameSingular: string, + fields: FlatFieldMetadata[], ): GraphQLOutputTypeFieldConfigMap { const allGeneratedFields: GraphQLOutputTypeFieldConfigMap = {}; - for (const field of objectMetadata.fields) { + for (const field of fields) { const typeFactoryOptions = { nullable: field.isNullable ?? undefined, isArray: field.type === FieldMetadataType.MULTI_SELECT, @@ -66,13 +70,11 @@ export class ObjectMetadataGqlObjectTypeGenerator { isIdField: false, }; - if (isMorphOrRelationFieldMetadataType(field.type)) { + if (isMorphOrRelationFlatFieldMetadata(field)) { const relationFieldObjectType = this.relationFieldMetadataGqlObjectTypeGenerator.generateRelationFieldObjectType( { - fieldMetadata: field as FieldMetadataEntity< - FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION - >, + fieldMetadata: field, typeOptions: typeFactoryOptions, }, ); @@ -96,7 +98,7 @@ export class ObjectMetadataGqlObjectTypeGenerator { !isDefined(compositeFieldObjectType) || isInputObjectType(compositeFieldObjectType) ) { - const message = `Could not find a GraphQL output type for ${field.name} composite field metadata of object ${objectMetadata.nameSingular}`; + const message = `Could not find a GraphQL output type for ${field.name} composite field metadata of object ${objectNameSingular}`; this.logger.error(message, { type: field.type, @@ -109,7 +111,7 @@ export class ObjectMetadataGqlObjectTypeGenerator { type = compositeFieldObjectType; } else if (isEnumFieldMetadataType(field.type)) { const enumFieldEnumTypeKey = computeEnumFieldGqlTypeKey( - objectMetadata.nameSingular, + objectNameSingular, field.name, ); @@ -120,7 +122,7 @@ export class ObjectMetadataGqlObjectTypeGenerator { !isDefined(enumFieldEnumType) || isInputObjectType(enumFieldEnumType) ) { - const message = `Could not find a GraphQL output type for ${field.name} enum field metadata of object ${objectMetadata.nameSingular}`; + const message = `Could not find a GraphQL output type for ${field.name} enum field metadata of object ${objectNameSingular}`; this.logger.error(message, { type: field.type, @@ -138,7 +140,7 @@ export class ObjectMetadataGqlObjectTypeGenerator { ); if (!isDefined(type)) { - const message = `Could not find a GraphQL output type for ${field.name} scalar field metadata of object ${objectMetadata.nameSingular}`; + const message = `Could not find a GraphQL output type for ${field.name} scalar field metadata of object ${objectNameSingular}`; this.logger.error(message, { type: field.type, diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/object-metadata-with-relations-gql-object-type.generator.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/object-metadata-with-relations-gql-object-type.generator.ts index 1bda5a5e23e..7f7fcf9ba83 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/object-metadata-with-relations-gql-object-type.generator.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/object-metadata-with-relations-gql-object-type.generator.ts @@ -15,12 +15,13 @@ import { ObjectTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schem import { ArgsTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/args-type/args-type.generator'; import { GqlTypesStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/gql-types.storage'; import { GraphQLOutputTypeFieldConfigMap } from 'src/engine/api/graphql/workspace-schema-builder/types/graphql-field-config-map.types'; +import { type SchemaGenerationContext } from 'src/engine/api/graphql/workspace-schema-builder/types/schema-generation-context.type'; import { computeObjectMetadataObjectTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-object-metadata-object-type-key.util'; import { getResolverArgs } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util'; -import { objectContainsRelationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/object-contains-relation-field'; -import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; +import { isMorphOrRelationFieldMetadataType } from 'src/engine/utils/is-morph-or-relation-field-metadata-type.util'; @Injectable() export class ObjectMetadataWithRelationsGqlObjectTypeGenerator { @@ -34,11 +35,12 @@ export class ObjectMetadataWithRelationsGqlObjectTypeGenerator { ) {} public buildAndStore( - objectMetadata: ObjectMetadataEntity, - objectMetadataCollection: ObjectMetadataEntity[], + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadatas: FlatFieldMetadata[], + context: SchemaGenerationContext, ) { const key = computeObjectMetadataObjectTypeKey( - objectMetadata.nameSingular, + flatObjectMetadata.nameSingular, ObjectTypeDefinitionKind.Plain, ); @@ -46,28 +48,26 @@ export class ObjectMetadataWithRelationsGqlObjectTypeGenerator { if (!isDefined(gqlType) || !isObjectType(gqlType)) { this.logger.error( - `Could not find a GraphQL type for ${objectMetadata.id.toString()}`, - { - objectMetadata, - }, + `Could not find a GraphQL type for ${flatObjectMetadata.id.toString()}`, ); throw new Error( - `Could not find a GraphQL type for ${objectMetadata.id.toString()}`, + `Could not find a GraphQL type for ${flatObjectMetadata.id.toString()}`, ); } + const containsRelationOrMorphField = flatFieldMetadatas.some((field) => + isMorphOrRelationFieldMetadataType(field.type), + ); + // Security check to avoid extending an object that does not need to be extended - if (!objectContainsRelationField(objectMetadata)) { + if (!containsRelationOrMorphField) { this.logger.error( - `This object does not need to be extended: ${objectMetadata.id.toString()}`, - { - objectMetadata, - }, + `This object does not need to be extended: ${flatObjectMetadata.id.toString()}`, ); throw new Error( - `This object does not need to be extended: ${objectMetadata.id.toString()}`, + `This object does not need to be extended: ${flatObjectMetadata.id.toString()}`, ); } @@ -81,66 +81,57 @@ export class ObjectMetadataWithRelationsGqlObjectTypeGenerator { ...config, fields: () => ({ ...config.fields, - ...this.generateFields(objectMetadata, objectMetadataCollection), + ...this.generateFields(flatFieldMetadatas, context), }), }), ); } private generateFields( - objectMetadata: ObjectMetadataEntity, - objectMetadataCollection: ObjectMetadataEntity[], + flatFieldMetadatas: FlatFieldMetadata[], + context: SchemaGenerationContext, ): GraphQLOutputTypeFieldConfigMap { - const fields: GraphQLOutputTypeFieldConfigMap = {}; + const outputFields: GraphQLOutputTypeFieldConfigMap = {}; - for (const fieldMetadata of objectMetadata.fields) { - // Ignore non-relation fields as they are already defined - const isRelation = - isFieldMetadataEntityOfType( - fieldMetadata, - FieldMetadataType.RELATION, - ) || - isFieldMetadataEntityOfType( - fieldMetadata, - FieldMetadataType.MORPH_RELATION, - ); - - if (!isRelation) { + for (const flatFieldMetadata of flatFieldMetadatas) { + if (!isMorphOrRelationFlatFieldMetadata(flatFieldMetadata)) { continue; } - if (!fieldMetadata.settings) { + if (!flatFieldMetadata.settings) { throw new Error( - `Field Metadata of type RELATION or MORPH_RELATION with id ${fieldMetadata.id} has no settings`, + `Field Metadata of type RELATION or MORPH_RELATION with id ${flatFieldMetadata.id} has no settings`, ); } - if (!fieldMetadata.relationTargetObjectMetadataId) { + if (!flatFieldMetadata.relationTargetObjectMetadataId) { throw new Error( - `Field Metadata of type RELATION or MORPH_RELATION with id ${fieldMetadata.id} has no relation target object metadata id`, + `Field Metadata of type RELATION or MORPH_RELATION with id ${flatFieldMetadata.id} has no relation target object metadata id`, ); } - const objectMetadataTarget = objectMetadataCollection.find( - (objectMetadata) => - objectMetadata.id === fieldMetadata.relationTargetObjectMetadataId, - ); + const objectMetadataTarget = + context.flatObjectMetadataMaps.byId[ + flatFieldMetadata.relationTargetObjectMetadataId + ]; if (!isDefined(objectMetadataTarget)) { throw new Error( - `Field Metadata of type RELATION or MORPH_RELATION with id ${fieldMetadata.id} has no relation target object metadata`, + `Field Metadata of type RELATION or MORPH_RELATION with id ${flatFieldMetadata.id} has no relation target object metadata`, ); } const targetObjectMetadataGqlObjectType = this.fetchTargetObjectMetadataGqlObjectType( - fieldMetadata, + flatFieldMetadata, objectMetadataTarget, ); let argsType: GraphQLFieldConfigArgumentMap | undefined = undefined; - if (fieldMetadata.settings.relationType === RelationType.ONE_TO_MANY) { + if ( + flatFieldMetadata.settings?.relationType === RelationType.ONE_TO_MANY + ) { const args = getResolverArgs('findMany'); argsType = this.argsTypeGenerator.generate({ @@ -149,31 +140,31 @@ export class ObjectMetadataWithRelationsGqlObjectTypeGenerator { }); } - fields[fieldMetadata.name] = { + outputFields[flatFieldMetadata.name] = { type: targetObjectMetadataGqlObjectType, args: argsType, - description: fieldMetadata.description, + description: flatFieldMetadata.description, }; } - return fields; + return outputFields; } private fetchTargetObjectMetadataGqlObjectType( - fieldMetadata: FieldMetadataEntity< + flatFieldMetadata: FlatFieldMetadata< FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION >, - objectMetadataTarget: ObjectMetadataEntity, + objectMetadataTarget: FlatObjectMetadata, ): GraphQLOutputType { - if (!fieldMetadata.settings) { + if (!flatFieldMetadata.settings) { throw new Error( - `Field Metadata of type RELATION or MORPH_RELATION with id ${fieldMetadata.id} has no settings`, + `Field Metadata of type RELATION or MORPH_RELATION with id ${flatFieldMetadata.id} has no settings`, ); } const key = computeObjectMetadataObjectTypeKey( objectMetadataTarget.nameSingular, - fieldMetadata.settings.relationType === RelationType.ONE_TO_MANY + flatFieldMetadata.settings.relationType === RelationType.ONE_TO_MANY ? ObjectTypeDefinitionKind.Connection : ObjectTypeDefinitionKind.Plain, ); @@ -186,13 +177,12 @@ export class ObjectMetadataWithRelationsGqlObjectTypeGenerator { !isObjectType(targetObjectMetadataGqlObjectType) ) { this.logger.error( - `Could not find a relation type for ${fieldMetadata.id}`, - { - fieldMetadata, - }, + `Could not find a relation type for ${flatFieldMetadata.id}`, ); - throw new Error(`Could not find a relation type for ${fieldMetadata.id}`); + throw new Error( + `Could not find a relation type for ${flatFieldMetadata.id}`, + ); } return targetObjectMetadataGqlObjectType; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/relation-field-metadata-gql-object-type.generator.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/relation-field-metadata-gql-object-type.generator.ts index 89cbcc8b2dc..fa6fd522556 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/relation-field-metadata-gql-object-type.generator.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/relation-field-metadata-gql-object-type.generator.ts @@ -8,7 +8,7 @@ import { TypeOptions, } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service'; import { extractGraphQLRelationFieldNames } from 'src/engine/api/graphql/workspace-schema-builder/utils/extract-graphql-relation-field-names.util'; -import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; @Injectable() export class RelationFieldMetadataGqlObjectTypeGenerator { @@ -22,7 +22,7 @@ export class RelationFieldMetadataGqlObjectTypeGenerator { fieldMetadata, typeOptions, }: { - fieldMetadata: FieldMetadataEntity< + fieldMetadata: FlatFieldMetadata< FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION >; typeOptions: TypeOptions; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/root-types/mutation-type.generator.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/root-types/mutation-type.generator.ts index cb0d144643a..84ef8655467 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/root-types/mutation-type.generator.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/root-types/mutation-type.generator.ts @@ -6,7 +6,7 @@ import { workspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/work import { GqlOperation } from 'src/engine/api/graphql/workspace-schema-builder/enums/gql-operation.enum'; import { RootTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/root-types/root-type.generator'; import { GqlTypesStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/gql-types.storage'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { type SchemaGenerationContext } from 'src/engine/api/graphql/workspace-schema-builder/types/schema-generation-context.type'; @Injectable() export class MutationTypeGenerator { @@ -15,9 +15,9 @@ export class MutationTypeGenerator { private readonly gqlTypesStorage: GqlTypesStorage, ) {} - buildAndStore(objectMetadataCollection: ObjectMetadataEntity[]) { + buildAndStore(context: SchemaGenerationContext) { return this.rootTypeGenerator.buildAndStore( - objectMetadataCollection, + context, [...workspaceResolverBuilderMethodNames.mutations], GqlOperation.Mutation, ); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/root-types/query-type.generator.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/root-types/query-type.generator.ts index 30c3fca1ca1..368cede098e 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/root-types/query-type.generator.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/root-types/query-type.generator.ts @@ -6,7 +6,7 @@ import { workspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/work import { GqlOperation } from 'src/engine/api/graphql/workspace-schema-builder/enums/gql-operation.enum'; import { RootTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/root-types/root-type.generator'; import { GqlTypesStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/gql-types.storage'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { type SchemaGenerationContext } from 'src/engine/api/graphql/workspace-schema-builder/types/schema-generation-context.type'; @Injectable() export class QueryTypeGenerator { @@ -15,9 +15,9 @@ export class QueryTypeGenerator { private readonly gqlTypesStorage: GqlTypesStorage, ) {} - async buildAndStore(objectMetadataCollection: ObjectMetadataEntity[]) { + async buildAndStore(context: SchemaGenerationContext) { return this.rootTypeGenerator.buildAndStore( - objectMetadataCollection, + context, workspaceResolverBuilderMethodNames.queries, GqlOperation.Query, ); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/root-types/root-type.generator.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/root-types/root-type.generator.ts index 131d3357b3e..e5e660227ad 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/root-types/root-type.generator.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/root-types/root-type.generator.ts @@ -11,10 +11,11 @@ import { ObjectTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schem import { ArgsTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/args-type/args-type.generator'; import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service'; import { GqlTypesStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/gql-types.storage'; +import { type SchemaGenerationContext } from 'src/engine/api/graphql/workspace-schema-builder/types/schema-generation-context.type'; import { GraphQLRootTypeFieldConfigMap } from 'src/engine/api/graphql/workspace-schema-builder/types/graphql-field-config-map.types'; import { computeObjectMetadataObjectTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-object-metadata-object-type-key.util'; import { getResolverArgs } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { getResolverName } from 'src/engine/utils/get-resolver-name.util'; @Injectable() @@ -29,7 +30,7 @@ export class RootTypeGenerator { ) {} buildAndStore( - objectMetadataCollection: ObjectMetadataEntity[], + context: SchemaGenerationContext, workspaceResolverMethodNames: readonly WorkspaceResolverBuilderMethodNames[], objectTypeName: GqlOperation, ) { @@ -47,6 +48,10 @@ export class RootTypeGenerator { ); } + const objectMetadataCollection = Object.values( + context.flatObjectMetadataMaps.byId, + ).filter(isDefined); + this.gqlTypesStorage.addGqlType( objectTypeName, new GraphQLObjectType({ @@ -60,7 +65,7 @@ export class RootTypeGenerator { } private generateFields( - objectMetadataCollection: ObjectMetadataEntity[], + objectMetadataCollection: FlatObjectMetadata[], workspaceResolverMethodNames: readonly WorkspaceResolverBuilderMethodNames[], ): GraphQLRootTypeFieldConfigMap { const fieldConfigMap: GraphQLRootTypeFieldConfigMap = {}; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface.ts index 470fc18cca8..1b7737ac387 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface.ts @@ -1,9 +1,12 @@ import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; export interface WorkspaceSchemaBuilderContext { authContext: AuthContext; - objectMetadataMaps: ObjectMetadataMaps; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; + flatObjectMetadata: FlatObjectMetadata; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; + objectIdByNameSingular: Record; } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/types/schema-generation-context.type.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/types/schema-generation-context.type.ts new file mode 100644 index 00000000000..1919f5feada --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/types/schema-generation-context.type.ts @@ -0,0 +1,10 @@ +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; + +export type SchemaGenerationContext = { + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; + flatIndexMaps: FlatEntityMaps; +}; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/compute-field-input-type-options.util.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/compute-field-input-type-options.util.ts index 706c73fa250..d07e17d4e01 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/compute-field-input-type-options.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/compute-field-input-type-options.util.ts @@ -2,15 +2,15 @@ import { FieldMetadataType } from 'twenty-shared/types'; import { GqlInputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/enums/gql-input-type-definition-kind.enum'; import { type TypeOptions } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; -export const computeFieldInputTypeOptions = ( - fieldMetadata: FieldMetadataEntity, +export const computeFieldInputTypeOptions = ( + fieldMetadata: FlatFieldMetadata, kind: GqlInputTypeDefinitionKind, ): TypeOptions => { return { nullable: fieldMetadata.isNullable ?? undefined, - defaultValue: fieldMetadata.defaultValue, + defaultValue: fieldMetadata.defaultValue ?? undefined, isArray: kind !== GqlInputTypeDefinitionKind.Filter && fieldMetadata.type === FieldMetadataType.MULTI_SELECT, diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/extract-graphql-relation-field-names.util.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/extract-graphql-relation-field-names.util.ts index 8fb0c525d35..7426cafd880 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/extract-graphql-relation-field-names.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/extract-graphql-relation-field-names.util.ts @@ -1,13 +1,14 @@ import { type FieldMetadataType } from 'twenty-shared/types'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; export const extractGraphQLRelationFieldNames = ( - fieldMetadata: FieldMetadataEntity< + fieldMetadata: FlatFieldMetadata< FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION >, ) => { - const joinColumnName = fieldMetadata.settings?.joinColumnName; + const settings = fieldMetadata.settings; + const joinColumnName = settings?.joinColumnName; if (!joinColumnName) { throw new Error('Join column name is not defined'); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util.ts index 92c59a84913..3c872a52465 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util.ts @@ -6,7 +6,7 @@ import { FieldMetadataType } from 'twenty-shared/types'; import { capitalize, isFieldMetadataDateKind } from 'twenty-shared/utils'; import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; import { getSubfieldsForAggregateOperation } from 'src/engine/twenty-orm/utils/get-subfields-for-aggregate-operation.util'; export type AggregationField = { @@ -20,7 +20,7 @@ export type AggregationField = { }; export const getAvailableAggregationsFromObjectFields = ( - fields: FieldMetadataEntity[], + fields: FlatFieldMetadata[], ): Record => { return fields.reduce>( (acc, field) => { diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util.ts new file mode 100644 index 00000000000..3c7fcfc5967 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util.ts @@ -0,0 +1,14 @@ +import { isDefined } from 'twenty-shared/utils'; + +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; + +export const getFlatFieldsFromFlatObjectMetadata = ( + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, +): FlatFieldMetadata[] => { + return flatObjectMetadata.fieldMetadataIds + .map((fieldId) => flatFieldMetadataMaps.byId[fieldId]) + .filter(isDefined); +}; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/object-contains-morph-relation-field.util.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/object-contains-morph-relation-field.util.ts deleted file mode 100644 index 58a4ea5a136..00000000000 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/object-contains-morph-relation-field.util.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { isMorphRelationFieldMetadataType } from 'src/engine/utils/is-morph-relation-field-metadata-type.util'; - -export const objectContainsMorphRelationField = ( - objectMetadata: ObjectMetadataEntity, -): boolean => { - return objectMetadata.fields.some((field) => - isMorphRelationFieldMetadataType(field.type), - ); -}; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/object-contains-relation-field.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/object-contains-relation-field.ts deleted file mode 100644 index 33e70833933..00000000000 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/object-contains-relation-field.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { isMorphOrRelationFieldMetadataType } from 'src/engine/utils/is-morph-or-relation-field-metadata-type.util'; - -export const objectContainsRelationField = ( - objectMetadata: ObjectMetadataEntity, -): boolean => { - return objectMetadata.fields.some((field) => - isMorphOrRelationFieldMetadataType(field.type), - ); -}; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/workspace-graphql-schema.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/workspace-graphql-schema.factory.ts index 980d6f79d28..bc2c62503a7 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/workspace-graphql-schema.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/workspace-graphql-schema.factory.ts @@ -6,7 +6,7 @@ import { GqlTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builde import { OrphanedTypesGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/orphaned-types.generator'; import { MutationTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/root-types/mutation-type.generator'; import { QueryTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/root-types/query-type.generator'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { type SchemaGenerationContext } from 'src/engine/api/graphql/workspace-schema-builder/types/schema-generation-context.type'; @Injectable() export class WorkspaceGraphQLSchemaGenerator { @@ -18,9 +18,9 @@ export class WorkspaceGraphQLSchemaGenerator { ) {} async generateSchema( - objectMetadataCollection: ObjectMetadataEntity[], + context: SchemaGenerationContext, ): Promise { - await this.gqlTypeGenerator.buildAndStore(objectMetadataCollection); + await this.gqlTypeGenerator.buildAndStore(context); // Assemble schema const schema = new GraphQLSchema({ diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema.factory.ts index 91cf818db5e..988dd0890a5 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema.factory.ts @@ -14,7 +14,6 @@ import { AuthExceptionCode, } from 'src/engine/core-modules/auth/auth.exception'; import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; -import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { FlatEntityMapsException, @@ -22,11 +21,7 @@ import { } from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util'; -import { buildObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-metadata-item-with-field-maps.util'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; -import { standardObjectMetadataDefinitions } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects'; -import { shouldExcludeFromWorkspaceApi } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/should-exclude-from-workspace-api.util'; @Injectable() export class WorkspaceSchemaFactory { @@ -37,7 +32,6 @@ export class WorkspaceSchemaFactory { private readonly workspaceResolverFactory: WorkspaceResolverFactory, private readonly workspaceCacheStorageService: WorkspaceCacheStorageService, private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, - private readonly featureFlagService: FeatureFlagService, ) {} async createGraphQLSchema(authContext: AuthContext): Promise { @@ -82,8 +76,12 @@ export class WorkspaceSchemaFactory { ); } - const workspaceFeatureFlagsMap = - await this.featureFlagService.getWorkspaceFeatureFlagsMap(workspaceId); + if (!isDefined(flatFieldMetadataMaps)) { + throw new FlatEntityMapsException( + 'Field metadata collection not found', + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } let metadataVersion = await this.workspaceCacheStorageService.getMetadataVersion(workspaceId); @@ -99,37 +97,7 @@ export class WorkspaceSchemaFactory { const { idByNameSingular } = buildObjectIdByNameMaps( flatObjectMetadataMaps, ); - const objectMetadataMaps: ObjectMetadataMaps = { - byId: {}, - idByNameSingular, - }; - for (const [id, flatObj] of Object.entries(flatObjectMetadataMaps.byId)) { - if (isDefined(flatObj)) { - objectMetadataMaps.byId[id] = buildObjectMetadataItemWithFieldMaps( - flatObj, - flatFieldMetadataMaps, - flatIndexMaps, - ); - } - } - - const objectMetadataCollection = Object.values(objectMetadataMaps.byId) - .filter(isDefined) - .map((objectMetadataItem) => ({ - ...objectMetadataItem, - fields: Object.values(objectMetadataItem.fieldsById), - indexes: objectMetadataItem.indexMetadatas, - })) - .filter((objectMetadata) => { - return !shouldExcludeFromWorkspaceApi( - objectMetadata, - standardObjectMetadataDefinitions, - workspaceFeatureFlagsMap, - ); - }); - - // Get typeDefs from cache let typeDefs = await this.workspaceCacheStorageService.getGraphQLTypeDefs( authContext.workspace.id, metadataVersion, @@ -140,12 +108,13 @@ export class WorkspaceSchemaFactory { metadataVersion, ); - // If typeDefs are not cached, generate them if (!typeDefs || !usedScalarNames) { const autoGeneratedSchema = - await this.workspaceGraphQLSchemaGenerator.generateSchema( - objectMetadataCollection, - ); + await this.workspaceGraphQLSchemaGenerator.generateSchema({ + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatIndexMaps, + }); usedScalarNames = this.scalarsExplorerService.getUsedScalarNames(autoGeneratedSchema); @@ -165,7 +134,9 @@ export class WorkspaceSchemaFactory { const autoGeneratedResolvers = await this.workspaceResolverFactory.create( authContext, - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + idByNameSingular, workspaceResolverBuilderMethodNames, ); const scalarsResolvers = diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-base.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-base.handler.ts index 800d9a9c3ec..84a1b7b9897 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-base.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-base.handler.ts @@ -22,15 +22,16 @@ import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspac import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type'; import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util'; -import { buildObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-metadata-item-with-field-maps.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { PermissionsException, PermissionsExceptionCode, PermissionsExceptionMessage, } from 'src/engine/metadata-modules/permissions/permissions.exception'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; @@ -128,21 +129,24 @@ export abstract class RestApiBaseHandler { async computeSelectedFields({ authContext, depth, - objectMetadataMapItem, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }: { authContext: WorkspaceAuthContext; depth?: Depth | undefined; - objectMetadataMapItem: ObjectMetadataItemWithFieldMaps; - objectMetadataMaps: ObjectMetadataMaps; + flatObjectMetadata: FlatObjectMetadata; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; }): Promise { const { objectsPermissions } = await this.getObjectsPermissions(authContext); return this.restToCommonSelectedFieldsHandler.computeFromDepth({ objectsPermissions, - objectMetadataMaps, - objectMetadataMapItem, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatObjectMetadata, depth, }); } @@ -150,15 +154,21 @@ export abstract class RestApiBaseHandler { async buildCommonOptions(request: AuthenticatedRequest) { const { object: parsedObject } = parseCorePath(request); - const { objectMetadataMaps, objectMetadataMapItem } = - await this.getObjectMetadata(request, parsedObject); + const { + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, + } = await this.getObjectMetadata(request, parsedObject); const authContext = this.getAuthContextFromRequest(request); return { authContext, - objectMetadataItemWithFieldMaps: objectMetadataMapItem, - objectMetadataMaps: objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, }; } @@ -166,8 +176,11 @@ export abstract class RestApiBaseHandler { request: AuthenticatedRequest, parsedObject: string, ): Promise<{ - objectMetadataMaps: ObjectMetadataMaps; - objectMetadataMapItem: ObjectMetadataItemWithFieldMaps; + flatObjectMetadata: FlatObjectMetadata; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; + flatIndexMaps: FlatEntityMaps; + objectIdByNameSingular: Record; }> { const { workspace } = await this.accessTokenService.validateTokenByRequest(request); @@ -242,16 +255,10 @@ export abstract class RestApiBaseHandler { const workspaceFeatureFlagsMap = await this.featureFlagService.getWorkspaceFeatureFlagsMap(workspace.id); - const objectMetadataItem = buildObjectMetadataItemWithFieldMaps( - flatObjectMetadataItem, - flatFieldMetadataMaps, - flatIndexMaps, - ); - // Check if this entity is workspace-gated and should be blocked from workspace API if ( shouldExcludeFromWorkspaceApi( - objectMetadataItem, + flatObjectMetadataItem, standardObjectMetadataDefinitions, workspaceFeatureFlagsMap, ) @@ -261,24 +268,12 @@ export abstract class RestApiBaseHandler { ); } - const objectMetadataMaps: ObjectMetadataMaps = { - byId: {}, - idByNameSingular, - }; - - for (const [id, flatObj] of Object.entries(flatObjectMetadataMaps.byId)) { - if (isDefined(flatObj)) { - objectMetadataMaps.byId[id] = buildObjectMetadataItemWithFieldMaps( - flatObj, - flatFieldMetadataMaps, - flatIndexMaps, - ); - } - } - return { - objectMetadataMaps, - objectMetadataMapItem: objectMetadataItem, + flatObjectMetadata: flatObjectMetadataItem, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatIndexMaps, + objectIdByNameSingular: idByNameSingular, }; } } diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-create-many.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-create-many.handler.ts index 0cb0b8a0ec3..61b40ae6c40 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-create-many.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-create-many.handler.ts @@ -23,14 +23,17 @@ export class RestApiCreateManyHandler extends RestApiBaseHandler { const { authContext, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, } = await this.buildCommonOptions(request); const selectedFields = await this.computeSelectedFields({ depth, - objectMetadataMapItem: objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext, }); @@ -38,15 +41,14 @@ export class RestApiCreateManyHandler extends RestApiBaseHandler { { data, selectedFields, upsert }, { authContext, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, }, ); - return this.formatRestResponse( - records, - objectMetadataItemWithFieldMaps.namePlural, - ); + return this.formatRestResponse(records, flatObjectMetadata.namePlural); } catch (error) { return workspaceQueryRunnerRestApiExceptionHandler(error); } diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-create-one.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-create-one.handler.ts index 5694046a476..97f4b67de76 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-create-one.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-create-one.handler.ts @@ -24,14 +24,17 @@ export class RestApiCreateOneHandler extends RestApiBaseHandler { const { authContext, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, } = await this.buildCommonOptions(request); const selectedFields = await this.computeSelectedFields({ depth, - objectMetadataMapItem: objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext, }); @@ -39,15 +42,14 @@ export class RestApiCreateOneHandler extends RestApiBaseHandler { { data, selectedFields, upsert }, { authContext, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, }, ); - return this.formatRestResponse( - record, - objectMetadataItemWithFieldMaps.nameSingular, - ); + return this.formatRestResponse(record, flatObjectMetadata.nameSingular); } catch (error) { return workspaceQueryRunnerRestApiExceptionHandler(error); } diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-delete-many.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-delete-many.handler.ts index 85e19dd1c22..e7fff3d16ad 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-delete-many.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-delete-many.handler.ts @@ -31,23 +31,27 @@ export class RestApiDeleteManyHandler extends RestApiBaseHandler { ); } - const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } = - await this.buildCommonOptions(request); + const { + authContext, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, + } = await this.buildCommonOptions(request); try { const records = await this.commonDeleteManyQueryRunnerService.execute( { filter, selectedFields: { id: true } }, { authContext, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, }, ); - return this.formatRestResponse( - records, - objectMetadataItemWithFieldMaps.namePlural, - ); + return this.formatRestResponse(records, flatObjectMetadata.namePlural); } catch (error) { return workspaceQueryRunnerRestApiExceptionHandler(error); } diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-delete-one.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-delete-one.handler.ts index 93e95b030ca..9f85b317e9a 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-delete-one.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-delete-one.handler.ts @@ -23,23 +23,24 @@ export class RestApiDeleteOneHandler extends RestApiBaseHandler { const { authContext, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, } = await this.buildCommonOptions(request); const record = await this.commonDeleteOneQueryRunnerService.execute( { id, selectedFields: { id: true } }, { authContext, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, }, ); - return this.formatRestResponse( - record, - objectMetadataItemWithFieldMaps.nameSingular, - ); + return this.formatRestResponse(record, flatObjectMetadata.nameSingular); } catch (error) { return workspaceQueryRunnerRestApiExceptionHandler(error); } diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-destroy-many.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-destroy-many.handler.ts index 3f16703d624..5e675588373 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-destroy-many.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-destroy-many.handler.ts @@ -31,23 +31,27 @@ export class RestApiDestroyManyHandler extends RestApiBaseHandler { ); } - const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } = - await this.buildCommonOptions(request); + const { + authContext, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, + } = await this.buildCommonOptions(request); try { const records = await this.commonDestroyManyQueryRunnerService.execute( { filter, selectedFields: { id: true } }, { authContext, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, }, ); - return this.formatRestResponse( - records, - objectMetadataItemWithFieldMaps.namePlural, - ); + return this.formatRestResponse(records, flatObjectMetadata.namePlural); } catch (error) { return workspaceQueryRunnerRestApiExceptionHandler(error); } diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-destroy-one.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-destroy-one.handler.ts index b5feb4be1c2..572b95e65d1 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-destroy-one.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-destroy-one.handler.ts @@ -23,23 +23,24 @@ export class RestApiDestroyOneHandler extends RestApiBaseHandler { const { authContext, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, } = await this.buildCommonOptions(request); const record = await this.commonDestroyOneQueryRunnerService.execute( { id, selectedFields: { id: true } }, { authContext, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, }, ); - return this.formatRestResponse( - record, - objectMetadataItemWithFieldMaps.nameSingular, - ); + return this.formatRestResponse(record, flatObjectMetadata.nameSingular); } catch (error) { return workspaceQueryRunnerRestApiExceptionHandler(error); } diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-find-duplicates.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-find-duplicates.handler.ts index 66b0125baa8..ba1e29a308a 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-find-duplicates.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-find-duplicates.handler.ts @@ -22,14 +22,17 @@ export class RestApiFindDuplicatesHandler extends RestApiBaseHandler { const { authContext, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, } = await this.buildCommonOptions(request); const selectedFields = await this.computeSelectedFields({ depth, - objectMetadataMapItem: objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext, }); @@ -38,14 +41,16 @@ export class RestApiFindDuplicatesHandler extends RestApiBaseHandler { { data, ids, selectedFields }, { authContext, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, }, ); return this.formatRestResponse( duplicateConnections, - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, ); } catch (error) { return workspaceQueryRunnerRestApiExceptionHandler(error); diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-find-many.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-find-many.handler.ts index 7ffda7a5816..6d6e9909e44 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-find-many.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-find-many.handler.ts @@ -29,14 +29,17 @@ export class RestApiFindManyHandler extends RestApiBaseHandler { const parsedArgs = this.parseRequestArgs(request); const { authContext, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, } = await this.buildCommonOptions(request); const selectedFields = await this.computeSelectedFields({ depth: parsedArgs.depth, - objectMetadataMapItem: objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext, }); @@ -48,15 +51,17 @@ export class RestApiFindManyHandler extends RestApiBaseHandler { }, { authContext, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, }, ); return this.formatRestResponse( records, aggregatedValues, - objectMetadataItemWithFieldMaps.namePlural, + flatObjectMetadata.namePlural, pageInfo, ); } catch (error) { diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-find-one.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-find-one.handler.ts index a018ed6660a..6f5cabdfe1d 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-find-one.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-find-one.handler.ts @@ -22,14 +22,17 @@ export class RestApiFindOneHandler extends RestApiBaseHandler { const { filter, depth } = await this.parseRequestArgs(request); const { authContext, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, } = await this.buildCommonOptions(request); const selectedFields = await this.computeSelectedFields({ depth, - objectMetadataMapItem: objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext, }); @@ -37,15 +40,14 @@ export class RestApiFindOneHandler extends RestApiBaseHandler { { filter, selectedFields }, { authContext, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, }, ); - return this.formatRestResponse( - record, - objectMetadataItemWithFieldMaps.nameSingular, - ); + return this.formatRestResponse(record, flatObjectMetadata.nameSingular); } catch (error) { return workspaceQueryRunnerRestApiExceptionHandler(error); } diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-group-by.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-group-by.handler.ts index d8fe20944a0..72af8d35e6e 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-group-by.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-group-by.handler.ts @@ -29,8 +29,10 @@ export class RestApiGroupByHandler extends RestApiBaseHandler { groupBy, selectedFields, authContext, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, includeRecords, orderByForRecords, } = await this.parseRequestArgs(request); @@ -47,8 +49,10 @@ export class RestApiGroupByHandler extends RestApiBaseHandler { }, { authContext, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, }, ); } catch (error) { @@ -57,8 +61,13 @@ export class RestApiGroupByHandler extends RestApiBaseHandler { } private async parseRequestArgs(request: AuthenticatedRequest) { - const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } = - await this.buildCommonOptions(request); + const { + authContext, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, + } = await this.buildCommonOptions(request); const orderByWithGroupBy = parseOrderByWithGroupByRestRequest(request); const orderByForRecordsWithGroupBy = @@ -73,8 +82,9 @@ export class RestApiGroupByHandler extends RestApiBaseHandler { if (includeRecords) { const selectableFields = await this.computeSelectedFields({ depth: 0, - objectMetadataMapItem: objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext, }); @@ -83,8 +93,10 @@ export class RestApiGroupByHandler extends RestApiBaseHandler { return { authContext, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, filter, orderBy: orderByWithGroupBy, orderByForRecords: orderByForRecordsWithGroupBy, diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-merge-many.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-merge-many.handler.ts index f624570ce9c..34689a286b0 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-merge-many.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-merge-many.handler.ts @@ -3,8 +3,8 @@ import { Injectable } from '@nestjs/common'; import { ObjectRecord } from 'twenty-shared/types'; import { capitalize } from 'twenty-shared/utils'; -import { RestApiBaseHandler } from 'src/engine/api/rest/core/handlers/rest-api-base.handler'; import { CommonMergeManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-merge-many-query-runner.service'; +import { RestApiBaseHandler } from 'src/engine/api/rest/core/handlers/rest-api-base.handler'; import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util'; import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request'; import { workspaceQueryRunnerRestApiExceptionHandler } from 'src/engine/api/rest/utils/workspace-query-runner-rest-api-exception-handler.util'; @@ -22,14 +22,17 @@ export class RestApiMergeManyHandler extends RestApiBaseHandler { const { depth, ...restArgs } = await this.parseRequestArgs(request); const { authContext, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, } = await this.buildCommonOptions(request); const selectedFields = await this.computeSelectedFields({ depth, - objectMetadataMapItem: objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext, }); @@ -37,15 +40,14 @@ export class RestApiMergeManyHandler extends RestApiBaseHandler { { ...restArgs, selectedFields }, { authContext, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, }, ); - return this.formatRestResponse( - record, - objectMetadataItemWithFieldMaps.nameSingular, - ); + return this.formatRestResponse(record, flatObjectMetadata.nameSingular); } catch (error) { return workspaceQueryRunnerRestApiExceptionHandler(error); } diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-restore-many.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-restore-many.handler.ts index 11519881490..c9788916c7c 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-restore-many.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-restore-many.handler.ts @@ -3,8 +3,8 @@ import { Injectable } from '@nestjs/common'; import { ObjectRecord } from 'twenty-shared/types'; import { capitalize } from 'twenty-shared/utils'; -import { RestApiBaseHandler } from 'src/engine/api/rest/core/handlers/rest-api-base.handler'; import { CommonRestoreManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-restore-many-query-runner.service'; +import { RestApiBaseHandler } from 'src/engine/api/rest/core/handlers/rest-api-base.handler'; import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util'; import { parseFilterRestRequest } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-rest-request.util'; import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request'; @@ -28,14 +28,17 @@ export class RestApiRestoreManyHandler extends RestApiBaseHandler { const { authContext, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, } = await this.buildCommonOptions(request); const selectedFields = await this.computeSelectedFields({ depth, - objectMetadataMapItem: objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext, }); @@ -43,15 +46,14 @@ export class RestApiRestoreManyHandler extends RestApiBaseHandler { { filter, selectedFields }, { authContext, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, }, ); - return this.formatRestResponse( - records, - objectMetadataItemWithFieldMaps.namePlural, - ); + return this.formatRestResponse(records, flatObjectMetadata.namePlural); } catch (error) { return workspaceQueryRunnerRestApiExceptionHandler(error); } diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-restore-one.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-restore-one.handler.ts index b248987e93e..575dab82d6c 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-restore-one.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-restore-one.handler.ts @@ -24,14 +24,17 @@ export class RestApiRestoreOneHandler extends RestApiBaseHandler { const { authContext, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, } = await this.buildCommonOptions(request); const selectedFields = await this.computeSelectedFields({ depth, - objectMetadataMapItem: objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext, }); @@ -39,15 +42,14 @@ export class RestApiRestoreOneHandler extends RestApiBaseHandler { { id, selectedFields }, { authContext, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, }, ); - return this.formatRestResponse( - record, - objectMetadataItemWithFieldMaps.nameSingular, - ); + return this.formatRestResponse(record, flatObjectMetadata.nameSingular); } catch (error) { return workspaceQueryRunnerRestApiExceptionHandler(error); } diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-update-many.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-update-many.handler.ts index 2746f10a55e..28f081a386b 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-update-many.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-update-many.handler.ts @@ -24,14 +24,17 @@ export class RestApiUpdateManyHandler extends RestApiBaseHandler { const { authContext, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, } = await this.buildCommonOptions(request); const selectedFields = await this.computeSelectedFields({ depth, - objectMetadataMapItem: objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext, }); @@ -39,15 +42,14 @@ export class RestApiUpdateManyHandler extends RestApiBaseHandler { { data, filter, selectedFields }, { authContext, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, }, ); - return this.formatRestResponse( - records, - objectMetadataItemWithFieldMaps.namePlural, - ); + return this.formatRestResponse(records, flatObjectMetadata.namePlural); } catch (error) { return workspaceQueryRunnerRestApiExceptionHandler(error); } diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-update-one.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-update-one.handler.ts index b881ba80988..efb138b04cc 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-update-one.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-update-one.handler.ts @@ -24,14 +24,17 @@ export class RestApiUpdateOneHandler extends RestApiBaseHandler { const { authContext, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, } = await this.buildCommonOptions(request); const selectedFields = await this.computeSelectedFields({ depth, - objectMetadataMapItem: objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext, }); @@ -39,15 +42,14 @@ export class RestApiUpdateOneHandler extends RestApiBaseHandler { { id, data, selectedFields }, { authContext, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, }, ); - return this.formatRestResponse( - record, - objectMetadataItemWithFieldMaps.nameSingular, - ); + return this.formatRestResponse(record, flatObjectMetadata.nameSingular); } catch (error) { return workspaceQueryRunnerRestApiExceptionHandler(error); } diff --git a/packages/twenty-server/src/engine/api/rest/core/rest-to-common-args-handlers/selected-fields-handler.ts b/packages/twenty-server/src/engine/api/rest/core/rest-to-common-args-handlers/selected-fields-handler.ts index 7635f675b76..2f3fc4fee5d 100644 --- a/packages/twenty-server/src/engine/api/rest/core/rest-to-common-args-handlers/selected-fields-handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/rest-to-common-args-handlers/selected-fields-handler.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { FieldMetadataType, ObjectsPermissions } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; @@ -7,9 +7,11 @@ import { CommonSelectedFields } from 'src/engine/api/common/types/common-selecte import { getAllSelectableFields } from 'src/engine/api/rest/core/rest-to-common-args-handlers/utils/get-all-selectable-fields.util'; import { MAX_DEPTH } from 'src/engine/api/rest/input-request-parsers/constants/max-depth.constant'; import { Depth } from 'src/engine/api/rest/input-request-parsers/types/depth.type'; -import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; -import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; +import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; +import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util'; +import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; type SelectFields = { [key: string]: boolean | SelectFields; @@ -19,30 +21,32 @@ type SelectFields = { export class RestToCommonSelectedFieldsHandler { computeFromDepth = ({ objectsPermissions, - objectMetadataMaps, - objectMetadataMapItem, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatObjectMetadata, depth, }: { objectsPermissions: ObjectsPermissions; - objectMetadataMaps: ObjectMetadataMaps; - objectMetadataMapItem: ObjectMetadataItemWithFieldMaps; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; + flatObjectMetadata: FlatObjectMetadata; depth: Depth | undefined; }): CommonSelectedFields => { const restrictedFields = - objectsPermissions[objectMetadataMapItem.id].restrictedFields; + objectsPermissions[flatObjectMetadata.id].restrictedFields; const relationsSelectFields = this.getRelationsAndRelationsSelectFields({ - objectMetadataMaps, - objectMetadataMapItem, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatObjectMetadata, objectsPermissions, depth, }); const selectableFields = getAllSelectableFields({ restrictedFields, - objectMetadata: { - objectMetadataMapItem, - }, + flatObjectMetadata, + flatFieldMetadataMaps, }); return { @@ -52,13 +56,15 @@ export class RestToCommonSelectedFieldsHandler { }; private getRelationsAndRelationsSelectFields({ - objectMetadataMaps, - objectMetadataMapItem, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatObjectMetadata, objectsPermissions, depth, }: { - objectMetadataMaps: ObjectMetadataMaps; - objectMetadataMapItem: ObjectMetadataItemWithFieldMaps; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; + flatObjectMetadata: FlatObjectMetadata; objectsPermissions: ObjectsPermissions; depth: Depth | undefined; }) { @@ -66,46 +72,49 @@ export class RestToCommonSelectedFieldsHandler { let relationsSelectFields: SelectFields = {}; - for (const field of Object.values(objectMetadataMapItem.fieldsById)) { - if (!isFieldMetadataEntityOfType(field, FieldMetadataType.RELATION)) + for (const fieldId of flatObjectMetadata.fieldMetadataIds) { + const flatField = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityMaps: flatFieldMetadataMaps, + flatEntityId: fieldId, + }); + + if (!isFlatFieldMetadataOfType(flatField, FieldMetadataType.RELATION)) continue; const relationTargetObjectMetadata = - objectMetadataMaps.byId[field.relationTargetObjectMetadataId]; + findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityMaps: flatObjectMetadataMaps, + flatEntityId: flatField.relationTargetObjectMetadataId, + }); - if (!isDefined(relationTargetObjectMetadata)) { - throw new BadRequestException( - `Object metadata relation target not found for relation creation payload`, - ); - } const relationFieldSelectFields = getAllSelectableFields({ restrictedFields: objectsPermissions[relationTargetObjectMetadata.id].restrictedFields, - objectMetadata: { - objectMetadataMapItem: relationTargetObjectMetadata, - }, + flatObjectMetadata: relationTargetObjectMetadata, + flatFieldMetadataMaps, }); if (Object.keys(relationFieldSelectFields).length === 0) continue; if ( depth === MAX_DEPTH && - isDefined(field.relationTargetObjectMetadataId) + isDefined(flatField.relationTargetObjectMetadataId) ) { const depth2RelationsSelectFields = this.getRelationsAndRelationsSelectFields({ - objectMetadataMaps, - objectMetadataMapItem: relationTargetObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatObjectMetadata: relationTargetObjectMetadata, objectsPermissions, depth: 1, }); - relationsSelectFields[field.name] = { + relationsSelectFields[flatField.name] = { ...relationFieldSelectFields, ...depth2RelationsSelectFields, }; } else { - relationsSelectFields[field.name] = relationFieldSelectFields; + relationsSelectFields[flatField.name] = relationFieldSelectFields; } } diff --git a/packages/twenty-server/src/engine/api/rest/core/rest-to-common-args-handlers/utils/__tests__/get-all-selectable-fields.util.spec.ts b/packages/twenty-server/src/engine/api/rest/core/rest-to-common-args-handlers/utils/__tests__/get-all-selectable-fields.util.spec.ts index 1325e1f4912..3f49092f821 100644 --- a/packages/twenty-server/src/engine/api/rest/core/rest-to-common-args-handlers/utils/__tests__/get-all-selectable-fields.util.spec.ts +++ b/packages/twenty-server/src/engine/api/rest/core/rest-to-common-args-handlers/utils/__tests__/get-all-selectable-fields.util.spec.ts @@ -1,30 +1,97 @@ import { FieldMetadataType } from 'twenty-shared/types'; import { getAllSelectableFields } from 'src/engine/api/rest/core/rest-to-common-args-handlers/utils/get-all-selectable-fields.util'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; describe('getAllSelectableFields', () => { + const createMockField = ( + overrides: Partial & { + id: string; + name: string; + type: FieldMetadataType; + }, + ): FlatFieldMetadata => + ({ + objectMetadataId: 'object-id', + workspaceId: 'workspace-id', + isNullable: true, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), + universalIdentifier: overrides.id, + viewFieldIds: [], + viewFilterIds: [], + viewGroupIds: [], + kanbanAggregateOperationViewIds: [], + calendarViewIds: [], + applicationId: null, + label: overrides.name, + ...overrides, + }) as FlatFieldMetadata; + + const buildFlatFieldMetadataMaps = ( + fields: FlatFieldMetadata[], + ): FlatEntityMaps => ({ + byId: fields.reduce( + (acc, field) => { + acc[field.id] = field; + + return acc; + }, + {} as Record, + ), + idByUniversalIdentifier: fields.reduce( + (acc, field) => { + acc[field.universalIdentifier] = field.id; + + return acc; + }, + {} as Record, + ), + universalIdentifiersByApplicationId: {}, + }); + + const buildFlatObjectMetadata = (fieldIds: string[]): FlatObjectMetadata => + ({ + id: 'object-id', + workspaceId: 'workspace-id', + nameSingular: 'testObject', + namePlural: 'testObjects', + labelSingular: 'Test Object', + labelPlural: 'Test Objects', + isCustom: false, + isRemote: false, + isActive: true, + isSystem: false, + createdAt: new Date(), + updatedAt: new Date(), + universalIdentifier: 'object-id', + fieldMetadataIds: fieldIds, + indexMetadataIds: [], + viewIds: [], + applicationId: null, + }) as unknown as FlatObjectMetadata; + it('should return all fields as selectable when no restrictions', () => { - const objectMetadata = { - objectMetadataMapItem: { - fieldsById: { - 'field-1': { - id: 'field-1', - name: 'name', - type: FieldMetadataType.TEXT, - }, - 'field-2': { - id: 'field-2', - name: 'email', - type: FieldMetadataType.TEXT, - }, - }, - } as any as ObjectMetadataItemWithFieldMaps, - }; + const field1 = createMockField({ + id: 'field-1', + name: 'name', + type: FieldMetadataType.TEXT, + }); + const field2 = createMockField({ + id: 'field-2', + name: 'email', + type: FieldMetadataType.TEXT, + }); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps([field1, field2]); + const flatObjectMetadata = buildFlatObjectMetadata(['field-1', 'field-2']); const result = getAllSelectableFields({ restrictedFields: {}, - objectMetadata, + flatObjectMetadata, + flatFieldMetadataMaps, }); expect(result).toEqual({ @@ -34,28 +101,25 @@ describe('getAllSelectableFields', () => { }); it('should not return restricted fields', () => { - const objectMetadata = { - objectMetadataMapItem: { - fieldsById: { - 'field-1': { - id: 'field-1', - name: 'name', - type: FieldMetadataType.TEXT, - }, - 'field-2': { - id: 'field-2', - name: 'email', - type: FieldMetadataType.TEXT, - }, - }, - } as any as ObjectMetadataItemWithFieldMaps, - }; + const field1 = createMockField({ + id: 'field-1', + name: 'name', + type: FieldMetadataType.TEXT, + }); + const field2 = createMockField({ + id: 'field-2', + name: 'email', + type: FieldMetadataType.TEXT, + }); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps([field1, field2]); + const flatObjectMetadata = buildFlatObjectMetadata(['field-1', 'field-2']); const result = getAllSelectableFields({ restrictedFields: { 'field-2': { canRead: false }, }, - objectMetadata, + flatObjectMetadata, + flatFieldMetadataMaps, }); expect(result).toEqual({ @@ -64,31 +128,36 @@ describe('getAllSelectableFields', () => { }); it('should create nested objects for composite fields', () => { - const objectMetadata = { - objectMetadataMapItem: { - fieldsById: { - 'field-1': { - id: 'field-1', - name: 'name', - type: FieldMetadataType.TEXT, - }, - 'field-2': { - id: 'field-2', - name: 'fullName', - type: FieldMetadataType.FULL_NAME, - }, - 'field-3': { - id: 'field-3', - name: 'domainName', - type: FieldMetadataType.LINKS, - }, - }, - } as any as ObjectMetadataItemWithFieldMaps, - }; + const field1 = createMockField({ + id: 'field-1', + name: 'name', + type: FieldMetadataType.TEXT, + }); + const field2 = createMockField({ + id: 'field-2', + name: 'fullName', + type: FieldMetadataType.FULL_NAME, + }); + const field3 = createMockField({ + id: 'field-3', + name: 'domainName', + type: FieldMetadataType.LINKS, + }); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps([ + field1, + field2, + field3, + ]); + const flatObjectMetadata = buildFlatObjectMetadata([ + 'field-1', + 'field-2', + 'field-3', + ]); const result = getAllSelectableFields({ restrictedFields: {}, - objectMetadata, + flatObjectMetadata, + flatFieldMetadataMaps, }); expect(result).toEqual({ @@ -106,28 +175,25 @@ describe('getAllSelectableFields', () => { }); it('should restrict all sub-fields when composite field is restricted', () => { - const objectMetadata = { - objectMetadataMapItem: { - fieldsById: { - 'field-1': { - id: 'field-1', - name: 'name', - type: FieldMetadataType.TEXT, - }, - 'field-2': { - id: 'field-2', - name: 'fullName', - type: FieldMetadataType.FULL_NAME, - }, - }, - } as any as ObjectMetadataItemWithFieldMaps, - }; + const field1 = createMockField({ + id: 'field-1', + name: 'name', + type: FieldMetadataType.TEXT, + }); + const field2 = createMockField({ + id: 'field-2', + name: 'fullName', + type: FieldMetadataType.FULL_NAME, + }); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps([field1, field2]); + const flatObjectMetadata = buildFlatObjectMetadata(['field-1', 'field-2']); const result = getAllSelectableFields({ restrictedFields: { 'field-2': { canRead: false }, }, - objectMetadata, + flatObjectMetadata, + flatFieldMetadataMaps, }); expect(result).toEqual({ @@ -136,39 +202,46 @@ describe('getAllSelectableFields', () => { }); it('should handle mixed regular and composite fields with restrictions', () => { - const objectMetadata = { - objectMetadataMapItem: { - fieldsById: { - 'field-1': { - id: 'field-1', - name: 'name', - type: FieldMetadataType.TEXT, - }, - 'field-2': { - id: 'field-2', - name: 'email', - type: FieldMetadataType.TEXT, - }, - 'field-3': { - id: 'field-3', - name: 'fullName', - type: FieldMetadataType.FULL_NAME, - }, - 'field-4': { - id: 'field-4', - name: 'address', - type: FieldMetadataType.ADDRESS, - }, - }, - } as any as ObjectMetadataItemWithFieldMaps, - }; + const field1 = createMockField({ + id: 'field-1', + name: 'name', + type: FieldMetadataType.TEXT, + }); + const field2 = createMockField({ + id: 'field-2', + name: 'email', + type: FieldMetadataType.TEXT, + }); + const field3 = createMockField({ + id: 'field-3', + name: 'fullName', + type: FieldMetadataType.FULL_NAME, + }); + const field4 = createMockField({ + id: 'field-4', + name: 'address', + type: FieldMetadataType.ADDRESS, + }); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps([ + field1, + field2, + field3, + field4, + ]); + const flatObjectMetadata = buildFlatObjectMetadata([ + 'field-1', + 'field-2', + 'field-3', + 'field-4', + ]); const result = getAllSelectableFields({ restrictedFields: { 'field-2': { canRead: false }, 'field-4': { canRead: false }, }, - objectMetadata, + flatObjectMetadata, + flatFieldMetadataMaps, }); expect(result).toEqual({ diff --git a/packages/twenty-server/src/engine/api/rest/core/rest-to-common-args-handlers/utils/get-all-selectable-fields.util.ts b/packages/twenty-server/src/engine/api/rest/core/rest-to-common-args-handlers/utils/get-all-selectable-fields.util.ts index f4da0546bfc..d62d4c0570d 100644 --- a/packages/twenty-server/src/engine/api/rest/core/rest-to-common-args-handlers/utils/get-all-selectable-fields.util.ts +++ b/packages/twenty-server/src/engine/api/rest/core/rest-to-common-args-handlers/utils/get-all-selectable-fields.util.ts @@ -7,8 +7,11 @@ import { import { isDefined } from 'twenty-shared/utils'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; type SelectableFieldsStructured = Record< string, @@ -17,24 +20,29 @@ type SelectableFieldsStructured = Record< export const getAllSelectableFields = ({ restrictedFields, - objectMetadata, + flatObjectMetadata, + flatFieldMetadataMaps, }: { restrictedFields: RestrictedFieldsPermissions; - objectMetadata: { objectMetadataMapItem: ObjectMetadataItemWithFieldMaps }; + flatObjectMetadata: FlatObjectMetadata; + flatFieldMetadataMaps: FlatEntityMaps; }): SelectableFieldsStructured => { const result: SelectableFieldsStructured = {}; - const fields = Object.values(objectMetadata.objectMetadataMapItem.fieldsById); + for (const fieldId of flatObjectMetadata.fieldMetadataIds) { + const flatField = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityMaps: flatFieldMetadataMaps, + flatEntityId: fieldId, + }); - for (const field of fields) { - if (restrictedFields[field.id]?.canRead === false) continue; + if (restrictedFields[flatField.id]?.canRead === false) continue; - if (isCompositeFieldMetadataType(field.type)) { - const compositeType = compositeTypeDefinitions.get(field.type); + if (isCompositeFieldMetadataType(flatField.type)) { + const compositeType = compositeTypeDefinitions.get(flatField.type); if (!compositeType) { throw new Error( - `Composite type definition not found for type: ${field.type}`, + `Composite type definition not found for type: ${flatField.type}`, ); } @@ -44,15 +52,15 @@ export const getAllSelectableFields = ({ compositeFields[property.name] = true; } - result[field.name] = compositeFields; + result[flatField.name] = compositeFields; } else if ( - isFieldMetadataEntityOfType(field, FieldMetadataType.RELATION) && - field.settings.relationType === RelationType.MANY_TO_ONE && - isDefined(field.settings.joinColumnName) + isFlatFieldMetadataOfType(flatField, FieldMetadataType.RELATION) && + flatField.settings.relationType === RelationType.MANY_TO_ONE && + isDefined(flatField.settings.joinColumnName) ) { - result[field.settings.joinColumnName] = true; + result[flatField.settings.joinColumnName] = true; } else { - result[field.name] = true; + result[flatField.name] = true; } } diff --git a/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/__tests__/check-filter-enum-values.util.spec.ts b/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/__tests__/check-filter-enum-values.util.spec.ts deleted file mode 100644 index bc2fba7b388..00000000000 --- a/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/__tests__/check-filter-enum-values.util.spec.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { FieldMetadataType } from 'twenty-shared/types'; - -import { - fieldSelectMock, - objectMetadataItemMock, -} from 'src/engine/api/__mocks__/object-metadata-item.mock'; -import { checkFilterEnumValues } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/check-filter-enum-values.util'; -import { type FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; -import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock'; - -describe('checkFilterEnumValues', () => { - const completeFieldSelectMock = getMockFieldMetadataEntity({ - workspaceId: '20202020-0000-0000-0000-000000000000', - objectMetadataId: '20202020-0000-0000-0000-000000000001', - id: 'field-select-id', - type: fieldSelectMock.type, - name: fieldSelectMock.name, - label: 'Field Select', - isNullable: fieldSelectMock.isNullable, - defaultValue: fieldSelectMock.defaultValue, - options: fieldSelectMock.options, - isLabelSyncedWithName: true, - createdAt: new Date(), - updatedAt: new Date(), - }); - - const fieldsById: FieldMetadataMap = { - 'field-select-id': completeFieldSelectMock, - }; - - const mockObjectMetadataWithFieldMaps = { - ...objectMetadataItemMock, - fieldsById, - fieldIdByName: { - [completeFieldSelectMock.name]: completeFieldSelectMock.id, - }, - fieldIdByJoinColumnName: {}, - indexMetadatas: [], - }; - - it('should check properly', () => { - expect(() => - checkFilterEnumValues( - FieldMetadataType.SELECT, - fieldSelectMock.name, - 'OPTION_1', - mockObjectMetadataWithFieldMaps, - ), - ).not.toThrow(); - - expect(() => - checkFilterEnumValues( - FieldMetadataType.SELECT, - fieldSelectMock.name, - 'MISSING_OPTION', - mockObjectMetadataWithFieldMaps, - ), - ).toThrow( - `'filter' enum value 'MISSING_OPTION' not available in '${fieldSelectMock.name}' enum. Available enum values are ['OPTION_1', 'OPTION_2']`, - ); - }); - - it('should allow filter by NULL or NOT_NULL values', () => { - expect(() => - checkFilterEnumValues( - FieldMetadataType.SELECT, - fieldSelectMock.name, - 'NULL', - mockObjectMetadataWithFieldMaps, - ), - ).not.toThrow(); - - expect(() => - checkFilterEnumValues( - FieldMetadataType.SELECT, - fieldSelectMock.name, - 'NOT_NULL', - mockObjectMetadataWithFieldMaps, - ), - ).not.toThrow(); - }); -}); diff --git a/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/check-filter-enum-values.util.ts b/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/check-filter-enum-values.util.ts deleted file mode 100644 index 49b273214c6..00000000000 --- a/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/check-filter-enum-values.util.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { BadRequestException } from '@nestjs/common'; - -import { FieldMetadataType } from 'twenty-shared/types'; - -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; - -export const checkFilterEnumValues = ( - fieldType: FieldMetadataType | undefined, - fieldName: string, - value: string, - objectMetadataItem: ObjectMetadataItemWithFieldMaps, -): void => { - if ( - !fieldType || - ![FieldMetadataType.MULTI_SELECT, FieldMetadataType.SELECT].includes( - fieldType, - ) - ) { - return; - } - const fieldMetadataId = objectMetadataItem.fieldIdByName[fieldName]; - const field = objectMetadataItem.fieldsById[fieldMetadataId]; - - const values = /^\[.*\]$/.test(value) - ? value.slice(1, -1).split(',') - : [value]; - const enumValues = field?.options?.map((option) => option.value); - - if (!enumValues) { - return; - } - const allowedEnumValues = ['NULL', 'NOT_NULL', ...enumValues]; - - values.forEach((val) => { - if (!allowedEnumValues.includes(val)) { - throw new BadRequestException( - `'filter' enum value '${val}' not available in '${fieldName}' enum. Available enum values are ['${enumValues.join( - "', '", - )}']`, - ); - } - }); -}; diff --git a/packages/twenty-server/src/engine/api/utils/__tests__/build-duplicate-conditions.utils.spec.ts b/packages/twenty-server/src/engine/api/utils/__tests__/build-duplicate-conditions.utils.spec.ts index 042d885522c..4bdc6bb3c1d 100644 --- a/packages/twenty-server/src/engine/api/utils/__tests__/build-duplicate-conditions.utils.spec.ts +++ b/packages/twenty-server/src/engine/api/utils/__tests__/build-duplicate-conditions.utils.spec.ts @@ -1,11 +1,17 @@ -import { mockPersonObjectMetadataWithFieldMaps } from 'src/engine/api/graphql/graphql-query-runner/__mocks__/mockPersonObjectMetadata'; +import { + mockPersonFlatFieldMetadataMaps, + mockPersonFlatObjectMetadata, + mockPersonFlatObjectMetadataMaps, +} from 'src/engine/api/graphql/graphql-query-runner/__mocks__/mockPersonObjectMetadata'; import { mockPersonRecords } from 'src/engine/api/graphql/graphql-query-runner/__mocks__/mockPersonRecords'; import { buildDuplicateConditions } from 'src/engine/api/utils/build-duplicate-conditions.utils'; describe('buildDuplicateConditions', () => { it('should build conditions based on duplicate criteria from composite field', () => { const duplicateConditons = buildDuplicateConditions( - mockPersonObjectMetadataWithFieldMaps([['emailsPrimaryEmail']]), + mockPersonFlatObjectMetadata([['emailsPrimaryEmail']]), + mockPersonFlatObjectMetadataMaps([['emailsPrimaryEmail']]), + mockPersonFlatFieldMetadataMaps(), mockPersonRecords, 'recordId', ); @@ -28,7 +34,9 @@ describe('buildDuplicateConditions', () => { it('should build conditions based on duplicate criteria from basic field', () => { const duplicateConditons = buildDuplicateConditions( - mockPersonObjectMetadataWithFieldMaps([['jobTitle']]), + mockPersonFlatObjectMetadata([['jobTitle']]), + mockPersonFlatObjectMetadataMaps([['jobTitle']]), + mockPersonFlatFieldMetadataMaps(), mockPersonRecords, 'recordId', ); @@ -49,7 +57,9 @@ describe('buildDuplicateConditions', () => { it('should not build conditions based on duplicate criteria if record value is null or too small', () => { const duplicateConditons = buildDuplicateConditions( - mockPersonObjectMetadataWithFieldMaps([['linkedinLinkPrimaryLinkUrl']]), + mockPersonFlatObjectMetadata([['linkedinLinkPrimaryLinkUrl']]), + mockPersonFlatObjectMetadataMaps([['linkedinLinkPrimaryLinkUrl']]), + mockPersonFlatFieldMetadataMaps(), mockPersonRecords, 'recordId', ); @@ -59,7 +69,9 @@ describe('buildDuplicateConditions', () => { it('should build conditions based on duplicate criteria and without recordId filter', () => { const duplicateConditons = buildDuplicateConditions( - mockPersonObjectMetadataWithFieldMaps([['jobTitle']]), + mockPersonFlatObjectMetadata([['jobTitle']]), + mockPersonFlatObjectMetadataMaps([['jobTitle']]), + mockPersonFlatFieldMetadataMaps(), mockPersonRecords, ); diff --git a/packages/twenty-server/src/engine/api/utils/__tests__/compute-cursor-arg-filter.utils.spec.ts b/packages/twenty-server/src/engine/api/utils/__tests__/compute-cursor-arg-filter.utils.spec.ts index 773491cac57..998a43baa5d 100644 --- a/packages/twenty-server/src/engine/api/utils/__tests__/compute-cursor-arg-filter.utils.spec.ts +++ b/packages/twenty-server/src/engine/api/utils/__tests__/compute-cursor-arg-filter.utils.spec.ts @@ -2,79 +2,119 @@ import { FieldMetadataType, OrderByDirection } from 'twenty-shared/types'; import { GraphqlQueryRunnerException } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; import { computeCursorArgFilter } from 'src/engine/api/utils/compute-cursor-arg-filter.utils'; -import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock'; -import { getMockObjectMetadataItemWithFieldsMaps } from 'src/utils/__test__/get-object-metadata-item-with-fields-maps.mock'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; describe('computeCursorArgFilter', () => { - const objectMetadataItemWithFieldMaps = - getMockObjectMetadataItemWithFieldsMaps({ - id: 'object-id', - workspaceId: 'workspace-id', - nameSingular: 'person', - namePlural: 'people', - isCustom: false, - isRemote: false, - labelSingular: 'Person', - labelPlural: 'People', - targetTableName: 'person', - indexMetadatas: [], - isSystem: false, - isActive: true, - isAuditLogged: false, - isSearchable: false, - fieldIdByJoinColumnName: {}, - icon: 'Icon123', - fieldIdByName: { - name: 'name-id', - age: 'age-id', - fullName: 'fullname-id', + const workspaceId = 'workspace-id'; + const objectMetadataId = 'object-id'; + + const createMockField = ( + overrides: Partial & { + id: string; + name: string; + type: FieldMetadataType; + }, + ): FlatFieldMetadata => + ({ + workspaceId, + objectMetadataId, + isNullable: true, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), + universalIdentifier: overrides.id, + viewFieldIds: [], + viewFilterIds: [], + viewGroupIds: [], + kanbanAggregateOperationViewIds: [], + calendarViewIds: [], + applicationId: null, + label: overrides.name, + ...overrides, + }) as FlatFieldMetadata; + + const nameField = createMockField({ + id: 'name-id', + type: FieldMetadataType.TEXT, + name: 'name', + label: 'Name', + }); + + const ageField = createMockField({ + id: 'age-id', + type: FieldMetadataType.NUMBER, + name: 'age', + label: 'Age', + }); + + const fullNameField = createMockField({ + id: 'fullname-id', + type: FieldMetadataType.FULL_NAME, + name: 'fullName', + label: 'Full Name', + }); + + const buildFlatFieldMetadataMaps = ( + fields: FlatFieldMetadata[], + ): FlatEntityMaps => ({ + byId: fields.reduce( + (acc, field) => { + acc[field.id] = field; + + return acc; }, - fieldsById: { - 'name-id': getMockFieldMetadataEntity({ - workspaceId: 'workspace-id', - objectMetadataId: 'object-id', - id: 'name-id', - type: FieldMetadataType.TEXT, - name: 'name', - label: 'Name', - isLabelSyncedWithName: true, - isNullable: true, - createdAt: new Date(), - updatedAt: new Date(), - }), - 'age-id': getMockFieldMetadataEntity({ - workspaceId: 'workspace-id', - objectMetadataId: 'object-id', - id: 'age-id', - type: FieldMetadataType.NUMBER, - name: 'age', - label: 'Age', - isLabelSyncedWithName: true, - isNullable: true, - createdAt: new Date(), - updatedAt: new Date(), - }), - 'fullname-id': getMockFieldMetadataEntity({ - workspaceId: 'workspace-id', - objectMetadataId: 'object-id', - id: 'fullname-id', - type: FieldMetadataType.FULL_NAME, - name: 'fullName', - label: 'Full Name', - isLabelSyncedWithName: true, - isNullable: true, - createdAt: new Date(), - updatedAt: new Date(), - }), + {} as Record, + ), + idByUniversalIdentifier: fields.reduce( + (acc, field) => { + acc[field.universalIdentifier] = field.id; + + return acc; }, - }); + {} as Record, + ), + universalIdentifiersByApplicationId: {}, + }); + + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps([ + nameField, + ageField, + fullNameField, + ]); + + const flatObjectMetadata: FlatObjectMetadata = { + id: objectMetadataId, + workspaceId, + nameSingular: 'person', + namePlural: 'people', + labelSingular: 'Person', + labelPlural: 'People', + targetTableName: 'person', + isCustom: false, + isRemote: false, + isActive: true, + isSystem: false, + isAuditLogged: false, + isSearchable: false, + icon: 'Icon123', + createdAt: new Date(), + updatedAt: new Date(), + universalIdentifier: objectMetadataId, + fieldMetadataIds: ['name-id', 'age-id', 'fullname-id'], + indexMetadataIds: [], + viewIds: [], + applicationId: null, + } as unknown as FlatObjectMetadata; describe('basic cursor filtering', () => { it('should return empty array when cursor is empty', () => { const result = computeCursorArgFilter( {}, [], - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, true, ); @@ -88,7 +128,8 @@ describe('computeCursorArgFilter', () => { const result = computeCursorArgFilter( cursor, orderBy, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, true, ); @@ -102,7 +143,8 @@ describe('computeCursorArgFilter', () => { const result = computeCursorArgFilter( cursor, orderBy, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, false, ); @@ -121,7 +163,8 @@ describe('computeCursorArgFilter', () => { const result = computeCursorArgFilter( cursor, orderBy, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, true, ); @@ -149,7 +192,8 @@ describe('computeCursorArgFilter', () => { const result = computeCursorArgFilter( cursor, orderBy, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, true, ); @@ -195,7 +239,8 @@ describe('computeCursorArgFilter', () => { const result = computeCursorArgFilter( cursor, orderBy, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, true, ); @@ -224,7 +269,8 @@ describe('computeCursorArgFilter', () => { const result = computeCursorArgFilter( cursor, orderBy, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, false, ); @@ -265,7 +311,8 @@ describe('computeCursorArgFilter', () => { computeCursorArgFilter( cursor, orderBy, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, true, ), ).toThrow(GraphqlQueryRunnerException); @@ -279,7 +326,8 @@ describe('computeCursorArgFilter', () => { computeCursorArgFilter( cursor, orderBy, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, true, ), ).toThrow(GraphqlQueryRunnerException); diff --git a/packages/twenty-server/src/engine/api/utils/build-cursor-where-condition.utils.ts b/packages/twenty-server/src/engine/api/utils/build-cursor-where-condition.utils.ts index cadf79f2820..5772ecde9bc 100644 --- a/packages/twenty-server/src/engine/api/utils/build-cursor-where-condition.utils.ts +++ b/packages/twenty-server/src/engine/api/utils/build-cursor-where-condition.utils.ts @@ -16,14 +16,19 @@ import { computeOperator } from 'src/engine/api/utils/compute-operator.utils'; import { isAscendingOrder } from 'src/engine/api/utils/is-ascending-order.utils'; import { validateAndGetOrderByForScalarField } from 'src/engine/api/utils/validate-and-get-order-by.utils'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; type BuildCursorWhereConditionParams = { cursorKey: keyof ObjectRecord; cursorValue: | ObjectRecordCursorLeafScalarValue | ObjectRecordCursorLeafCompositeValue; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; + flatObjectMetadata: FlatObjectMetadata; + flatFieldMetadataMaps: FlatEntityMaps; orderBy: ObjectRecordOrderBy; isForwardPagination: boolean; isEqualityCondition?: boolean; @@ -32,15 +37,23 @@ type BuildCursorWhereConditionParams = { export const buildCursorWhereCondition = ({ cursorKey, cursorValue, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, orderBy, isForwardPagination, isEqualityCondition = false, }: BuildCursorWhereConditionParams): Record => { - const fieldMetadataId = - objectMetadataItemWithFieldMaps.fieldIdByName[cursorKey]; - const fieldMetadata = - objectMetadataItemWithFieldMaps.fieldsById[fieldMetadataId]; + const { fieldIdByName } = buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + flatObjectMetadata, + ); + + const fieldMetadataId = fieldIdByName[cursorKey]; + + const fieldMetadata = findFlatEntityByIdInFlatEntityMaps({ + flatEntityMaps: flatFieldMetadataMaps, + flatEntityId: fieldMetadataId, + }); if (!fieldMetadata) { throw new GraphqlQueryRunnerException( diff --git a/packages/twenty-server/src/engine/api/utils/build-duplicate-conditions.utils.ts b/packages/twenty-server/src/engine/api/utils/build-duplicate-conditions.utils.ts index b5f154c52c7..5ab8eb72ccc 100644 --- a/packages/twenty-server/src/engine/api/utils/build-duplicate-conditions.utils.ts +++ b/packages/twenty-server/src/engine/api/utils/build-duplicate-conditions.utils.ts @@ -4,12 +4,16 @@ import { type ObjectRecord } from 'twenty-shared/types'; import { type ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; import { settings } from 'src/engine/constants/settings'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { formatData } from 'src/engine/twenty-orm/utils/format-data.util'; import { getCompositeFieldMetadataMap } from 'src/engine/twenty-orm/utils/format-result.util'; export const buildDuplicateConditions = ( - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, records?: Partial[] | undefined, filteringByExistingRecordId?: string, ): Partial => { @@ -17,13 +21,17 @@ export const buildDuplicateConditions = ( return {}; } - const criteriaCollection = - objectMetadataItemWithFieldMaps.duplicateCriteria || []; + const criteriaCollection = flatObjectMetadata.duplicateCriteria || []; - const formattedRecords = formatData(records, objectMetadataItemWithFieldMaps); + const formattedRecords = formatData( + records, + flatObjectMetadata, + flatFieldMetadataMaps, + ); const compositeFieldMetadataMap = getCompositeFieldMetadataMap( - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ); const conditions = formattedRecords.flatMap((record) => { diff --git a/packages/twenty-server/src/engine/api/utils/compute-cursor-arg-filter.utils.ts b/packages/twenty-server/src/engine/api/utils/compute-cursor-arg-filter.utils.ts index a8196032f0e..04289618e6d 100644 --- a/packages/twenty-server/src/engine/api/utils/compute-cursor-arg-filter.utils.ts +++ b/packages/twenty-server/src/engine/api/utils/compute-cursor-arg-filter.utils.ts @@ -10,12 +10,15 @@ import { import { buildCursorCumulativeWhereCondition } from 'src/engine/api/utils/build-cursor-cumulative-where-conditions.utils'; import { buildCursorWhereCondition } from 'src/engine/api/utils/build-cursor-where-condition.utils'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; export const computeCursorArgFilter = ( cursor: ObjectRecordCursor, orderBy: ObjectRecordOrderBy, - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, isForwardPagination = true, ): ObjectRecordFilter[] => { const cursorEntries = Object.entries(cursor) @@ -42,7 +45,8 @@ export const computeCursorArgFilter = ( buildCursorWhereCondition({ cursorKey, cursorValue, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, orderBy, isForwardPagination: true, isEqualityCondition: true, @@ -51,7 +55,8 @@ export const computeCursorArgFilter = ( buildCursorWhereCondition({ cursorKey, cursorValue, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, orderBy, isForwardPagination, }), diff --git a/packages/twenty-server/src/engine/api/utils/get-all-selectable-column-names.utils.ts b/packages/twenty-server/src/engine/api/utils/get-all-selectable-column-names.utils.ts index c3512ac6879..acfc535ddb0 100644 --- a/packages/twenty-server/src/engine/api/utils/get-all-selectable-column-names.utils.ts +++ b/packages/twenty-server/src/engine/api/utils/get-all-selectable-column-names.utils.ts @@ -1,12 +1,17 @@ import { type RestrictedFieldsPermissions } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { getFieldMetadataIdToColumnNamesMap } from 'src/engine/twenty-orm/utils/get-field-metadata-id-to-column-names-map.util'; type GetAllSelectableFieldsArgs = { restrictedFields: RestrictedFieldsPermissions; - objectMetadata: { objectMetadataMapItem: ObjectMetadataItemWithFieldMaps }; + objectMetadata: { + objectMetadataMapItem: FlatObjectMetadata; + flatFieldMetadataMaps: FlatEntityMaps; + }; }; export const getAllSelectableColumnNames = ({ @@ -19,6 +24,7 @@ export const getAllSelectableColumnNames = ({ const fieldMetadataIdToColumnNamesMap = getFieldMetadataIdToColumnNamesMap( objectMetadata.objectMetadataMapItem, + objectMetadata.flatFieldMetadataMaps, ); const restrictedFieldsColumnNames: string[] = restrictedFieldsIds diff --git a/packages/twenty-server/src/engine/core-modules/__mocks__/mockFlatObjectMetadatas.ts b/packages/twenty-server/src/engine/core-modules/__mocks__/mockFlatObjectMetadatas.ts new file mode 100644 index 00000000000..69a0a10493d --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/__mocks__/mockFlatObjectMetadatas.ts @@ -0,0 +1,168 @@ +import { FieldMetadataType } from 'twenty-shared/types'; + +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { getFlatFieldMetadataMock } from 'src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { getFlatObjectMetadataMock } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/get-flat-object-metadata.mock'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; + +const workspaceId = '20202020-0000-0000-0000-000000000000'; + +const personNameFieldId = 'person-name-field-id'; +const companyNameFieldId = 'company-name-field-id'; +const companyDomainNameFieldId = 'company-domain-name-field-id'; +const customObjectNameFieldId = 'custom-object-name-field-id'; +const customObjectImageFieldId = 'custom-object-image-field-id'; + +export const mockFlatObjectMetadatas: FlatObjectMetadata[] = [ + getFlatObjectMetadataMock({ + id: '20202020-8dec-43d5-b2ff-6eef05095bec', + nameSingular: 'person', + namePlural: 'people', + labelSingular: 'Person', + labelPlural: 'People', + description: 'A person', + icon: 'test-person-icon', + isCustom: false, + isSearchable: true, + labelIdentifierFieldMetadataId: personNameFieldId, + imageIdentifierFieldMetadataId: null, + workspaceId, + fieldMetadataIds: [personNameFieldId], + universalIdentifier: 'person-universal-id', + applicationId: workspaceId, + }), + getFlatObjectMetadataMock({ + id: '20202020-c03c-45d6-a4b0-04afe1357c5c', + nameSingular: 'company', + namePlural: 'companies', + labelSingular: 'Company', + labelPlural: 'Companies', + description: 'A company', + icon: 'test-company-icon', + isCustom: false, + isSearchable: true, + labelIdentifierFieldMetadataId: companyNameFieldId, + imageIdentifierFieldMetadataId: null, + workspaceId, + fieldMetadataIds: [companyNameFieldId, companyDomainNameFieldId], + universalIdentifier: 'company-universal-id', + applicationId: workspaceId, + }), + getFlatObjectMetadataMock({ + id: '20202020-3d75-4aab-bacd-ee176c5f63ca', + nameSingular: 'regular-custom-object', + namePlural: 'regular-custom-objects', + labelSingular: 'Regular Custom Object', + labelPlural: 'Regular Custom Objects', + description: 'A regular custom object', + icon: 'test-regular-custom-object-icon', + isCustom: true, + isSearchable: true, + labelIdentifierFieldMetadataId: customObjectNameFieldId, + imageIdentifierFieldMetadataId: customObjectImageFieldId, + workspaceId, + fieldMetadataIds: [customObjectNameFieldId, customObjectImageFieldId], + universalIdentifier: 'custom-object-universal-id', + applicationId: workspaceId, + }), + getFlatObjectMetadataMock({ + id: '20202020-540c-4397-b872-2a90ea2fb809', + nameSingular: 'non-searchable-object', + namePlural: 'non-searchable-objects', + labelSingular: '', + labelPlural: '', + description: '', + icon: 'test-non-searchable-object-icon', + isCustom: false, + isSystem: true, + isSearchable: false, + labelIdentifierFieldMetadataId: null, + imageIdentifierFieldMetadataId: null, + workspaceId, + fieldMetadataIds: [], + universalIdentifier: 'non-searchable-object-universal-id', + applicationId: workspaceId, + }), +]; + +export const mockFlatFieldMetadataMaps: FlatEntityMaps = { + byId: { + [personNameFieldId]: getFlatFieldMetadataMock({ + id: personNameFieldId, + type: FieldMetadataType.FULL_NAME, + icon: 'test-field-icon', + name: 'name', + label: 'Name', + description: "Contact's name", + isCustom: false, + defaultValue: { lastName: "''", firstName: "''" }, + objectMetadataId: '20202020-8dec-43d5-b2ff-6eef05095bec', + workspaceId, + universalIdentifier: 'person-name-field-universal-id', + applicationId: workspaceId, + }), + [companyNameFieldId]: getFlatFieldMetadataMock({ + id: companyNameFieldId, + type: FieldMetadataType.TEXT, + icon: 'test-field-icon', + name: 'name', + label: 'Name', + description: null, + isCustom: false, + defaultValue: '', + objectMetadataId: '20202020-c03c-45d6-a4b0-04afe1357c5c', + workspaceId, + universalIdentifier: 'company-name-field-universal-id', + applicationId: workspaceId, + }), + [companyDomainNameFieldId]: getFlatFieldMetadataMock({ + id: companyDomainNameFieldId, + type: FieldMetadataType.LINKS, + icon: 'test-field-icon', + name: 'domainName', + label: 'Domain Name', + description: null, + isCustom: false, + defaultValue: { + primaryLinkLabel: '', + primaryLinkUrl: '', + secondaryLinks: [], + }, + objectMetadataId: '20202020-c03c-45d6-a4b0-04afe1357c5c', + workspaceId, + universalIdentifier: 'company-domain-name-field-universal-id', + applicationId: workspaceId, + }), + [customObjectNameFieldId]: getFlatFieldMetadataMock({ + id: customObjectNameFieldId, + type: FieldMetadataType.TEXT, + icon: 'test-field-icon', + name: 'name', + label: 'Name', + description: null, + isCustom: false, + defaultValue: '', + objectMetadataId: '20202020-3d75-4aab-bacd-ee176c5f63ca', + workspaceId, + universalIdentifier: 'custom-object-name-field-universal-id', + applicationId: workspaceId, + }), + [customObjectImageFieldId]: getFlatFieldMetadataMock({ + id: customObjectImageFieldId, + type: FieldMetadataType.TEXT, + icon: 'test-field-icon', + name: 'imageIdentifierFieldName', + label: 'Image Identifier Field Name', + description: null, + isCustom: false, + defaultValue: '', + objectMetadataId: '20202020-3d75-4aab-bacd-ee176c5f63ca', + workspaceId, + universalIdentifier: 'custom-object-image-field-universal-id', + applicationId: workspaceId, + }), + }, + idByUniversalIdentifier: {}, + universalIdentifiersByApplicationId: {}, +}; diff --git a/packages/twenty-server/src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps.ts b/packages/twenty-server/src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps.ts index 297b2a8e828..43d4dfed135 100644 --- a/packages/twenty-server/src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps.ts +++ b/packages/twenty-server/src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps.ts @@ -1,204 +1,290 @@ import { FieldMetadataType } from 'twenty-shared/types'; -import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock'; -import { getMockObjectMetadataItemWithFieldsMaps } from 'src/utils/__test__/get-object-metadata-item-with-fields-maps.mock'; +import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { getFlatFieldMetadataMock } from 'src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; +import { getFlatObjectMetadataMock } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/get-flat-object-metadata.mock'; +import { type ObjectMetadataInfo } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; const workspaceId = '20202020-0000-0000-0000-000000000000'; -export const mockObjectMetadataItemsWithFieldMaps = [ - getMockObjectMetadataItemWithFieldsMaps({ - id: '20202020-8dec-43d5-b2ff-6eef05095bec', - standardId: '', - nameSingular: 'person', - namePlural: 'people', - labelSingular: 'Person', - labelPlural: 'People', - description: 'A person', - icon: 'test-person-icon', - targetTableName: 'DEPRECATED', - isCustom: false, - isRemote: false, - isActive: true, - isSystem: false, - isAuditLogged: true, - isSearchable: true, - labelIdentifierFieldMetadataId: 'nameFieldMetadataId', - imageIdentifierFieldMetadataId: '', - workspaceId, - indexMetadatas: [], - fieldsById: { - nameFieldMetadataId: getMockFieldMetadataEntity({ - workspaceId, - objectMetadataId: '20202020-8dec-43d5-b2ff-6eef05095bec', - id: 'nameFieldMetadataId', - type: FieldMetadataType.FULL_NAME, - icon: 'test-field-icon', - name: 'name', - label: 'Name', - defaultValue: { - lastName: "''", - firstName: "''", - }, - description: "Contact's name", - isCustom: false, - isNullable: true, - isUnique: false, - isLabelSyncedWithName: true, - createdAt: new Date(), - updatedAt: new Date(), - }), - }, - fieldIdByName: { - name: 'nameFieldMetadataId', - }, - fieldIdByJoinColumnName: {}, - }), - getMockObjectMetadataItemWithFieldsMaps({ - id: '20202020-c03c-45d6-a4b0-04afe1357c5c', - standardId: '', - nameSingular: 'company', - namePlural: 'companies', - labelSingular: 'Company', - labelPlural: 'Companies', - description: 'A company', - icon: 'test-company-icon', - targetTableName: 'DEPRECATED', - isCustom: false, - isRemote: false, - isActive: true, - isSystem: false, - isAuditLogged: true, - isSearchable: true, - labelIdentifierFieldMetadataId: 'nameFieldMetadataId', - imageIdentifierFieldMetadataId: '', - workspaceId, - indexMetadatas: [], - fieldsById: { - nameFieldMetadataId: getMockFieldMetadataEntity({ - workspaceId, - objectMetadataId: '20202020-c03c-45d6-a4b0-04afe1357c5c', - id: 'nameFieldMetadataId', - type: FieldMetadataType.TEXT, - icon: 'test-field-icon', - name: 'name', - label: 'Name', - defaultValue: '', - isCustom: false, - isNullable: true, - isUnique: false, - isLabelSyncedWithName: true, - createdAt: new Date(), - updatedAt: new Date(), - }), - domainNameFieldMetadataId: getMockFieldMetadataEntity({ - workspaceId, - objectMetadataId: '20202020-c03c-45d6-a4b0-04afe1357c5c', - id: 'domainNameFieldMetadataId', - type: FieldMetadataType.LINKS, - icon: 'test-field-icon', - name: 'domainName', - label: 'Domain Name', - defaultValue: { - primaryLinkLabel: '', - primaryLinkUrl: '', - secondaryLinks: [], - }, - isCustom: false, - isNullable: true, - isUnique: false, - isLabelSyncedWithName: true, - createdAt: new Date(), - updatedAt: new Date(), - }), - }, - fieldIdByName: { - name: 'nameFieldMetadataId', - domainName: 'domainNameFieldMetadataId', - }, - fieldIdByJoinColumnName: {}, - }), - getMockObjectMetadataItemWithFieldsMaps({ - id: '20202020-3d75-4aab-bacd-ee176c5f63ca', - standardId: '', - nameSingular: 'regular-custom-object', - namePlural: 'regular-custom-objects', - labelSingular: 'Regular Custom Object', - labelPlural: 'Regular Custom Objects', - description: 'A regular custom object', - icon: 'test-regular-custom-object-icon', - targetTableName: 'DEPRECATED', - isCustom: true, - isRemote: false, - isActive: true, - isSystem: false, - isAuditLogged: true, - isSearchable: true, - labelIdentifierFieldMetadataId: 'nameFieldMetadataId', - imageIdentifierFieldMetadataId: 'imageIdentifierFieldMetadataId', - workspaceId, - indexMetadatas: [], - fieldsById: { - nameFieldMetadataId: getMockFieldMetadataEntity({ - workspaceId, - objectMetadataId: '20202020-3d75-4aab-bacd-ee176c5f63ca', - id: 'nameFieldMetadataId', - type: FieldMetadataType.TEXT, - icon: 'test-field-icon', - name: 'name', - label: 'Name', - defaultValue: '', - isCustom: false, - isNullable: true, - isUnique: false, - isLabelSyncedWithName: true, - createdAt: new Date(), - updatedAt: new Date(), - }), - imageIdentifierFieldMetadataId: getMockFieldMetadataEntity({ - workspaceId, - objectMetadataId: '20202020-3d75-4aab-bacd-ee176c5f63ca', - id: 'imageIdentifierFieldMetadataId', - type: FieldMetadataType.TEXT, - icon: 'test-field-icon', - name: 'imageIdentifierFieldName', - label: 'Image Identifier Field Name', - defaultValue: '', - isCustom: false, - isNullable: true, - isUnique: false, - isLabelSyncedWithName: true, - createdAt: new Date(), - updatedAt: new Date(), - }), - }, - fieldIdByName: { - name: 'nameFieldMetadataId', - imageIdentifierFieldName: 'imageIdentifierFieldMetadataId', - }, - fieldIdByJoinColumnName: {}, - }), - getMockObjectMetadataItemWithFieldsMaps({ - id: '20202020-540c-4397-b872-2a90ea2fb809', - standardId: '', - nameSingular: 'non-searchable-object', - namePlural: 'non-searchable-objects', - labelSingular: '', - labelPlural: '', - description: '', - icon: 'test-non-searchable-object-icon', - targetTableName: 'DEPRECATED', - isCustom: false, - isRemote: false, - isActive: true, - isSystem: true, - isAuditLogged: true, - isSearchable: false, - labelIdentifierFieldMetadataId: '', - imageIdentifierFieldMetadataId: '', - workspaceId, - indexMetadatas: [], - fieldsById: {}, - fieldIdByName: {}, - fieldIdByJoinColumnName: {}, - }), +const personNameFieldId = 'nameFieldMetadataId-person'; +const companyNameFieldId = 'nameFieldMetadataId-company'; +const companyDomainNameFieldId = 'domainNameFieldMetadataId'; +const customObjectNameFieldId = 'nameFieldMetadataId-custom'; +const customObjectImageFieldId = 'imageIdentifierFieldMetadataId'; + +const personFlatObject = getFlatObjectMetadataMock({ + id: '20202020-8dec-43d5-b2ff-6eef05095bec', + standardId: '20202020-8dec-43d5-b2ff-6eef05095bec', + nameSingular: 'person', + namePlural: 'people', + labelSingular: 'Person', + labelPlural: 'People', + description: 'A person', + icon: 'test-person-icon', + targetTableName: 'DEPRECATED', + isCustom: false, + isRemote: false, + isActive: true, + isSystem: false, + isAuditLogged: true, + isSearchable: true, + labelIdentifierFieldMetadataId: personNameFieldId, + imageIdentifierFieldMetadataId: null, + workspaceId, + universalIdentifier: '20202020-8dec-43d5-b2ff-6eef05095bec', + fieldMetadataIds: [personNameFieldId], + indexMetadataIds: [], + viewIds: [], +}); + +const companyFlatObject = getFlatObjectMetadataMock({ + id: '20202020-c03c-45d6-a4b0-04afe1357c5c', + standardId: '20202020-c03c-45d6-a4b0-04afe1357c5c', + nameSingular: 'company', + namePlural: 'companies', + labelSingular: 'Company', + labelPlural: 'Companies', + description: 'A company', + icon: 'test-company-icon', + targetTableName: 'DEPRECATED', + isCustom: false, + isRemote: false, + isActive: true, + isSystem: false, + isAuditLogged: true, + isSearchable: true, + labelIdentifierFieldMetadataId: companyNameFieldId, + imageIdentifierFieldMetadataId: null, + workspaceId, + universalIdentifier: '20202020-c03c-45d6-a4b0-04afe1357c5c', + fieldMetadataIds: [companyNameFieldId, companyDomainNameFieldId], + indexMetadataIds: [], + viewIds: [], +}); + +const customObjectFlatObject = getFlatObjectMetadataMock({ + id: '20202020-3d75-4aab-bacd-ee176c5f63ca', + standardId: null, + nameSingular: 'regular-custom-object', + namePlural: 'regular-custom-objects', + labelSingular: 'Regular Custom Object', + labelPlural: 'Regular Custom Objects', + description: 'A regular custom object', + icon: 'test-regular-custom-object-icon', + targetTableName: 'DEPRECATED', + isCustom: true, + isRemote: false, + isActive: true, + isSystem: false, + isAuditLogged: true, + isSearchable: true, + labelIdentifierFieldMetadataId: customObjectNameFieldId, + imageIdentifierFieldMetadataId: customObjectImageFieldId, + workspaceId, + universalIdentifier: '20202020-3d75-4aab-bacd-ee176c5f63ca', + fieldMetadataIds: [customObjectNameFieldId, customObjectImageFieldId], + indexMetadataIds: [], + viewIds: [], +}); + +const nonSearchableFlatObject = getFlatObjectMetadataMock({ + id: '20202020-540c-4397-b872-2a90ea2fb809', + standardId: '20202020-540c-4397-b872-2a90ea2fb809', + nameSingular: 'non-searchable-object', + namePlural: 'non-searchable-objects', + labelSingular: '', + labelPlural: '', + description: '', + icon: 'test-non-searchable-object-icon', + targetTableName: 'DEPRECATED', + isCustom: false, + isRemote: false, + isActive: true, + isSystem: true, + isAuditLogged: true, + isSearchable: false, + labelIdentifierFieldMetadataId: null, + imageIdentifierFieldMetadataId: null, + workspaceId, + universalIdentifier: '20202020-540c-4397-b872-2a90ea2fb809', + fieldMetadataIds: [], + indexMetadataIds: [], + viewIds: [], +}); + +const personNameField = getFlatFieldMetadataMock({ + id: personNameFieldId, + objectMetadataId: personFlatObject.id, + type: FieldMetadataType.FULL_NAME, + icon: 'test-field-icon', + name: 'name', + label: 'Name', + defaultValue: { + lastName: "''", + firstName: "''", + }, + description: "Contact's name", + isCustom: false, + isNullable: true, + isUnique: false, + isLabelSyncedWithName: true, + universalIdentifier: personNameFieldId, + workspaceId, +}); + +const companyNameField = getFlatFieldMetadataMock({ + id: companyNameFieldId, + objectMetadataId: companyFlatObject.id, + type: FieldMetadataType.TEXT, + icon: 'test-field-icon', + name: 'name', + label: 'Name', + defaultValue: '', + isCustom: false, + isNullable: true, + isUnique: false, + isLabelSyncedWithName: true, + universalIdentifier: companyNameFieldId, + workspaceId, +}); + +const companyDomainNameField = getFlatFieldMetadataMock({ + id: companyDomainNameFieldId, + objectMetadataId: companyFlatObject.id, + type: FieldMetadataType.LINKS, + icon: 'test-field-icon', + name: 'domainName', + label: 'Domain Name', + defaultValue: { + primaryLinkLabel: '', + primaryLinkUrl: '', + secondaryLinks: [], + }, + isCustom: false, + isNullable: true, + isUnique: false, + isLabelSyncedWithName: true, + universalIdentifier: companyDomainNameFieldId, + workspaceId, +}); + +const customObjectNameField = getFlatFieldMetadataMock({ + id: customObjectNameFieldId, + objectMetadataId: customObjectFlatObject.id, + type: FieldMetadataType.TEXT, + icon: 'test-field-icon', + name: 'name', + label: 'Name', + defaultValue: '', + isCustom: false, + isNullable: true, + isUnique: false, + isLabelSyncedWithName: true, + universalIdentifier: customObjectNameFieldId, + workspaceId, +}); + +const customObjectImageField = getFlatFieldMetadataMock({ + id: customObjectImageFieldId, + objectMetadataId: customObjectFlatObject.id, + type: FieldMetadataType.TEXT, + icon: 'test-field-icon', + name: 'imageIdentifierFieldName', + label: 'Image Identifier Field Name', + defaultValue: '', + isCustom: false, + isNullable: true, + isUnique: false, + isLabelSyncedWithName: true, + universalIdentifier: customObjectImageFieldId, + workspaceId, +}); + +const ALL_FLAT_OBJECTS = [ + personFlatObject, + companyFlatObject, + customObjectFlatObject, + nonSearchableFlatObject, ]; + +const ALL_FLAT_FIELDS = [ + personNameField, + companyNameField, + companyDomainNameField, + customObjectNameField, + customObjectImageField, +]; + +export const mockFlatObjectMetadataMaps: FlatEntityMaps = + ALL_FLAT_OBJECTS.reduce( + (acc, object) => ({ + ...acc, + byId: { + ...acc.byId, + [object.id]: object, + }, + idByUniversalIdentifier: { + ...acc.idByUniversalIdentifier, + [object.universalIdentifier]: object.id, + }, + }), + createEmptyFlatEntityMaps() as FlatEntityMaps, + ); + +export const mockFlatFieldMetadataMaps: FlatEntityMaps = + ALL_FLAT_FIELDS.reduce( + (acc, field) => ({ + ...acc, + byId: { + ...acc.byId, + [field.id]: field, + }, + idByUniversalIdentifier: { + ...acc.idByUniversalIdentifier, + [field.universalIdentifier]: field.id, + }, + }), + createEmptyFlatEntityMaps() as FlatEntityMaps, + ); + +export const mockObjectIdByNameSingular: Record = + ALL_FLAT_OBJECTS.reduce( + (acc, object) => ({ + ...acc, + [object.nameSingular]: object.id, + }), + {} as Record, + ); + +export const getMockObjectMetadataInfo = ( + nameSingular: string, +): ObjectMetadataInfo => { + const objectId = mockObjectIdByNameSingular[nameSingular]; + const flatObjectMetadata = mockFlatObjectMetadataMaps.byId[objectId]; + + if (!flatObjectMetadata) { + throw new Error( + `Object metadata not found for nameSingular: ${nameSingular}`, + ); + } + + return { + flatObjectMetadata, + flatObjectMetadataMaps: mockFlatObjectMetadataMaps, + flatFieldMetadataMaps: mockFlatFieldMetadataMaps, + }; +}; + +export const mockPersonObjectMetadataInfo = getMockObjectMetadataInfo('person'); +export const mockCompanyObjectMetadataInfo = + getMockObjectMetadataInfo('company'); +export const mockCustomObjectMetadataInfo = getMockObjectMetadataInfo( + 'regular-custom-object', +); +export const mockNonSearchableObjectMetadataInfo = getMockObjectMetadataInfo( + 'non-searchable-object', +); diff --git a/packages/twenty-server/src/engine/core-modules/actor/services/created-by-from-auth-context.service.ts b/packages/twenty-server/src/engine/core-modules/actor/services/created-by-from-auth-context.service.ts index 5da432f910b..fd7b66031b6 100644 --- a/packages/twenty-server/src/engine/core-modules/actor/services/created-by-from-auth-context.service.ts +++ b/packages/twenty-server/src/engine/core-modules/actor/services/created-by-from-auth-context.service.ts @@ -8,7 +8,7 @@ import { buildCreatedByFromFullNameMetadata } from 'src/engine/core-modules/acto import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; -import { buildFieldMapsForObject } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-for-object.util'; +import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; @@ -55,8 +55,10 @@ export class CreatedByFromAuthContextService { : undefined; const fieldIdByName = objectMetadata - ? buildFieldMapsForObject(flatFieldMetadataMaps, objectMetadata.id) - .fieldIdByName + ? buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + objectMetadata, + ).fieldIdByName : {}; this.logger.log( diff --git a/packages/twenty-server/src/engine/core-modules/event-emitter/utils/__tests__/object-record-changed-values.spec.ts b/packages/twenty-server/src/engine/core-modules/event-emitter/utils/__tests__/object-record-changed-values.spec.ts index 5eb7d1e271e..285c0515280 100644 --- a/packages/twenty-server/src/engine/core-modules/event-emitter/utils/__tests__/object-record-changed-values.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/event-emitter/utils/__tests__/object-record-changed-values.spec.ts @@ -1,27 +1,46 @@ +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { objectRecordChangedValues } from 'src/engine/core-modules/event-emitter/utils/object-record-changed-values'; -import { getMockObjectMetadataItemWithFieldsMaps } from 'src/utils/__test__/get-object-metadata-item-with-fields-maps.mock'; -const mockObjectMetadata = getMockObjectMetadataItemWithFieldsMaps({ +const mockObjectMetadata = { id: '1', icon: 'Icon123', nameSingular: 'Object', namePlural: 'Objects', labelSingular: 'Object', labelPlural: 'Objects', - description: 'Test object metadata', targetTableName: 'test_table', workspaceId: '1', - fieldsById: {}, - fieldIdByName: {}, + universalIdentifier: '1', isSystem: false, isCustom: false, isActive: true, isRemote: false, isAuditLogged: true, isSearchable: true, - indexMetadatas: [], - fieldIdByJoinColumnName: {}, -}); + indexMetadataIds: [], + fieldMetadataIds: [], + viewIds: [], + applicationId: null, + isLabelSyncedWithName: false, + createdAt: new Date(), + updatedAt: new Date(), + shortcut: null, + description: null, + standardOverrides: null, + isUIReadOnly: false, + standardId: null, + labelIdentifierFieldMetadataId: null, + imageIdentifierFieldMetadataId: null, + duplicateCriteria: null, +} as FlatObjectMetadata; + +const mockFlatFieldMetadataMaps: FlatEntityMaps = { + byId: {}, + idByUniversalIdentifier: {}, + universalIdentifiersByApplicationId: {}, +}; describe('objectRecordChangedValues', () => { it('detects changes in scalar values correctly', () => { @@ -40,6 +59,7 @@ describe('objectRecordChangedValues', () => { oldRecord, newRecord, mockObjectMetadata, + mockFlatFieldMetadataMaps, ); expect(result).toEqual({ @@ -61,6 +81,7 @@ describe('objectRecordChangedValues', () => { oldRecord, newRecord, mockObjectMetadata, + mockFlatFieldMetadataMaps, ); expect(result).toEqual({}); @@ -82,6 +103,7 @@ describe('objectRecordChangedValues', () => { oldRecord, newRecord, mockObjectMetadata, + mockFlatFieldMetadataMaps, ); expect(result).toEqual({}); @@ -111,6 +133,7 @@ describe('objectRecordChangedValues', () => { oldRecord, newRecord, mockObjectMetadata, + mockFlatFieldMetadataMaps, ); expect(result).toEqual(expectedChanges); diff --git a/packages/twenty-server/src/engine/core-modules/event-emitter/utils/object-record-changed-values.ts b/packages/twenty-server/src/engine/core-modules/event-emitter/utils/object-record-changed-values.ts index 395cc81a6bb..eed9d7af47c 100644 --- a/packages/twenty-server/src/engine/core-modules/event-emitter/utils/object-record-changed-values.ts +++ b/packages/twenty-server/src/engine/core-modules/event-emitter/utils/object-record-changed-values.ts @@ -2,10 +2,13 @@ import deepEqual from 'deep-equal'; import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata'; import { FieldMetadataType, type ObjectRecord } from 'twenty-shared/types'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; const isWorkflowVersionStepsOrTrigger = ( - objectMetadataItem: ObjectMetadataItemWithFieldMaps, + objectMetadataItem: FlatObjectMetadata, key: string, ) => { return ( @@ -15,7 +18,7 @@ const isWorkflowVersionStepsOrTrigger = ( }; const isWorkflowAutomatedTriggerSettings = ( - objectMetadataItem: Pick, + objectMetadataItem: Pick, key: string, ) => { return ( @@ -27,12 +30,18 @@ const isWorkflowAutomatedTriggerSettings = ( export const objectRecordChangedValues = ( oldRecord: Partial, newRecord: Partial, - objectMetadataItem: ObjectMetadataItemWithFieldMaps, + objectMetadataItem: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, ) => { + const { fieldIdByName } = buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + objectMetadataItem, + ); + return Object.keys(newRecord).reduce( (acc, key) => { - const field = - objectMetadataItem.fieldsById[objectMetadataItem.fieldIdByName[key]]; + const fieldId = fieldIdByName[key]; + const field = fieldId ? flatFieldMetadataMaps.byId[fieldId] : undefined; const oldRecordValue = oldRecord[key]; const newRecordValue = newRecord[key]; diff --git a/packages/twenty-server/src/engine/core-modules/open-api/open-api.module.ts b/packages/twenty-server/src/engine/core-modules/open-api/open-api.module.ts index 1666013cfe9..e61bd8db862 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/open-api.module.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/open-api.module.ts @@ -4,10 +4,16 @@ import { AuthModule } from 'src/engine/core-modules/auth/auth.module'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { OpenApiController } from 'src/engine/core-modules/open-api/open-api.controller'; import { OpenApiService } from 'src/engine/core-modules/open-api/open-api.service'; +import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module'; import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module'; @Module({ - imports: [ObjectMetadataModule, AuthModule, FeatureFlagModule], + imports: [ + ObjectMetadataModule, + AuthModule, + FeatureFlagModule, + WorkspaceManyOrAllFlatEntityMapsCacheModule, + ], controllers: [OpenApiController], providers: [OpenApiService], }) diff --git a/packages/twenty-server/src/engine/core-modules/open-api/open-api.service.spec.ts b/packages/twenty-server/src/engine/core-modules/open-api/open-api.service.spec.ts index 6305e114367..24a562d4e6c 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/open-api.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/open-api.service.spec.ts @@ -4,6 +4,7 @@ import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/ import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { OpenApiService } from 'src/engine/core-modules/open-api/open-api.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; +import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service'; describe('OpenApiService', () => { @@ -29,6 +30,12 @@ describe('OpenApiService', () => { provide: FeatureFlagService, useValue: {}, }, + { + provide: WorkspaceManyOrAllFlatEntityMapsCacheService, + useValue: { + getOrRecomputeManyOrAllFlatEntityMaps: jest.fn(), + }, + }, ], }).compile(); diff --git a/packages/twenty-server/src/engine/core-modules/open-api/open-api.service.ts b/packages/twenty-server/src/engine/core-modules/open-api/open-api.service.ts index b9c141c8ed7..3984f93a099 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/open-api.service.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/open-api.service.ts @@ -45,10 +45,9 @@ import { getUpdateOneResponse200, } from 'src/engine/core-modules/open-api/utils/responses.utils'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; -import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception'; -import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { standardObjectMetadataDefinitions } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects'; import { shouldExcludeFromWorkspaceApi } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/should-exclude-from-workspace-api.util'; import { getServerUrl } from 'src/utils/get-server-url'; @@ -58,7 +57,7 @@ export class OpenApiService { constructor( private readonly accessTokenService: AccessTokenService, private readonly twentyConfigService: TwentyConfigService, - private readonly objectMetadataService: ObjectMetadataServiceV2, + private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, private readonly featureFlagService: FeatureFlagService, ) {} @@ -75,15 +74,28 @@ export class OpenApiService { } } - private async getObjectMetadataItems(workspace: WorkspaceEntity) { - return await this.objectMetadataService.findManyWithinWorkspace( - workspace.id, - { - order: { - namePlural: 'ASC', + private async getFlatObjectMetadataArray(workspaceId: string) { + const { flatObjectMetadataMaps, flatFieldMetadataMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatObjectMetadataMaps', 'flatFieldMetadataMaps'], }, - }, + ); + + const flatObjectMetadataArray = Object.values( + flatObjectMetadataMaps.byId, + ).filter(isDefined); + + flatObjectMetadataArray.sort((a, b) => + a.namePlural.localeCompare(b.namePlural), ); + + return { + flatObjectMetadataArray, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + }; } async generateCoreSchema(request: Request): Promise { @@ -105,33 +117,65 @@ export class OpenApiService { return schema; } - const objectMetadataItems = await this.getObjectMetadataItems(workspace); + const { + flatObjectMetadataArray, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + } = await this.getFlatObjectMetadataArray(workspace.id); - if (!objectMetadataItems.length) { + if (!flatObjectMetadataArray.length) { return schema; } const workspaceFeatureFlagsMap = await this.featureFlagService.getWorkspaceFeatureFlagsMap(workspace.id); - const filteredObjectMetadataItems = objectMetadataItems.filter((item) => { - return !shouldExcludeFromWorkspaceApi( - item, - standardObjectMetadataDefinitions, - workspaceFeatureFlagsMap, - ); - }); + const filteredObjectMetadataItems = flatObjectMetadataArray.filter( + (item) => { + return !shouldExcludeFromWorkspaceApi( + item, + standardObjectMetadataDefinitions, + workspaceFeatureFlagsMap, + ); + }, + ); schema.paths = filteredObjectMetadataItems.reduce((paths, item) => { - paths[`/${item.namePlural}`] = computeManyResultPath(item); - paths[`/batch/${item.namePlural}`] = computeBatchPath(item); - paths[`/${item.namePlural}/{id}`] = computeSingleResultPath(item); - paths[`/${item.namePlural}/duplicates`] = - computeDuplicatesResultPath(item); - paths[`/restore/${item.namePlural}/{id}`] = - computeRestoreOneResultPath(item); - paths[`/restore/${item.namePlural}`] = computeRestoreManyResultPath(item); - paths[`/${item.namePlural}/merge`] = computeMergeManyResultPath(item); + paths[`/${item.namePlural}`] = computeManyResultPath( + item, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + ); + paths[`/batch/${item.namePlural}`] = computeBatchPath( + item, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + ); + paths[`/${item.namePlural}/{id}`] = computeSingleResultPath( + item, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + ); + paths[`/${item.namePlural}/duplicates`] = computeDuplicatesResultPath( + item, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + ); + paths[`/restore/${item.namePlural}/{id}`] = computeRestoreOneResultPath( + item, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + ); + paths[`/restore/${item.namePlural}`] = computeRestoreManyResultPath( + item, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + ); + paths[`/${item.namePlural}/merge`] = computeMergeManyResultPath( + item, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + ); return paths; }, schema.paths as OpenAPIV3_1.PathsObject); @@ -143,19 +187,34 @@ export class OpenApiService { DatabaseEventAction.CREATED, item.nameSingular, ) - ] = computeWebhooks(DatabaseEventAction.CREATED, item); + ] = computeWebhooks( + DatabaseEventAction.CREATED, + item, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + ); paths[ this.createWebhookEventName( DatabaseEventAction.UPDATED, item.nameSingular, ) - ] = computeWebhooks(DatabaseEventAction.UPDATED, item); + ] = computeWebhooks( + DatabaseEventAction.UPDATED, + item, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + ); paths[ this.createWebhookEventName( DatabaseEventAction.DELETED, item.nameSingular, ) - ] = computeWebhooks(DatabaseEventAction.DELETED, item); + ] = computeWebhooks( + DatabaseEventAction.DELETED, + item, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + ); return paths; }, @@ -167,7 +226,11 @@ export class OpenApiService { schema.components = { ...schema.components, // components.securitySchemes is defined in base Schema - schemas: computeSchemaComponents(filteredObjectMetadataItems), + schemas: computeSchemaComponents( + filteredObjectMetadataItems, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + ), parameters: computeParameterComponents(), responses: { '400': get400ErrorResponses(), @@ -347,7 +410,7 @@ export class OpenApiService { metadata.map((item) => ({ nameSingular: item.nameSingular, namePlural: item.namePlural, - })) as ObjectMetadataEntity[], + })) as FlatObjectMetadata[], ); return schema; diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/components.utils.spec.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/components.utils.spec.ts index 125330843db..dc3515575d5 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/components.utils.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/components.utils.spec.ts @@ -5,15 +5,48 @@ import { FieldMetadataType, NumberDataType } from 'twenty-shared/types'; import { objectMetadataItemMock } from 'src/engine/api/__mocks__/object-metadata-item.mock'; import { computeSchemaComponents } from 'src/engine/core-modules/open-api/utils/components.utils'; import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; describe('computeSchemaComponents', () => { faker.seed(1); it('should compute schema components', () => { + const flatObjectMetadata: FlatObjectMetadata = { + ...objectMetadataItemMock, + universalIdentifier: 'objectName', + fieldMetadataIds: objectMetadataItemMock.fields.map((f) => f.id), + } as any; + + const flatFieldMetadataMaps = { + byId: Object.fromEntries( + objectMetadataItemMock.fields.map((f) => [f.id, f as any]), + ), + idByUniversalIdentifier: {}, + universalIdentifiersByApplicationId: {}, + }; + + const relationTargetObjectMetadata: FlatObjectMetadata = { + id: 'relationTargetObjectId', + nameSingular: 'relationTargetObject', + namePlural: 'relationTargetObjects', + universalIdentifier: 'relationTargetObject', + fieldMetadataIds: [], + } as any; + + const flatObjectMetadataMaps = { + byId: { + [flatObjectMetadata.id]: flatObjectMetadata, + [relationTargetObjectMetadata.id]: relationTargetObjectMetadata, + }, + idByUniversalIdentifier: {}, + universalIdentifiersByApplicationId: {}, + }; + expect( - computeSchemaComponents([ - objectMetadataItemMock, - ] as ObjectMetadataEntity[]), + computeSchemaComponents( + [flatObjectMetadata], + flatObjectMetadataMaps, + flatFieldMetadataMaps, + ), ).toMatchInlineSnapshot(` { "ObjectName": { @@ -819,17 +852,37 @@ describe('computeSchemaComponents', () => { ]; it.each(testsCases)('$title', ({ context: field }) => { + const flatObjectMetadata: FlatObjectMetadata = { + targetTableName: 'testingObject', + id: 'mockObjectId', + nameSingular: 'objectName', + namePlural: 'objectsName', + universalIdentifier: 'objectName', + fieldMetadataIds: [field.id], + } as any; + + const flatFieldMetadataMaps = { + byId: { + [field.id]: field as any, + }, + idByUniversalIdentifier: {}, + universalIdentifiersByApplicationId: {}, + }; + + const flatObjectMetadataMaps = { + byId: { + [flatObjectMetadata.id]: flatObjectMetadata, + }, + idByUniversalIdentifier: {}, + universalIdentifiersByApplicationId: {}, + }; + expect( - computeSchemaComponents([ - { - targetTableName: 'testingObject', - id: 'mockObjectId', - nameSingular: 'objectName', - namePlural: 'objectsName', - //@ts-expect-error Passing partial FieldMetadataEntity array - fields: [field], - }, - ]), + computeSchemaComponents( + [flatObjectMetadata], + flatObjectMetadataMaps, + flatFieldMetadataMaps, + ), ).toMatchSnapshot(); }); }); diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts index 55ef3053198..a553eda44e8 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts @@ -17,6 +17,10 @@ import { computeStartingAfterParameters, computeUpsertParameters, } from 'src/engine/core-modules/open-api/utils/parameters.utils'; +import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; +import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { convertObjectMetadataToSchemaProperties } from 'src/engine/utils/convert-object-metadata-to-schema-properties.util'; import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; @@ -31,12 +35,18 @@ type Properties = { type OpenApiExample = Record; const getSchemaComponentsExample = ( - item: ObjectMetadataEntity, + item: FlatObjectMetadata, + flatFieldMetadatas: FlatFieldMetadata[], ): OpenApiExample => { - return item.fields.reduce((node, field) => { + return flatFieldMetadatas.reduce((node, field) => { // If field is required if (!field.isNullable && field.defaultValue === null) { - return { ...node, [field.name]: generateRandomFieldValue({ field }) }; + return { + ...node, + [field.name]: generateRandomFieldValue({ + field, + }), + }; } switch (field.type) { @@ -60,7 +70,9 @@ const getSchemaComponentsExample = ( case FieldMetadataType.PHONES: { return { ...node, - [field.name]: generateRandomFieldValue({ field }), + [field.name]: generateRandomFieldValue({ + field, + }), }; } @@ -72,9 +84,13 @@ const getSchemaComponentsExample = ( }; const getSchemaComponentsRelationProperties = ( - item: ObjectMetadataEntity, + flatFieldMetadatas: FlatFieldMetadata[], + flatObjectMetadataMaps: Pick< + AllFlatEntityMaps, + 'flatObjectMetadataMaps' + >['flatObjectMetadataMaps'], ): Properties => { - return item.fields.reduce((node, field) => { + return flatFieldMetadatas.reduce((node, field) => { if (field.type !== FieldMetadataType.RELATION) { return node; } @@ -82,13 +98,20 @@ const getSchemaComponentsRelationProperties = ( let itemProperty = {} as Property; if (isFieldMetadataEntityOfType(field, FieldMetadataType.RELATION)) { + const targetObjectMetadata = + flatObjectMetadataMaps.byId[field.relationTargetObjectMetadataId]; + + if (!targetObjectMetadata) { + return node; + } + if (field.settings?.relationType === RelationType.MANY_TO_ONE) { itemProperty = { type: 'object', oneOf: [ { $ref: `#/components/schemas/${capitalize( - field.relationTargetObjectMetadata.nameSingular, + targetObjectMetadata.nameSingular, )}ForResponse`, }, ], @@ -98,7 +121,7 @@ const getSchemaComponentsRelationProperties = ( type: 'array', items: { $ref: `#/components/schemas/${capitalize( - field.relationTargetObjectMetadata.nameSingular, + targetObjectMetadata.nameSingular, )}ForResponse`, }, }; @@ -117,8 +140,10 @@ const getSchemaComponentsRelationProperties = ( }, {} as Properties); }; -const getRequiredFields = (item: ObjectMetadataEntity): string[] => { - return item.fields.reduce((required, field) => { +const getRequiredFields = ( + flatFieldMetadatas: FlatFieldMetadata[], +): string[] => { + return flatFieldMetadatas.reduce((required, field) => { if (!field.isNullable && field.defaultValue === null) { required.push(field.name); @@ -131,10 +156,17 @@ const getRequiredFields = (item: ObjectMetadataEntity): string[] => { const computeSchemaComponent = ({ item, + flatFieldMetadatas, + flatObjectMetadataMaps, forResponse, forUpdate, }: { - item: ObjectMetadataEntity; + item: FlatObjectMetadata; + flatFieldMetadatas: FlatFieldMetadata[]; + flatObjectMetadataMaps: Pick< + AllFlatEntityMaps, + 'flatObjectMetadataMaps' + >['flatObjectMetadataMaps']; forResponse: boolean; forUpdate: boolean; }): OpenAPIV3_1.SchemaObject => { @@ -142,20 +174,31 @@ const computeSchemaComponent = ({ const withRequiredFields = !forResponse && !forUpdate; + // Create a temporary object that looks like ObjectMetadataEntity for the converter + const tempItem = { + ...item, + fields: flatFieldMetadatas, + } as unknown as ObjectMetadataEntity; + const result: OpenAPIV3_1.SchemaObject = { type: 'object', description: item.description ?? undefined, properties: convertObjectMetadataToSchemaProperties({ - item, + item: tempItem, forResponse, }) as Properties, - ...(!forResponse ? { example: getSchemaComponentsExample(item) } : {}), + ...(!forResponse + ? { example: getSchemaComponentsExample(item, flatFieldMetadatas) } + : {}), }; if (withRelations) { result.properties = { ...result.properties, - ...getSchemaComponentsRelationProperties(item), + ...getSchemaComponentsRelationProperties( + flatFieldMetadatas, + flatObjectMetadataMaps, + ), }; } @@ -163,7 +206,7 @@ const computeSchemaComponent = ({ return result; } - const requiredFields = getRequiredFields(item); + const requiredFields = getRequiredFields(flatFieldMetadatas); if (requiredFields?.length) { result.required = requiredFields; @@ -173,24 +216,44 @@ const computeSchemaComponent = ({ }; export const computeSchemaComponents = ( - objectMetadataItems: ObjectMetadataEntity[], + flatObjectMetadataItems: FlatObjectMetadata[], + flatObjectMetadataMaps: Pick< + AllFlatEntityMaps, + 'flatObjectMetadataMaps' + >['flatObjectMetadataMaps'], + flatFieldMetadataMaps: Pick< + AllFlatEntityMaps, + 'flatFieldMetadataMaps' + >['flatFieldMetadataMaps'], ): Record => { - return objectMetadataItems.reduce( + return flatObjectMetadataItems.reduce( (schemas, item) => { + const flatFieldMetadatas = + findManyFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityMaps: flatFieldMetadataMaps, + flatEntityIds: item.fieldMetadataIds, + }); + schemas[capitalize(item.nameSingular)] = computeSchemaComponent({ item, + flatFieldMetadatas, + flatObjectMetadataMaps, forResponse: false, forUpdate: false, }); schemas[capitalize(item.nameSingular) + 'ForUpdate'] = computeSchemaComponent({ item, + flatFieldMetadatas, + flatObjectMetadataMaps, forResponse: false, forUpdate: true, }); schemas[capitalize(item.nameSingular) + 'ForResponse'] = computeSchemaComponent({ item, + flatFieldMetadatas, + flatObjectMetadataMaps, forResponse: true, forUpdate: false, }); diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/compute-schema-tags.utils.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/compute-schema-tags.utils.ts index 9fd013f0b50..bff393dcae7 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/compute-schema-tags.utils.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/compute-schema-tags.utils.ts @@ -1,10 +1,10 @@ import { type OpenAPIV3_1 } from 'openapi-types'; import { capitalize } from 'twenty-shared/utils'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; export const computeSchemaTags = ( - items: ObjectMetadataEntity[], + items: Pick[], ): OpenAPIV3_1.TagObject[] => { const results = [{ name: 'General', description: 'General requests' }]; diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/computeWebhooks.utils.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/computeWebhooks.utils.ts index efa50eeee4b..d7c10ae9e8e 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/computeWebhooks.utils.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/computeWebhooks.utils.ts @@ -2,11 +2,20 @@ import { type OpenAPIV3_1 } from 'openapi-types'; import { capitalize } from 'twenty-shared/utils'; import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; export const computeWebhooks = ( type: DatabaseEventAction, - item: ObjectMetadataEntity, + item: Pick, + _flatObjectMetadataMaps: Pick< + AllFlatEntityMaps, + 'flatObjectMetadataMaps' + >['flatObjectMetadataMaps'], + _flatFieldMetadataMaps: Pick< + AllFlatEntityMaps, + 'flatFieldMetadataMaps' + >['flatFieldMetadataMaps'], ): OpenAPIV3_1.PathItemObject => { const updatedFields = { type: 'array', diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/generate-random-field-value.util.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/generate-random-field-value.util.ts index 9f285702168..ca46acac848 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/generate-random-field-value.util.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/generate-random-field-value.util.ts @@ -6,11 +6,12 @@ import { v4 } from 'uuid'; import { type FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface'; import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; export const generateRandomFieldValue = ({ field, }: { - field: FieldMetadataEntity; + field: FieldMetadataEntity | FlatFieldMetadata; }): FieldMetadataDefaultValue => { switch (field.type) { case FieldMetadataType.UUID: { diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/path.utils.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/path.utils.ts index 17d3de5acb7..2f53964f432 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/path.utils.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/path.utils.ts @@ -23,10 +23,19 @@ import { getUpdateManyResponse200, getUpdateOneResponse200, } from 'src/engine/core-modules/open-api/utils/responses.utils'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; export const computeBatchPath = ( - item: ObjectMetadataEntity, + item: Pick, + _flatObjectMetadataMaps: Pick< + AllFlatEntityMaps, + 'flatObjectMetadataMaps' + >['flatObjectMetadataMaps'], + _flatFieldMetadataMaps: Pick< + AllFlatEntityMaps, + 'flatFieldMetadataMaps' + >['flatFieldMetadataMaps'], ): OpenAPIV3_1.PathItemObject => { return { post: { @@ -48,7 +57,15 @@ export const computeBatchPath = ( }; export const computeManyResultPath = ( - item: ObjectMetadataEntity, + item: Pick, + _flatObjectMetadataMaps: Pick< + AllFlatEntityMaps, + 'flatObjectMetadataMaps' + >['flatObjectMetadataMaps'], + _flatFieldMetadataMaps: Pick< + AllFlatEntityMaps, + 'flatFieldMetadataMaps' + >['flatFieldMetadataMaps'], ): OpenAPIV3_1.PathItemObject => { return { get: { @@ -118,7 +135,15 @@ export const computeManyResultPath = ( }; export const computeSingleResultPath = ( - item: ObjectMetadataEntity, + item: Pick, + _flatObjectMetadataMaps: Pick< + AllFlatEntityMaps, + 'flatObjectMetadataMaps' + >['flatObjectMetadataMaps'], + _flatFieldMetadataMaps: Pick< + AllFlatEntityMaps, + 'flatFieldMetadataMaps' + >['flatFieldMetadataMaps'], ): OpenAPIV3_1.PathItemObject => { return { get: { @@ -189,7 +214,15 @@ export const computeOpenApiPath = ( }; export const computeDuplicatesResultPath = ( - item: ObjectMetadataEntity, + item: Pick, + _flatObjectMetadataMaps: Pick< + AllFlatEntityMaps, + 'flatObjectMetadataMaps' + >['flatObjectMetadataMaps'], + _flatFieldMetadataMaps: Pick< + AllFlatEntityMaps, + 'flatFieldMetadataMaps' + >['flatFieldMetadataMaps'], ): OpenAPIV3_1.PathItemObject => { return { post: { @@ -209,7 +242,15 @@ export const computeDuplicatesResultPath = ( }; export const computeRestoreOneResultPath = ( - item: ObjectMetadataEntity, + item: Pick, + _flatObjectMetadataMaps: Pick< + AllFlatEntityMaps, + 'flatObjectMetadataMaps' + >['flatObjectMetadataMaps'], + _flatFieldMetadataMaps: Pick< + AllFlatEntityMaps, + 'flatFieldMetadataMaps' + >['flatFieldMetadataMaps'], ): OpenAPIV3_1.PathItemObject => { return { patch: { @@ -230,7 +271,15 @@ export const computeRestoreOneResultPath = ( }; export const computeRestoreManyResultPath = ( - item: ObjectMetadataEntity, + item: Pick, + _flatObjectMetadataMaps: Pick< + AllFlatEntityMaps, + 'flatObjectMetadataMaps' + >['flatObjectMetadataMaps'], + _flatFieldMetadataMaps: Pick< + AllFlatEntityMaps, + 'flatFieldMetadataMaps' + >['flatFieldMetadataMaps'], ): OpenAPIV3_1.PathItemObject => { return { patch: { @@ -251,7 +300,15 @@ export const computeRestoreManyResultPath = ( }; export const computeMergeManyResultPath = ( - item: ObjectMetadataEntity, + item: Pick, + _flatObjectMetadataMaps: Pick< + AllFlatEntityMaps, + 'flatObjectMetadataMaps' + >['flatObjectMetadataMaps'], + _flatFieldMetadataMaps: Pick< + AllFlatEntityMaps, + 'flatFieldMetadataMaps' + >['flatFieldMetadataMaps'], ): OpenAPIV3_1.PathItemObject => { return { patch: { diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/responses.utils.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/responses.utils.ts index 88aba47abee..feaed463e3b 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/responses.utils.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/responses.utils.ts @@ -1,9 +1,9 @@ import { capitalize } from 'twenty-shared/utils'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; export const getFindManyResponse200 = ( - item: Pick, + item: Pick, fromMetadata = false, ) => { const schemaRef = `#/components/schemas/${capitalize( @@ -55,7 +55,7 @@ export const getFindManyResponse200 = ( }; export const getFindOneResponse200 = ( - item: Pick, + item: Pick, ) => { const schemaRef = `#/components/schemas/${capitalize(item.nameSingular)}ForResponse`; @@ -82,7 +82,7 @@ export const getFindOneResponse200 = ( }; export const getRestoreOneResponse200 = ( - item: Pick, + item: Pick, ) => { const schemaRef = `#/components/schemas/${capitalize(item.nameSingular)}ForResponse`; @@ -109,7 +109,7 @@ export const getRestoreOneResponse200 = ( }; export const getRestoreManyResponse200 = ( - item: Pick, + item: Pick, ) => { const schemaRef = `#/components/schemas/${capitalize( item.nameSingular, @@ -141,7 +141,7 @@ export const getRestoreManyResponse200 = ( }; export const getCreateOneResponse201 = ( - item: Pick, + item: Pick, fromMetadata = false, ) => { const one = fromMetadata ? 'One' : ''; @@ -173,7 +173,7 @@ export const getCreateOneResponse201 = ( }; export const getCreateManyResponse201 = ( - item: Pick, + item: Pick, ) => { const schemaRef = `#/components/schemas/${capitalize( item.nameSingular, @@ -205,7 +205,7 @@ export const getCreateManyResponse201 = ( }; export const getUpdateOneResponse200 = ( - item: Pick, + item: Pick, fromMetadata = false, ) => { const one = fromMetadata ? 'One' : ''; @@ -234,7 +234,7 @@ export const getUpdateOneResponse200 = ( }; export const getDeleteManyResponse200 = ( - item: Pick, + item: Pick, ) => { return { description: 'Successful operation', @@ -268,7 +268,7 @@ export const getDeleteManyResponse200 = ( }; export const getUpdateManyResponse200 = ( - item: Pick, + item: Pick, ) => { const schemaRef = `#/components/schemas/${capitalize(item.nameSingular)}ForResponse`; @@ -298,7 +298,7 @@ export const getUpdateManyResponse200 = ( }; export const getDeleteResponse200 = ( - item: Pick, + item: Pick, fromMetadata = false, ) => { const one = fromMetadata ? 'One' : ''; @@ -391,7 +391,7 @@ export const getJsonResponse = () => { }; export const getFindDuplicatesResponse200 = ( - item: Pick, + item: Pick, ) => { const schemaRef = `#/components/schemas/${capitalize( item.nameSingular, @@ -441,7 +441,7 @@ export const getFindDuplicatesResponse200 = ( }; export const getMergeManyResponse200 = ( - item: Pick, + item: Pick, ) => { const schemaRef = `#/components/schemas/${capitalize( item.nameSingular, diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/services/create-record.service.ts b/packages/twenty-server/src/engine/core-modules/record-crud/services/create-record.service.ts index e4a00c6c643..93ce3b01a81 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/services/create-record.service.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/services/create-record.service.ts @@ -1,8 +1,8 @@ import { Injectable, Logger } from '@nestjs/common'; -import { isDefined } from 'class-validator'; -import { canObjectBeManagedByWorkflow } from 'twenty-shared/workflow'; import { FieldActorSource } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; +import { canObjectBeManagedByWorkflow } from 'twenty-shared/workflow'; import { RecordCrudException, @@ -13,11 +13,11 @@ import { getSelectedColumnsFromRestrictedFields } from 'src/engine/core-modules/ import { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service'; import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service'; import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type'; +import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; +import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; -import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; @Injectable() -// eslint-disable-next-line @nx/workspace-inject-workspace-repository export class CreateRecordService { private readonly logger = new Logger(CreateRecordService.name); @@ -25,7 +25,6 @@ export class CreateRecordService { private readonly twentyORMGlobalManager: TwentyORMGlobalManager, private readonly recordPositionService: RecordPositionService, private readonly recordInputTransformerService: RecordInputTransformerService, - private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService, ) {} async execute(params: CreateRecordParams): Promise { @@ -48,16 +47,30 @@ export class CreateRecordService { rolePermissionConfig, ); - const { objectMetadataItemWithFieldsMaps } = - await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps( - objectName, - workspaceId, + const { + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, + } = repository.internalContext; + + const objectId = objectIdByNameSingular[objectName]; + + if (!isDefined(objectId)) { + throw new RecordCrudException( + `Object ${objectName} not found`, + RecordCrudExceptionCode.INVALID_REQUEST, ); + } + + const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityMaps: flatObjectMetadataMaps, + flatEntityId: objectId, + }); if ( !canObjectBeManagedByWorkflow({ - nameSingular: objectMetadataItemWithFieldsMaps.nameSingular, - isSystem: objectMetadataItemWithFieldsMaps.isSystem, + nameSingular: flatObjectMetadata.nameSingular, + isSystem: flatObjectMetadata.isSystem, }) ) { throw new RecordCrudException( @@ -68,34 +81,39 @@ export class CreateRecordService { const position = await this.recordPositionService.buildRecordPosition({ value: 'first', - objectMetadata: objectMetadataItemWithFieldsMaps, + objectMetadata: flatObjectMetadata, workspaceId, }); + const { fieldIdByName, fieldIdByJoinColumnName } = + buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + flatObjectMetadata, + ); + const validObjectRecord = Object.fromEntries( Object.entries(objectRecord).filter( ([key]) => - isDefined(objectMetadataItemWithFieldsMaps.fieldIdByName[key]) || - isDefined( - objectMetadataItemWithFieldsMaps.fieldIdByJoinColumnName[key], - ), + isDefined(fieldIdByName[key]) || + isDefined(fieldIdByJoinColumnName[key]), ), ); const transformedObjectRecord = await this.recordInputTransformerService.process({ recordInput: validObjectRecord, - objectMetadataMapItem: objectMetadataItemWithFieldsMaps, + flatObjectMetadata, + flatFieldMetadataMaps, }); const restrictedFields = - repository.objectRecordsPermissions?.[ - objectMetadataItemWithFieldsMaps.id - ]?.restrictedFields; + repository.objectRecordsPermissions?.[flatObjectMetadata.id] + ?.restrictedFields; const selectedColumns = getSelectedColumnsFromRestrictedFields( restrictedFields, - objectMetadataItemWithFieldsMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ); const insertResult = await repository.insert( diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/services/delete-record.service.ts b/packages/twenty-server/src/engine/core-modules/record-crud/services/delete-record.service.ts index a7a1102ab2d..417350b3468 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/services/delete-record.service.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/services/delete-record.service.ts @@ -9,17 +9,15 @@ import { } from 'src/engine/core-modules/record-crud/exceptions/record-crud.exception'; import { type DeleteRecordParams } from 'src/engine/core-modules/record-crud/types/delete-record-params.type'; import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type'; +import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; -import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; @Injectable() -// eslint-disable-next-line @nx/workspace-inject-workspace-repository export class DeleteRecordService { private readonly logger = new Logger(DeleteRecordService.name); constructor( private readonly twentyORMGlobalManager: TwentyORMGlobalManager, - private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService, ) {} async execute(params: DeleteRecordParams): Promise { @@ -55,16 +53,27 @@ export class DeleteRecordService { rolePermissionConfig, ); - const { objectMetadataItemWithFieldsMaps } = - await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps( - objectName, - workspaceId, + const { flatObjectMetadataMaps, objectIdByNameSingular } = + repository.internalContext; + + const objectId = objectIdByNameSingular[objectName]; + + if (!isDefined(objectId)) { + throw new RecordCrudException( + `Object ${objectName} not found`, + RecordCrudExceptionCode.INVALID_REQUEST, ); + } + + const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityMaps: flatObjectMetadataMaps, + flatEntityId: objectId, + }); if ( !canObjectBeManagedByWorkflow({ - nameSingular: objectMetadataItemWithFieldsMaps.nameSingular, - isSystem: objectMetadataItemWithFieldsMaps.isSystem, + nameSingular: flatObjectMetadata.nameSingular, + isSystem: flatObjectMetadata.isSystem, }) ) { throw new RecordCrudException( diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/services/find-records.service.ts b/packages/twenty-server/src/engine/core-modules/record-crud/services/find-records.service.ts index b112990860f..a174cfde684 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/services/find-records.service.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/services/find-records.service.ts @@ -3,6 +3,7 @@ import { Injectable, Logger } from '@nestjs/common'; import isEmpty from 'lodash.isempty'; import { QUERY_MAX_RECORDS } from 'twenty-shared/constants'; import { OrderByDirection } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; import { type ObjectLiteral } from 'typeorm'; import { @@ -12,23 +13,27 @@ import { import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser'; import { getAllSelectableColumnNames } from 'src/engine/api/utils/get-all-selectable-column-names.utils'; +import { + RecordCrudException, + RecordCrudExceptionCode, +} from 'src/engine/core-modules/record-crud/exceptions/record-crud.exception'; import { type FindRecordsParams } from 'src/engine/core-modules/record-crud/types/find-records-params.type'; import { FindRecordsResult } from 'src/engine/core-modules/record-crud/types/find-records-result.type'; import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { type WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder'; import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; -import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; @Injectable() -// eslint-disable-next-line @nx/workspace-inject-workspace-repository export class FindRecordsService { private readonly logger = new Logger(FindRecordsService.name); constructor( private readonly twentyORMGlobalManager: TwentyORMGlobalManager, - private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService, ) {} async execute( @@ -60,15 +65,30 @@ export class FindRecordsService { rolePermissionConfig, ); - const { objectMetadataItemWithFieldsMaps, objectMetadataMaps } = - await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps( - objectName, - workspaceId, + const { + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, + } = repository.internalContext; + + const objectId = objectIdByNameSingular[objectName]; + + if (!isDefined(objectId)) { + throw new RecordCrudException( + `Object ${objectName} not found`, + RecordCrudExceptionCode.INVALID_REQUEST, ); + } + + const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityMaps: flatObjectMetadataMaps, + flatEntityId: objectId, + }); const graphqlQueryParser = new GraphqlQueryParser( - objectMetadataItemWithFieldsMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, ); const records = await this.getObjectRecords({ @@ -79,7 +99,8 @@ export class FindRecordsService { offset, repository, graphqlQueryParser, - objectMetadataItemWithFieldsMaps, + flatObjectMetadata, + flatFieldMetadataMaps, }); const totalCount = await this.getTotalCount({ @@ -87,7 +108,8 @@ export class FindRecordsService { filter, repository, graphqlQueryParser, - objectMetadataItemWithFieldsMaps, + flatObjectMetadata, + flatFieldMetadataMaps, }); this.logger.log(`Found ${records.length} records in ${objectName}`); @@ -115,10 +137,11 @@ export class FindRecordsService { private applyRestrictedFieldsToQueryBuilder( queryBuilder: WorkspaceSelectQueryBuilder, repository: WorkspaceRepository, - objectMetadataItemWithFieldsMaps: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, ): WorkspaceSelectQueryBuilder { const restrictedFields = - repository.objectRecordsPermissions?.[objectMetadataItemWithFieldsMaps.id] + repository.objectRecordsPermissions?.[flatObjectMetadata.id] ?.restrictedFields; if (!restrictedFields || isEmpty(restrictedFields)) { @@ -128,7 +151,8 @@ export class FindRecordsService { const selectableFields = getAllSelectableColumnNames({ restrictedFields, objectMetadata: { - objectMetadataMapItem: objectMetadataItemWithFieldsMaps, + objectMetadataMapItem: flatObjectMetadata, + flatFieldMetadataMaps, }, }); @@ -146,7 +170,8 @@ export class FindRecordsService { offset, repository, graphqlQueryParser, - objectMetadataItemWithFieldsMaps, + flatObjectMetadata, + flatFieldMetadataMaps, }: { objectName: string; filter: @@ -158,7 +183,8 @@ export class FindRecordsService { offset: number; repository: WorkspaceRepository; graphqlQueryParser: GraphqlQueryParser; - objectMetadataItemWithFieldsMaps: ObjectMetadataItemWithFieldMaps; + flatObjectMetadata: FlatObjectMetadata; + flatFieldMetadataMaps: FlatEntityMaps; }): Promise { const queryBuilder = repository.createQueryBuilder(objectName); @@ -183,7 +209,8 @@ export class FindRecordsService { const queryBuilderWithSelect = this.applyRestrictedFieldsToQueryBuilder( withOrderByQueryBuilder, repository, - objectMetadataItemWithFieldsMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ); return queryBuilderWithSelect @@ -197,7 +224,8 @@ export class FindRecordsService { filter, repository, graphqlQueryParser, - objectMetadataItemWithFieldsMaps, + flatObjectMetadata, + flatFieldMetadataMaps, }: { objectName: string; filter: @@ -206,7 +234,8 @@ export class FindRecordsService { | undefined; repository: WorkspaceRepository; graphqlQueryParser: GraphqlQueryParser; - objectMetadataItemWithFieldsMaps: ObjectMetadataItemWithFieldMaps; + flatObjectMetadata: FlatObjectMetadata; + flatFieldMetadataMaps: FlatEntityMaps; }): Promise { const countQueryBuilder = repository.createQueryBuilder(objectName); @@ -225,7 +254,8 @@ export class FindRecordsService { const queryBuilderWithSelect = this.applyRestrictedFieldsToQueryBuilder( withDeletedCountQueryBuilder, repository, - objectMetadataItemWithFieldsMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ); return queryBuilderWithSelect.getCount(); diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/services/update-record.service.ts b/packages/twenty-server/src/engine/core-modules/record-crud/services/update-record.service.ts index 8d974af88ef..4699e2598ed 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/services/update-record.service.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/services/update-record.service.ts @@ -12,18 +12,16 @@ import { type UpdateRecordParams } from 'src/engine/core-modules/record-crud/typ import { getSelectedColumnsFromRestrictedFields } from 'src/engine/core-modules/record-crud/utils/get-selected-columns-from-restricted-fields.util'; import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service'; import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type'; +import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; -import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; @Injectable() -// eslint-disable-next-line @nx/workspace-inject-workspace-repository export class UpdateRecordService { private readonly logger = new Logger(UpdateRecordService.name); constructor( private readonly twentyORMGlobalManager: TwentyORMGlobalManager, private readonly recordInputTransformerService: RecordInputTransformerService, - private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService, ) {} async execute(params: UpdateRecordParams): Promise { @@ -60,20 +58,34 @@ export class UpdateRecordService { rolePermissionConfig, ); - const { objectMetadataItemWithFieldsMaps } = - await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps( - objectName, - workspaceId, + const { + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, + } = repository.internalContext; + + const objectId = objectIdByNameSingular[objectName]; + + if (!isDefined(objectId)) { + throw new RecordCrudException( + `Object ${objectName} not found`, + RecordCrudExceptionCode.INVALID_REQUEST, ); + } + + const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityMaps: flatObjectMetadataMaps, + flatEntityId: objectId, + }); const restrictedFields = - repository.objectRecordsPermissions?.[ - objectMetadataItemWithFieldsMaps.id - ]?.restrictedFields; + repository.objectRecordsPermissions?.[flatObjectMetadata.id] + ?.restrictedFields; const selectedColumns = getSelectedColumnsFromRestrictedFields( restrictedFields, - objectMetadataItemWithFieldsMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ); const previousObjectRecord = await repository.findOne({ @@ -102,8 +114,8 @@ export class UpdateRecordService { if ( !canObjectBeManagedByWorkflow({ - nameSingular: objectMetadataItemWithFieldsMaps.nameSingular, - isSystem: objectMetadataItemWithFieldsMaps.isSystem, + nameSingular: flatObjectMetadata.nameSingular, + isSystem: flatObjectMetadata.isSystem, }) ) { throw new RecordCrudException( @@ -129,7 +141,8 @@ export class UpdateRecordService { const transformedObjectRecord = await this.recordInputTransformerService.process({ recordInput: objectRecordWithFilteredFields, - objectMetadataMapItem: objectMetadataItemWithFieldsMaps, + flatObjectMetadata, + flatFieldMetadataMaps, }); const updatedObjectRecord = { diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/services/upsert-record.service.ts b/packages/twenty-server/src/engine/core-modules/record-crud/services/upsert-record.service.ts index 022504f56a6..972f37830d6 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/services/upsert-record.service.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/services/upsert-record.service.ts @@ -14,19 +14,18 @@ import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output. import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; import { getCompositeTypeOrThrow } from 'src/engine/metadata-modules/field-metadata/utils/get-composite-type-or-throw.util'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; +import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; +import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; import { computeUniqueIndexWhereClause } from 'src/engine/metadata-modules/index-metadata/utils/compute-unique-index-where-clause.util'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; -import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; @Injectable() -// eslint-disable-next-line @nx/workspace-inject-workspace-repository export class UpsertRecordService { private readonly logger = new Logger(UpsertRecordService.name); constructor( private readonly twentyORMGlobalManager: TwentyORMGlobalManager, private readonly recordInputTransformerService: RecordInputTransformerService, - private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService, ) {} async execute(params: UpsertRecordParams): Promise { @@ -53,16 +52,30 @@ export class UpsertRecordService { isDefined(objectRecord[field]), ); - const { objectMetadataItemWithFieldsMaps } = - await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps( - objectName, - workspaceId, + const { + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, + } = repository.internalContext; + + const objectId = objectIdByNameSingular[objectName]; + + if (!isDefined(objectId)) { + throw new RecordCrudException( + `Object ${objectName} not found`, + RecordCrudExceptionCode.INVALID_REQUEST, ); + } + + const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityMaps: flatObjectMetadataMaps, + flatEntityId: objectId, + }); if ( !canObjectBeManagedByWorkflow({ - nameSingular: objectMetadataItemWithFieldsMaps.nameSingular, - isSystem: objectMetadataItemWithFieldsMaps.isSystem, + nameSingular: flatObjectMetadata.nameSingular, + isSystem: flatObjectMetadata.isSystem, }) ) { throw new RecordCrudException( @@ -88,17 +101,24 @@ export class UpsertRecordService { const transformedObjectRecord = await this.recordInputTransformerService.process({ recordInput: objectRecordWithFilteredFields, - objectMetadataMapItem: objectMetadataItemWithFieldsMaps, + flatObjectMetadata, + flatFieldMetadataMaps, }); + const { fieldIdByName, fieldIdByJoinColumnName } = + buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + flatObjectMetadata, + ); + const uniqueFieldsToUpdate = fieldsToUpdateArray .map( - (field) => - objectMetadataItemWithFieldsMaps.fieldIdByName[field] || - objectMetadataItemWithFieldsMaps.fieldIdByJoinColumnName[field], + (fieldName) => + fieldIdByName[fieldName] || fieldIdByJoinColumnName[fieldName], ) - .map((fieldId) => objectMetadataItemWithFieldsMaps.fieldsById[fieldId]) - .filter((field) => field && (field.isUnique || field.name === 'id')); + .map((fieldId) => flatFieldMetadataMaps.byId[fieldId]) + .filter(isDefined) + .filter((field) => field.isUnique || field.name === 'id'); const conflictPathsUniqueFieldsToUpdate = uniqueFieldsToUpdate.flatMap( (field) => { @@ -140,13 +160,13 @@ export class UpsertRecordService { .filter(isDefined); const restrictedFields = - repository.objectRecordsPermissions?.[ - objectMetadataItemWithFieldsMaps.id - ]?.restrictedFields; + repository.objectRecordsPermissions?.[flatObjectMetadata.id] + ?.restrictedFields; const selectedColumns = getSelectedColumnsFromRestrictedFields( restrictedFields, - objectMetadataItemWithFieldsMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ); const upsertResult = await repository.upsert( diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/types/object-metadata-for-tool-schema.type.ts b/packages/twenty-server/src/engine/core-modules/record-crud/types/object-metadata-for-tool-schema.type.ts new file mode 100644 index 00000000000..43dcc9c2a94 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/record-crud/types/object-metadata-for-tool-schema.type.ts @@ -0,0 +1,7 @@ +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; + +// Object metadata type enriched with flat fields for tool schema generation +export type ObjectMetadataForToolSchema = FlatObjectMetadata & { + fields: FlatFieldMetadata[]; +}; diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/utils/generate-create-record-input-schema.util.ts b/packages/twenty-server/src/engine/core-modules/record-crud/utils/generate-create-record-input-schema.util.ts index 3e9e828b5a1..2e388585647 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/utils/generate-create-record-input-schema.util.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/utils/generate-create-record-input-schema.util.ts @@ -1,11 +1,11 @@ import { type RestrictedFieldsPermissions } from 'twenty-shared/types'; import { z } from 'zod'; +import { type ObjectMetadataForToolSchema } from 'src/engine/core-modules/record-crud/types/object-metadata-for-tool-schema.type'; import { generateRecordPropertiesZodSchema } from 'src/engine/core-modules/record-crud/zod-schemas/record-properties.zod-schema'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; export const generateCreateRecordInputSchema = ( - objectMetadata: ObjectMetadataEntity, + objectMetadata: ObjectMetadataForToolSchema, restrictedFields?: RestrictedFieldsPermissions, ) => { const recordPropertiesSchema = generateRecordPropertiesZodSchema( diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/utils/generate-update-record-input-schema.util.ts b/packages/twenty-server/src/engine/core-modules/record-crud/utils/generate-update-record-input-schema.util.ts index 65e014deab3..cd997484001 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/utils/generate-update-record-input-schema.util.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/utils/generate-update-record-input-schema.util.ts @@ -1,11 +1,11 @@ import { type RestrictedFieldsPermissions } from 'twenty-shared/types'; import { z } from 'zod'; +import { type ObjectMetadataForToolSchema } from 'src/engine/core-modules/record-crud/types/object-metadata-for-tool-schema.type'; import { generateRecordPropertiesZodSchema } from 'src/engine/core-modules/record-crud/zod-schemas/record-properties.zod-schema'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; export const generateUpdateRecordInputSchema = ( - objectMetadata: ObjectMetadataEntity, + objectMetadata: ObjectMetadataForToolSchema, restrictedFields?: RestrictedFieldsPermissions, ) => { const recordPropertiesSchema = generateRecordPropertiesZodSchema( diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/utils/get-selected-columns-from-restricted-fields.util.ts b/packages/twenty-server/src/engine/core-modules/record-crud/utils/get-selected-columns-from-restricted-fields.util.ts index aa5ffd8a79b..0c3ac93abbc 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/utils/get-selected-columns-from-restricted-fields.util.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/utils/get-selected-columns-from-restricted-fields.util.ts @@ -2,11 +2,14 @@ import isEmpty from 'lodash.isempty'; import { type RestrictedFieldsPermissions } from 'twenty-shared/types'; import { getAllSelectableColumnNames } from 'src/engine/api/utils/get-all-selectable-column-names.utils'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; export const getSelectedColumnsFromRestrictedFields = ( restrictedFields: RestrictedFieldsPermissions | undefined, - objectMetadataItemWithFieldsMaps: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, ): string[] | undefined => { if (!restrictedFields || isEmpty(restrictedFields)) { return undefined; @@ -15,7 +18,8 @@ export const getSelectedColumnsFromRestrictedFields = ( const selectableFields = getAllSelectableColumnNames({ restrictedFields, objectMetadata: { - objectMetadataMapItem: objectMetadataItemWithFieldsMaps, + objectMetadataMapItem: flatObjectMetadata, + flatFieldMetadataMaps, }, }); diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/zod-schemas/field-filters.zod-schema.ts b/packages/twenty-server/src/engine/core-modules/record-crud/zod-schemas/field-filters.zod-schema.ts index b93c5623dd9..472bb6def96 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/zod-schemas/field-filters.zod-schema.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/zod-schemas/field-filters.zod-schema.ts @@ -1,15 +1,16 @@ -import { z } from 'zod'; import { FieldMetadataType } from 'twenty-shared/types'; +import { z } from 'zod'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; const NullCheckEnum = z.enum(['NULL', 'NOT_NULL']); export const generateFieldFilterZodSchema = ( - field: FieldMetadataEntity, + field: FieldMetadataEntity | FlatFieldMetadata, ): z.ZodTypeAny | null => { switch (field.type) { case FieldMetadataType.UUID: diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/zod-schemas/find-tool.zod-schema.ts b/packages/twenty-server/src/engine/core-modules/record-crud/zod-schemas/find-tool.zod-schema.ts index 66bfacdf34c..ae554682d9f 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/zod-schemas/find-tool.zod-schema.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/zod-schemas/find-tool.zod-schema.ts @@ -5,14 +5,14 @@ import { } from 'twenty-shared/types'; import { z } from 'zod'; +import { type ObjectMetadataForToolSchema } from 'src/engine/core-modules/record-crud/types/object-metadata-for-tool-schema.type'; import { generateFieldFilterZodSchema } from 'src/engine/core-modules/record-crud/zod-schemas/field-filters.zod-schema'; import { ObjectRecordOrderBySchema } from 'src/engine/core-modules/record-crud/zod-schemas/order-by.zod-schema'; import { shouldExcludeFieldFromAgentToolSchema } from 'src/engine/metadata-modules/field-metadata/utils/should-exclude-field-from-agent-tool-schema.util'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; export const generateFindToolInputSchema = ( - objectMetadata: ObjectMetadataEntity, + objectMetadata: ObjectMetadataForToolSchema, restrictedFields?: RestrictedFieldsPermissions, ) => { const filterShape: Record = {}; diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/zod-schemas/record-properties.zod-schema.ts b/packages/twenty-server/src/engine/core-modules/record-crud/zod-schemas/record-properties.zod-schema.ts index 41bb9d6134b..e24d29357ae 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/zod-schemas/record-properties.zod-schema.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/zod-schemas/record-properties.zod-schema.ts @@ -9,11 +9,11 @@ import { z } from 'zod'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { type ObjectMetadataForToolSchema } from 'src/engine/core-modules/record-crud/types/object-metadata-for-tool-schema.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; -const isFieldAvailable = (field: FieldMetadataEntity, forResponse: boolean) => { +const isFieldAvailable = (field: FlatFieldMetadata, forResponse: boolean) => { if (forResponse) { return true; } @@ -28,7 +28,7 @@ const isFieldAvailable = (field: FieldMetadataEntity, forResponse: boolean) => { } }; -const getFieldZodType = (field: FieldMetadataEntity): z.ZodTypeAny => { +const getFieldZodType = (field: FlatFieldMetadata): z.ZodTypeAny => { switch (field.type) { case FieldMetadataType.UUID: return z.string().uuidv4(); @@ -73,7 +73,7 @@ const getFieldZodType = (field: FieldMetadataEntity): z.ZodTypeAny => { }; export const generateRecordPropertiesZodSchema = ( - objectMetadata: ObjectMetadataEntity, + objectMetadata: ObjectMetadataForToolSchema, forResponse = false, restrictedFields?: RestrictedFieldsPermissions, ): z.ZodObject> => { diff --git a/packages/twenty-server/src/engine/core-modules/record-transformer/services/record-input-transformer.service.ts b/packages/twenty-server/src/engine/core-modules/record-transformer/services/record-input-transformer.service.ts index eb867ae12eb..75d5f222024 100644 --- a/packages/twenty-server/src/engine/core-modules/record-transformer/services/record-input-transformer.service.ts +++ b/packages/twenty-server/src/engine/core-modules/record-transformer/services/record-input-transformer.service.ts @@ -11,22 +11,34 @@ import { transformEmailsValue } from 'src/engine/core-modules/record-transformer import { transformLinksValue } from 'src/engine/core-modules/record-transformer/utils/transform-links-value.util'; import { transformPhonesValue } from 'src/engine/core-modules/record-transformer/utils/transform-phones-value.util'; import { transformRichTextV2Value } from 'src/engine/core-modules/record-transformer/utils/transform-rich-text-v2.util'; -import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; +import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; @Injectable() export class RecordInputTransformerService { async process({ recordInput, - objectMetadataMapItem, + flatObjectMetadata, + flatFieldMetadataMaps, }: { recordInput: Partial; - objectMetadataMapItem: ObjectMetadataItemWithFieldMaps; + flatObjectMetadata: FlatObjectMetadata; + flatFieldMetadataMaps: FlatEntityMaps; }): Promise> { let transformedEntries = {}; + const { fieldIdByName } = buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + flatObjectMetadata, + ); + for (const [key, value] of Object.entries(recordInput)) { - const fieldMetadataId = objectMetadataMapItem.fieldIdByName[key]; - const fieldMetadata = objectMetadataMapItem.fieldsById[fieldMetadataId]; + const fieldMetadataId = fieldIdByName[key]; + const fieldMetadata = fieldMetadataId + ? flatFieldMetadataMaps.byId[fieldMetadataId] + : undefined; if (!fieldMetadata) { transformedEntries = { ...transformedEntries, [key]: value }; diff --git a/packages/twenty-server/src/engine/core-modules/search/__tests__/search.service.spec.ts b/packages/twenty-server/src/engine/core-modules/search/__tests__/search.service.spec.ts index 2bfa4583658..6a984ed3174 100644 --- a/packages/twenty-server/src/engine/core-modules/search/__tests__/search.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/search/__tests__/search.service.spec.ts @@ -1,7 +1,10 @@ import { Test, type TestingModule } from '@nestjs/testing'; import { encodeCursorData } from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util'; -import { mockObjectMetadataItemsWithFieldMaps } from 'src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps'; +import { + mockFlatFieldMetadataMaps, + mockFlatObjectMetadatas, +} from 'src/engine/core-modules/__mocks__/mockFlatObjectMetadatas'; import { FileService } from 'src/engine/core-modules/file/services/file.service'; import { SearchService } from 'src/engine/core-modules/search/services/search.service'; import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; @@ -30,53 +33,53 @@ describe('SearchService', () => { describe('filterObjectMetadataItems', () => { it('should return searchable object metadata items', () => { const objectMetadataItems = service.filterObjectMetadataItems({ - objectMetadataItemWithFieldMaps: mockObjectMetadataItemsWithFieldMaps, + flatObjectMetadatas: mockFlatObjectMetadatas, includedObjectNameSingulars: [], excludedObjectNameSingulars: [], }); expect(objectMetadataItems).toEqual([ - mockObjectMetadataItemsWithFieldMaps[0], - mockObjectMetadataItemsWithFieldMaps[1], - mockObjectMetadataItemsWithFieldMaps[2], + mockFlatObjectMetadatas[0], + mockFlatObjectMetadatas[1], + mockFlatObjectMetadatas[2], ]); }); it('should return searchable object metadata items without excluded ones', () => { const objectMetadataItems = service.filterObjectMetadataItems({ - objectMetadataItemWithFieldMaps: mockObjectMetadataItemsWithFieldMaps, + flatObjectMetadatas: mockFlatObjectMetadatas, includedObjectNameSingulars: [], excludedObjectNameSingulars: ['company'], }); expect(objectMetadataItems).toEqual([ - mockObjectMetadataItemsWithFieldMaps[0], - mockObjectMetadataItemsWithFieldMaps[2], + mockFlatObjectMetadatas[0], + mockFlatObjectMetadatas[2], ]); }); it('should return searchable object metadata items with included ones only', () => { const objectMetadataItems = service.filterObjectMetadataItems({ - objectMetadataItemWithFieldMaps: mockObjectMetadataItemsWithFieldMaps, + flatObjectMetadatas: mockFlatObjectMetadatas, includedObjectNameSingulars: ['company'], excludedObjectNameSingulars: [], }); - expect(objectMetadataItems).toEqual([ - mockObjectMetadataItemsWithFieldMaps[1], - ]); + expect(objectMetadataItems).toEqual([mockFlatObjectMetadatas[1]]); }); }); describe('getLabelIdentifierColumns', () => { it('should return the two label identifier columns for a person object metadata item', () => { const labelIdentifierColumns = service.getLabelIdentifierColumns( - mockObjectMetadataItemsWithFieldMaps[0], + mockFlatObjectMetadatas[0], + mockFlatFieldMetadataMaps, ); expect(labelIdentifierColumns).toEqual(['nameFirstName', 'nameLastName']); }); it('should return the label identifier column for a regular object metadata item', () => { const labelIdentifierColumns = service.getLabelIdentifierColumns( - mockObjectMetadataItemsWithFieldMaps[1], + mockFlatObjectMetadatas[1], + mockFlatFieldMetadataMaps, ); expect(labelIdentifierColumns).toEqual(['name']); @@ -86,14 +89,16 @@ describe('SearchService', () => { describe('getImageIdentifierColumn', () => { it('should return null if the object metadata item does not have an image identifier', () => { const imageIdentifierColumn = service.getImageIdentifierColumn( - mockObjectMetadataItemsWithFieldMaps[0], + mockFlatObjectMetadatas[0], + mockFlatFieldMetadataMaps, ); expect(imageIdentifierColumn).toBeNull(); }); it('should return `domainNamePrimaryLinkUrl` column for a company object metadata item', () => { const imageIdentifierColumn = service.getImageIdentifierColumn( - mockObjectMetadataItemsWithFieldMaps[1], + mockFlatObjectMetadatas[1], + mockFlatFieldMetadataMaps, ); expect(imageIdentifierColumn).toEqual('domainNamePrimaryLinkUrl'); @@ -101,7 +106,8 @@ describe('SearchService', () => { it('should return the image identifier column', () => { const imageIdentifierColumn = service.getImageIdentifierColumn( - mockObjectMetadataItemsWithFieldMaps[2], + mockFlatObjectMetadatas[2], + mockFlatFieldMetadataMaps, ); expect(imageIdentifierColumn).toEqual('imageIdentifierFieldName'); diff --git a/packages/twenty-server/src/engine/core-modules/search/exceptions/search.exception.ts b/packages/twenty-server/src/engine/core-modules/search/exceptions/search.exception.ts index 78b84504f7e..66fcd12a76d 100644 --- a/packages/twenty-server/src/engine/core-modules/search/exceptions/search.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/search/exceptions/search.exception.ts @@ -4,4 +4,5 @@ export class SearchException extends CustomException {} export enum SearchExceptionCode { LABEL_IDENTIFIER_FIELD_NOT_FOUND = 'LABEL_IDENTIFIER_FIELD_NOT_FOUND', + OBJECT_METADATA_NOT_FOUND = 'OBJECT_METADATA_NOT_FOUND', } diff --git a/packages/twenty-server/src/engine/core-modules/search/filters/search-api-exception.filter.ts b/packages/twenty-server/src/engine/core-modules/search/filters/search-api-exception.filter.ts index bcd24349c4a..61cda97fec7 100644 --- a/packages/twenty-server/src/engine/core-modules/search/filters/search-api-exception.filter.ts +++ b/packages/twenty-server/src/engine/core-modules/search/filters/search-api-exception.filter.ts @@ -14,6 +14,7 @@ export class SearchApiExceptionFilter implements ExceptionFilter { catch(exception: SearchException) { switch (exception.code) { case SearchExceptionCode.LABEL_IDENTIFIER_FIELD_NOT_FOUND: + case SearchExceptionCode.OBJECT_METADATA_NOT_FOUND: throw exception; default: { assertUnreachable(exception.code); diff --git a/packages/twenty-server/src/engine/core-modules/search/search.resolver.ts b/packages/twenty-server/src/engine/core-modules/search/search.resolver.ts index 2ec10e1211f..caf1b4f27fc 100644 --- a/packages/twenty-server/src/engine/core-modules/search/search.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/search/search.resolver.ts @@ -14,9 +14,6 @@ import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorat import { CustomPermissionGuard } from 'src/engine/guards/custom-permission.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; -import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util'; -import { buildObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-metadata-item-with-field-maps.util'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; @Resolver() @UseFilters(SearchApiExceptionFilter, PreventNestToAutoLogGraphqlErrorsFilter) @@ -41,48 +38,29 @@ export class SearchResolver { after, }: SearchArgs, ) { - const { flatObjectMetadataMaps, flatFieldMetadataMaps, flatIndexMaps } = + const { flatObjectMetadataMaps, flatFieldMetadataMaps } = await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { workspaceId: workspace.id, - flatMapsKeys: [ - 'flatObjectMetadataMaps', - 'flatFieldMetadataMaps', - 'flatIndexMaps', - ], + flatMapsKeys: ['flatObjectMetadataMaps', 'flatFieldMetadataMaps'], }, ); - const { idByNameSingular } = buildObjectIdByNameMaps( - flatObjectMetadataMaps, - ); - const objectMetadataMaps: ObjectMetadataMaps = { - byId: {}, - idByNameSingular, - }; - - for (const [id, flatObj] of Object.entries(flatObjectMetadataMaps.byId)) { - if (isDefined(flatObj)) { - objectMetadataMaps.byId[id] = buildObjectMetadataItemWithFieldMaps( - flatObj, - flatFieldMetadataMaps, - flatIndexMaps, - ); - } - } + const flatObjectMetadatas = Object.values( + flatObjectMetadataMaps.byId, + ).filter(isDefined); const filteredObjectMetadataItems = this.searchService.filterObjectMetadataItems({ - objectMetadataItemWithFieldMaps: Object.values( - objectMetadataMaps.byId, - ).filter(isDefined), + flatObjectMetadatas, includedObjectNameSingulars: includedObjectNameSingulars ?? [], excludedObjectNameSingulars: excludedObjectNameSingulars ?? [], }); const allRecordsWithObjectMetadataItems = await this.searchService.getAllRecordsWithObjectMetadataItems({ - objectMetadataItemWithFieldMaps: filteredObjectMetadataItems, + flatObjectMetadatas: filteredObjectMetadataItems, + flatFieldMetadataMaps, searchInput, limit, filter, @@ -93,6 +71,7 @@ export class SearchResolver { return this.searchService.computeSearchObjectResults({ recordsWithObjectMetadataItems: allRecordsWithObjectMetadataItems, + flatFieldMetadataMaps, workspaceId: workspace.id, limit, after, diff --git a/packages/twenty-server/src/engine/core-modules/search/services/search.service.ts b/packages/twenty-server/src/engine/core-modules/search/services/search.service.ts index 9823fd69f6d..c7045d8bc40 100644 --- a/packages/twenty-server/src/engine/core-modules/search/services/search.service.ts +++ b/packages/twenty-server/src/engine/core-modules/search/services/search.service.ts @@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common'; import { isNonEmptyString } from '@sniptt/guards'; import chunk from 'lodash.chunk'; import { FieldMetadataType, ObjectRecord } from 'twenty-shared/types'; -import { getLogoUrlFromDomainName } from 'twenty-shared/utils'; +import { getLogoUrlFromDomainName, isDefined } from 'twenty-shared/utils'; import { Brackets, type ObjectLiteral } from 'typeorm'; import { type ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; @@ -26,9 +26,10 @@ import { } from 'src/engine/core-modules/search/exceptions/search.exception'; import { type RecordsWithObjectMetadataItem } from 'src/engine/core-modules/search/types/records-with-object-metadata-item'; import { formatSearchTerms } from 'src/engine/core-modules/search/utils/format-search-terms'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { generateObjectMetadataMaps } from 'src/engine/metadata-modules/utils/generate-object-metadata-maps.util'; import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; @@ -49,7 +50,8 @@ export class SearchService { ) {} async getAllRecordsWithObjectMetadataItems({ - objectMetadataItemWithFieldMaps, + flatObjectMetadatas, + flatFieldMetadataMaps, includedObjectNameSingulars, excludedObjectNameSingulars, searchInput, @@ -57,10 +59,11 @@ export class SearchService { filter, after, }: { - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps[]; + flatObjectMetadatas: FlatObjectMetadata[]; + flatFieldMetadataMaps: FlatEntityMaps; } & SearchArgs) { const filteredObjectMetadataItems = this.filterObjectMetadataItems({ - objectMetadataItemWithFieldMaps, + flatObjectMetadatas, includedObjectNameSingulars: includedObjectNameSingulars ?? [], excludedObjectNameSingulars: excludedObjectNameSingulars ?? [], }); @@ -75,17 +78,18 @@ export class SearchService { for (const objectMetadataItemChunk of filteredObjectMetadataItemsChunks) { const recordsWithObjectMetadataItems = await Promise.all( - objectMetadataItemChunk.map(async (objectMetadataItem) => { + objectMetadataItemChunk.map(async (flatObjectMetadata) => { const repository = await this.twentyORMManager.getRepository( - objectMetadataItem.nameSingular, + flatObjectMetadata.nameSingular, ); return { - objectMetadataItem, + objectMetadataItem: flatObjectMetadata, records: await this.buildSearchQueryAndGetRecords({ entityManager: repository, - objectMetadataItem, + flatObjectMetadata, + flatFieldMetadataMaps, searchTerms: formatSearchTerms(searchInput, 'and'), searchTermsOr: formatSearchTerms(searchInput, 'or'), limit: limit as number, @@ -103,34 +107,33 @@ export class SearchService { } filterObjectMetadataItems({ - objectMetadataItemWithFieldMaps, + flatObjectMetadatas, includedObjectNameSingulars, excludedObjectNameSingulars, }: { - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps[]; + flatObjectMetadatas: FlatObjectMetadata[]; includedObjectNameSingulars: string[]; excludedObjectNameSingulars: string[]; }) { - return objectMetadataItemWithFieldMaps.filter( - ({ nameSingular, isSearchable }) => { - if (!isSearchable) { - return false; - } - if (excludedObjectNameSingulars.includes(nameSingular)) { - return false; - } - if (includedObjectNameSingulars.length > 0) { - return includedObjectNameSingulars.includes(nameSingular); - } + return flatObjectMetadatas.filter(({ nameSingular, isSearchable }) => { + if (!isSearchable) { + return false; + } + if (excludedObjectNameSingulars.includes(nameSingular)) { + return false; + } + if (includedObjectNameSingulars.length > 0) { + return includedObjectNameSingulars.includes(nameSingular); + } - return true; - }, - ); + return true; + }); } async buildSearchQueryAndGetRecords({ entityManager, - objectMetadataItem, + flatObjectMetadata, + flatFieldMetadataMaps, searchTerms, searchTermsOr, limit, @@ -138,7 +141,8 @@ export class SearchService { after, }: { entityManager: WorkspaceRepository; - objectMetadataItem: ObjectMetadataItemWithFieldMaps; + flatObjectMetadata: FlatObjectMetadata; + flatFieldMetadataMaps: FlatEntityMaps; searchTerms: string; searchTermsOr: string; limit: number; @@ -147,30 +151,33 @@ export class SearchService { }) { const queryBuilder = entityManager.createQueryBuilder(); + const { flatObjectMetadataMaps } = entityManager.internalContext; + const queryParser = new GraphqlQueryParser( - objectMetadataItem, - generateObjectMetadataMaps([ - { - ...objectMetadataItem, - fields: Object.values(objectMetadataItem.fieldsById), - }, - ]), + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, ); queryParser.applyFilterToBuilder( queryBuilder, - objectMetadataItem.nameSingular, + flatObjectMetadata.nameSingular, filter, ); queryParser.applyDeletedAtToBuilder(queryBuilder, filter); - const imageIdentifierField = - this.getImageIdentifierColumn(objectMetadataItem); + const imageIdentifierField = this.getImageIdentifierColumn( + flatObjectMetadata, + flatFieldMetadataMaps, + ); const fieldsToSelect = [ 'id', - ...this.getLabelIdentifierColumns(objectMetadataItem), + ...this.getLabelIdentifierColumns( + flatObjectMetadata, + flatFieldMetadataMaps, + ), ...(imageIdentifierField ? [imageIdentifierField] : []), ].map((field) => `"${field}"`); @@ -180,7 +187,7 @@ export class SearchService { const cursorWhereCondition = this.computeCursorWhereCondition({ after, - objectMetadataNameSingular: objectMetadataItem.nameSingular, + objectMetadataNameSingular: flatObjectMetadata.nameSingular, tsRankExpr, tsRankCDExpr, }); @@ -273,9 +280,10 @@ export class SearchService { } getLabelIdentifierColumns( - objectMetadataItem: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, ) { - if (!objectMetadataItem.labelIdentifierFieldMetadataId) { + if (!flatObjectMetadata.labelIdentifierFieldMetadataId) { throw new SearchException( 'Label identifier field not found', SearchExceptionCode.LABEL_IDENTIFIER_FIELD_NOT_FOUND, @@ -283,10 +291,17 @@ export class SearchService { } const labelIdentifierField = - objectMetadataItem.fieldsById[ - objectMetadataItem.labelIdentifierFieldMetadataId + flatFieldMetadataMaps.byId[ + flatObjectMetadata.labelIdentifierFieldMetadataId ]; + if (!isDefined(labelIdentifierField)) { + throw new SearchException( + 'Label identifier field not found', + SearchExceptionCode.LABEL_IDENTIFIER_FIELD_NOT_FOUND, + ); + } + if (labelIdentifierField.type === FieldMetadataType.FULL_NAME) { return [ `${labelIdentifierField.name}FirstName`, @@ -294,37 +309,44 @@ export class SearchService { ]; } - return [ - objectMetadataItem.fieldsById[ - objectMetadataItem.labelIdentifierFieldMetadataId - ].name, - ]; + return [labelIdentifierField.name]; } getLabelIdentifierValue( record: ObjectRecord, - objectMetadataItem: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, ): string { - const labelIdentifierFields = - this.getLabelIdentifierColumns(objectMetadataItem); + const labelIdentifierFields = this.getLabelIdentifierColumns( + flatObjectMetadata, + flatFieldMetadataMaps, + ); return labelIdentifierFields.map((field) => record[field]).join(' '); } getImageIdentifierColumn( - objectMetadataItem: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, ) { - if (objectMetadataItem.nameSingular === 'company') { + if (flatObjectMetadata.nameSingular === 'company') { return 'domainNamePrimaryLinkUrl'; } - if (!objectMetadataItem.imageIdentifierFieldMetadataId) { + if (!flatObjectMetadata.imageIdentifierFieldMetadataId) { return null; } - return objectMetadataItem.fieldsById[ - objectMetadataItem.imageIdentifierFieldMetadataId - ].name; + const imageIdentifierField = + flatFieldMetadataMaps.byId[ + flatObjectMetadata.imageIdentifierFieldMetadataId + ]; + + if (!isDefined(imageIdentifierField)) { + return null; + } + + return imageIdentifierField.name; } private getImageUrlWithToken(avatarUrl: string, workspaceId: string): string { @@ -336,13 +358,16 @@ export class SearchService { getImageIdentifierValue( record: ObjectRecord, - objectMetadataItem: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, workspaceId: string, ): string { - const imageIdentifierField = - this.getImageIdentifierColumn(objectMetadataItem); + const imageIdentifierField = this.getImageIdentifierColumn( + flatObjectMetadata, + flatFieldMetadataMaps, + ); - if (objectMetadataItem.nameSingular === 'company') { + if (flatObjectMetadata.nameSingular === 'company') { return getLogoUrlFromDomainName(record.domainNamePrimaryLinkUrl) || ''; } @@ -391,11 +416,13 @@ export class SearchService { computeSearchObjectResults({ recordsWithObjectMetadataItems, + flatFieldMetadataMaps, workspaceId, limit, after, }: { recordsWithObjectMetadataItems: RecordsWithObjectMetadataItem[]; + flatFieldMetadataMaps: FlatEntityMaps; workspaceId: string; limit: number; after?: string; @@ -406,10 +433,15 @@ export class SearchService { return { recordId: record.id, objectNameSingular: objectMetadataItem.nameSingular, - label: this.getLabelIdentifierValue(record, objectMetadataItem), + label: this.getLabelIdentifierValue( + record, + objectMetadataItem, + flatFieldMetadataMaps, + ), imageUrl: this.getImageIdentifierValue( record, objectMetadataItem, + flatFieldMetadataMaps, workspaceId, ), tsRankCD: record.tsRankCD, diff --git a/packages/twenty-server/src/engine/core-modules/search/types/records-with-object-metadata-item.ts b/packages/twenty-server/src/engine/core-modules/search/types/records-with-object-metadata-item.ts index f9beee15ea1..030c52398a6 100644 --- a/packages/twenty-server/src/engine/core-modules/search/types/records-with-object-metadata-item.ts +++ b/packages/twenty-server/src/engine/core-modules/search/types/records-with-object-metadata-item.ts @@ -1,8 +1,8 @@ import { type ObjectRecord } from 'twenty-shared/types'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; export type RecordsWithObjectMetadataItem = { - objectMetadataItem: ObjectMetadataItemWithFieldMaps; + objectMetadataItem: FlatObjectMetadata; records: ObjectRecord[]; }; diff --git a/packages/twenty-server/src/engine/core-modules/webhook/utils/__tests__/transform-event-batch-to-webhook-events.spec.ts b/packages/twenty-server/src/engine/core-modules/webhook/utils/__tests__/transform-event-batch-to-webhook-events.spec.ts index 1a28b2ae444..ea8f40ed810 100644 --- a/packages/twenty-server/src/engine/core-modules/webhook/utils/__tests__/transform-event-batch-to-webhook-events.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/webhook/utils/__tests__/transform-event-batch-to-webhook-events.spec.ts @@ -2,18 +2,31 @@ import { type WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/typ import type { ObjectRecordEvent } from 'src/engine/core-modules/event-emitter/types/object-record-event.event'; import type { WebhookEntity } from 'src/engine/core-modules/webhook/webhook.entity'; import { transformEventBatchToWebhookEvents } from 'src/engine/core-modules/webhook/utils/transform-event-batch-to-webhook-events'; -import { getMockObjectMetadataEntity } from 'src/utils/__test__/get-object-metadata-entity.mock'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; -const mockObjectMetadata = getMockObjectMetadataEntity({ +const mockObjectMetadata: FlatObjectMetadata = { id: 'id', nameSingular: 'nameSingular', namePlural: 'namePlural', workspaceId: 'workspaceId', -}); + labelSingular: 'Label Singular', + labelPlural: 'Label Plural', + isCustom: false, + isRemote: false, + isActive: true, + isSystem: false, + createdAt: new Date(), + updatedAt: new Date(), + universalIdentifier: 'id', + fieldMetadataIds: [], + indexMetadataIds: [], + viewIds: [], + applicationId: null, +} as unknown as FlatObjectMetadata; describe('transformEventBatchToWebhookEvents', () => { it('should transform properly', () => { - const workspaceEventBatch = { + const workspaceEventBatch: WorkspaceEventBatch = { workspaceId: 'workspaceId', objectMetadata: mockObjectMetadata, name: 'objectNameSingular.created', @@ -48,7 +61,7 @@ describe('transformEventBatchToWebhookEvents', () => { }, }, ], - } as WorkspaceEventBatch; + }; const webhooks = [ { @@ -174,7 +187,7 @@ describe('transformEventBatchToWebhookEvents', () => { }); it('should sanitize records properly', () => { - const workspaceEventBatch = { + const workspaceEventBatch: WorkspaceEventBatch = { workspaceId: 'workspaceId', objectMetadata: mockObjectMetadata, name: 'webhook.created', @@ -190,7 +203,7 @@ describe('transformEventBatchToWebhookEvents', () => { }, }, ], - } as WorkspaceEventBatch; + }; const webhooks = [ { diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/services/agent-execution.service.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/services/agent-execution.service.ts index 0c692c4ba20..0174324f242 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/services/agent-execution.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/services/agent-execution.service.ts @@ -28,13 +28,12 @@ import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/ag import { AgentActorContextService } from 'src/engine/metadata-modules/ai/ai-agent/services/agent-actor-context.service'; import { AgentModelConfigService } from 'src/engine/metadata-modules/ai/ai-agent/services/agent-model-config.service'; import { AgentToolGeneratorService } from 'src/engine/metadata-modules/ai/ai-agent/services/agent-tool-generator.service'; -import { type RecordIdsByObjectMetadataNameSingularType } from 'src/engine/metadata-modules/ai/ai-agent/types/recordIdsByObjectMetadataNameSingular.type'; +import { RecordIdsByObjectMetadataNameSingularType } from 'src/engine/metadata-modules/ai/ai-agent/types/recordIdsByObjectMetadataNameSingular.type'; import { repairToolCall } from 'src/engine/metadata-modules/ai/ai-agent/utils/repair-tool-call.util'; import { AIBillingService } from 'src/engine/metadata-modules/ai/ai-billing/services/ai-billing.service'; -import { type ToolHints } from 'src/engine/metadata-modules/ai/ai-chat-router/types/tool-hints.interface'; +import { ToolHints } from 'src/engine/metadata-modules/ai/ai-chat-router/types/tool-hints.interface'; import { AI_TELEMETRY_CONFIG } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-telemetry.const'; import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service'; -import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util'; import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; @@ -188,8 +187,12 @@ export class AgentExecutionService { workspaceId: workspace.id, }); - const objectMetadataMaps = - workspaceDataSource.internalContext.objectMetadataMaps; + const flatObjectMetadataMaps = + workspaceDataSource.internalContext.flatObjectMetadataMaps; + const flatFieldMetadataMaps = + workspaceDataSource.internalContext.flatFieldMetadataMaps; + const objectIdByNameSingular = + workspaceDataSource.internalContext.objectIdByNameSingular; const objectMetadataPermissions = workspaceDataSource.permissionsPerRoleId; const contextObject = ( @@ -200,11 +203,13 @@ export class AgentExecutionService { return []; } - const objectMetadataMapItem = - getObjectMetadataMapItemByNameSingular( - objectMetadataMaps, - recordsWithObjectMetadataNameSingular.objectMetadataNameSingular, - ); + const objectMetadataId = + objectIdByNameSingular[ + recordsWithObjectMetadataNameSingular.objectMetadataNameSingular + ]; + const objectMetadataMapItem = objectMetadataId + ? flatObjectMetadataMaps.byId[objectMetadataId] + : undefined; if (!objectMetadataMapItem) { this.logger.warn( @@ -230,7 +235,10 @@ export class AgentExecutionService { const selectOptions = hasRestrictedFields ? getAllSelectableColumnNames({ restrictedFields, - objectMetadata: { objectMetadataMapItem }, + objectMetadata: { + objectMetadataMapItem, + flatFieldMetadataMaps, + }, }) : undefined; diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/utils/is-workflow-related-object.util.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/utils/is-workflow-related-object.util.ts index 9cdc361f41b..703f06e7f60 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/utils/is-workflow-related-object.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/utils/is-workflow-related-object.util.ts @@ -1,7 +1,5 @@ import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; - // All workflow-related standard object IDs that should be filtered out from agent access const WORKFLOW_STANDARD_OBJECT_IDS = [ STANDARD_OBJECT_IDS.workflow, @@ -10,9 +8,9 @@ const WORKFLOW_STANDARD_OBJECT_IDS = [ STANDARD_OBJECT_IDS.workflowAutomatedTrigger, ] as const; -export const isWorkflowRelatedObject = ( - objectMetadata: ObjectMetadataEntity, -): boolean => { +export const isWorkflowRelatedObject = (objectMetadata: { + standardId: string | null; +}): boolean => { return ( objectMetadata.standardId !== null && WORKFLOW_STANDARD_OBJECT_IDS.includes( diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-tools/ai-tools.module.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-tools/ai-tools.module.ts index 9601c4075a3..429f846c97c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-tools/ai-tools.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-tools/ai-tools.module.ts @@ -10,7 +10,7 @@ import { ToolModule } from 'src/engine/core-modules/tool/tool.module'; import { SearchArticlesTool } from 'src/engine/core-modules/tool/tools/search-articles-tool/search-articles-tool'; import { ToolAdapterService } from 'src/engine/metadata-modules/ai/ai-tools/services/tool-adapter.service'; import { ToolService } from 'src/engine/metadata-modules/ai/ai-tools/services/tool.service'; -import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module'; +import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module'; import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module'; @@ -27,7 +27,7 @@ import { MessagingModule } from 'src/modules/messaging/messaging.module'; TokenModule, FeatureFlagModule, RecordCrudModule, - ObjectMetadataModule, + WorkspaceManyOrAllFlatEntityMapsCacheModule, WorkspacePermissionsCacheModule, WorkspaceCacheStorageModule, UserRoleModule, diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-tools/services/__tests__/tool.service.spec.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-tools/services/__tests__/tool.service.spec.ts index 9f3914a2535..8aa0ebe589b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-tools/services/__tests__/tool.service.spec.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-tools/services/__tests__/tool.service.spec.ts @@ -6,7 +6,7 @@ import { FindRecordsService } from 'src/engine/core-modules/record-crud/services import { UpdateRecordService } from 'src/engine/core-modules/record-crud/services/update-record.service'; import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service'; import { ToolService } from 'src/engine/metadata-modules/ai/ai-tools/services/tool.service'; -import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service'; +import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; @@ -57,9 +57,21 @@ describe('ToolService', () => { }, }, { - provide: ObjectMetadataServiceV2, + provide: WorkspaceManyOrAllFlatEntityMapsCacheService, useValue: { - findManyWithinWorkspace: jest.fn().mockResolvedValue([testObject]), + getOrRecomputeManyOrAllFlatEntityMaps: jest.fn().mockResolvedValue({ + flatObjectMetadataMaps: { + byId: { + [testObject.id]: { + ...testObject, + fieldMetadataIds: [], + }, + }, + }, + flatFieldMetadataMaps: { + byId: {}, + }, + }), }, }, { diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-tools/services/tool.service.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-tools/services/tool.service.ts index 2de7758f15a..04c346af3ed 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-tools/services/tool.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-tools/services/tool.service.ts @@ -2,7 +2,9 @@ import { Injectable, Logger } from '@nestjs/common'; import { type ToolSet } from 'ai'; import { type ActorMetadata } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; +import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util'; import { CreateRecordService } from 'src/engine/core-modules/record-crud/services/create-record.service'; import { DeleteRecordService } from 'src/engine/core-modules/record-crud/services/delete-record.service'; import { FindRecordsService } from 'src/engine/core-modules/record-crud/services/find-records.service'; @@ -15,13 +17,13 @@ import { generateFindToolInputSchema } from 'src/engine/core-modules/record-crud import { SoftDeleteToolInputSchema } from 'src/engine/core-modules/record-crud/zod-schemas/soft-delete-tool.zod-schema'; import { isWorkflowRelatedObject } from 'src/engine/metadata-modules/ai/ai-agent/utils/is-workflow-related-object.util'; import { - type ToolHints, - type ToolOperation, + ToolHints, + ToolOperation, } from 'src/engine/metadata-modules/ai/ai-chat-router/types/tool-hints.interface'; -import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service'; +import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; -import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; +import { RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; import { computePermissionIntersection } from 'src/engine/twenty-orm/utils/compute-permission-intersection.util'; @Injectable() @@ -30,8 +32,8 @@ export class ToolService { constructor( private readonly twentyORMGlobalManager: TwentyORMGlobalManager, - private readonly objectMetadataService: ObjectMetadataServiceV2, protected readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService, + private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, private readonly createRecordService: CreateRecordService, private readonly updateRecordService: UpdateRecordService, private readonly deleteRecordService: DeleteRecordService, @@ -78,14 +80,25 @@ export class ToolService { return tools; } - const allObjectMetadata = - await this.objectMetadataService.findManyWithinWorkspace(workspaceId, { - where: { - isActive: true, - isSystem: false, + const { flatObjectMetadataMaps, flatFieldMetadataMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatObjectMetadataMaps', 'flatFieldMetadataMaps'], }, - relations: ['fields'], - }); + ); + + const allFlatObjects = Object.values(flatObjectMetadataMaps.byId) + .filter(isDefined) + .filter((obj) => obj.isActive && !obj.isSystem); + + const allObjectMetadata = allFlatObjects.map((flatObject) => ({ + ...flatObject, + fields: getFlatFieldsFromFlatObjectMetadata( + flatObject, + flatFieldMetadataMaps, + ), + })); let filteredObjectMetadata = allObjectMetadata.filter( (objectMetadata) => !isWorkflowRelatedObject(objectMetadata), diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-morph-relation.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-morph-relation.service.ts index 45701c2d02f..e4a49683cc9 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-morph-relation.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-morph-relation.service.ts @@ -17,8 +17,9 @@ import { import { FieldMetadataRelationService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-relation.service'; import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util'; import { prepareCustomFieldMetadataForCreation } from 'src/engine/metadata-modules/field-metadata/utils/prepare-field-metadata-for-creation.util'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { computeMetadataNameFromLabel } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util'; @Injectable() @@ -30,15 +31,17 @@ export class FieldMetadataMorphRelationService { async createMorphRelationFieldMetadataItems({ fieldMetadataForCreate, morphRelationsCreationPayload, - objectMetadata, + flatObjectMetadata, fieldMetadataRepository, - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }: { fieldMetadataForCreate: CreateFieldInput; morphRelationsCreationPayload: CreateFieldInput['morphRelationsCreationPayload']; - objectMetadata: ObjectMetadataItemWithFieldMaps; + flatObjectMetadata: FlatObjectMetadata; fieldMetadataRepository: Repository; - objectMetadataMaps: ObjectMetadataMaps; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; }): Promise { if ( !isDefined(morphRelationsCreationPayload) || @@ -62,7 +65,9 @@ export class FieldMetadataMorphRelationService { for (const relationCreationPayload of morphRelationsCreationPayload) { const targetObjectMetadata = - objectMetadataMaps.byId[relationCreationPayload.targetObjectMetadataId]; + flatObjectMetadataMaps.byId[ + relationCreationPayload.targetObjectMetadataId + ]; if (!isDefined(targetObjectMetadata)) { throw new FieldMetadataException( @@ -95,8 +100,9 @@ export class FieldMetadataMorphRelationService { { fieldMetadataInput: relationFieldMetadataForCreate, fieldMetadataType: relationFieldMetadataForCreate.type, - objectMetadataMaps, - objectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatObjectMetadata, }, ); @@ -126,7 +132,7 @@ export class FieldMetadataMorphRelationService { { fieldMetadataInput: targetFieldMetadataToCreate, relationCreationPayload: { - targetObjectMetadataId: objectMetadata.id, + targetObjectMetadataId: flatObjectMetadata.id, targetFieldLabel: fieldMetadataForCreate.label, targetFieldIcon: fieldMetadataForCreate.icon ?? 'Icon123', type: diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-relation.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-relation.service.ts index 5f166734179..817ead6e5b0 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-relation.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-relation.service.ts @@ -7,9 +7,9 @@ import { FieldMetadataType, RelationOnDeleteAction, } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; import { type Repository } from 'typeorm'; import { v4 } from 'uuid'; -import { isDefined } from 'twenty-shared/utils'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; @@ -23,8 +23,9 @@ import { import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util'; import { prepareCustomFieldMetadataForCreation } from 'src/engine/metadata-modules/field-metadata/utils/prepare-field-metadata-for-creation.util'; import { validateRelationCreationPayloadOrThrow } from 'src/engine/metadata-modules/field-metadata/utils/validate-relation-creation-payload-or-throw.util'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { validateFieldNameAvailabilityOrThrow } from 'src/engine/metadata-modules/utils/validate-field-name-availability.utils'; import { validateMetadataNameOrThrow } from 'src/engine/metadata-modules/utils/validate-metadata-name-or-throw.utils'; import { computeMetadataNameFromLabel } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util'; @@ -49,9 +50,10 @@ type ValidateFieldMetadataArgs = { fieldMetadataType: FieldMetadataType; fieldMetadataInput: T; - objectMetadata: ObjectMetadataItemWithFieldMaps; + flatObjectMetadata: FlatObjectMetadata; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; existingFieldMetadata?: FieldMetadataEntity; - objectMetadataMaps: ObjectMetadataMaps; }; @Injectable() @@ -62,11 +64,11 @@ export class FieldMetadataRelationService { async createRelationFieldMetadataItems({ fieldMetadataInput, - objectMetadata, + flatObjectMetadata, fieldMetadataRepository, }: { fieldMetadataInput: CreateFieldInput; - objectMetadata: ObjectMetadataItemWithFieldMaps; + flatObjectMetadata: FlatObjectMetadata; fieldMetadataRepository: Repository; }): Promise { const createdFieldMetadataItem = @@ -98,7 +100,7 @@ export class FieldMetadataRelationService { this.computeCustomRelationFieldMetadataForCreation({ fieldMetadataInput: targetFieldMetadataToCreate, relationCreationPayload: { - targetObjectMetadataId: objectMetadata.id, + targetObjectMetadataId: flatObjectMetadata.id, targetFieldLabel: fieldMetadataInput.label, targetFieldIcon: fieldMetadataInput.icon ?? 'Icon123', type: @@ -135,14 +137,16 @@ export class FieldMetadataRelationService { >({ fieldMetadataInput, fieldMetadataType, - objectMetadataMaps, - objectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatObjectMetadata, }: Pick< ValidateFieldMetadataArgs, | 'fieldMetadataInput' | 'fieldMetadataType' - | 'objectMetadataMaps' - | 'objectMetadata' + | 'flatObjectMetadataMaps' + | 'flatFieldMetadataMaps' + | 'flatObjectMetadata' >): Promise { // TODO: clean typings, we should try to validate both update and create inputs in the same function const isRelation = @@ -158,7 +162,8 @@ export class FieldMetadataRelationService { ) { validateFieldNameAvailabilityOrThrow({ name: `${fieldMetadataInput.name}Id`, - fieldMetadataMapById: objectMetadata.fieldsById, + flatObjectMetadata, + flatFieldMetadataMaps, }); const relationCreationPayload = ( @@ -173,12 +178,12 @@ export class FieldMetadataRelationService { validateMetadataNameOrThrow(computedMetadataNameFromLabel); - const objectMetadataTarget = - objectMetadataMaps.byId[ + const targetFlatObjectMetadata = + flatObjectMetadataMaps.byId[ relationCreationPayload.targetObjectMetadataId ]; - if (!isDefined(objectMetadataTarget)) { + if (!isDefined(targetFlatObjectMetadata)) { throw new FieldMetadataException( `Object metadata relation target not found for relation creation payload`, FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED, @@ -187,17 +192,19 @@ export class FieldMetadataRelationService { validateFieldNameAvailabilityOrThrow({ name: computedMetadataNameFromLabel, - fieldMetadataMapById: objectMetadataTarget.fieldsById, + flatObjectMetadata: targetFlatObjectMetadata, + flatFieldMetadataMaps, }); validateFieldNameAvailabilityOrThrow({ name: `${computedMetadataNameFromLabel}Id`, - fieldMetadataMapById: objectMetadataTarget.fieldsById, + flatObjectMetadata: targetFlatObjectMetadata, + flatFieldMetadataMaps, }); if ( computedMetadataNameFromLabel === fieldMetadataInput.name && - objectMetadata.id === objectMetadataTarget.id + flatObjectMetadata.id === targetFlatObjectMetadata.id ) { throw new FieldMetadataException( `Name "${computedMetadataNameFromLabel}" cannot be the same on both side of the relation`, diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-validation.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-validation.service.ts index 8b466787ca7..d279c101438 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-validation.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-validation.service.ts @@ -30,7 +30,9 @@ import { } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; import { FieldMetadataEnumValidationService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-enum-validation.service'; import { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { InvalidMetadataException } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception'; import { validateFieldNameAvailabilityOrThrow } from 'src/engine/metadata-modules/utils/validate-field-name-availability.utils'; import { validateMetadataNameOrThrow } from 'src/engine/metadata-modules/utils/validate-metadata-name-or-throw.utils'; @@ -38,7 +40,8 @@ import { validateMetadataNameOrThrow } from 'src/engine/metadata-modules/utils/v type ValidateFieldMetadataArgs = { fieldMetadataType: FieldMetadataType; fieldMetadataInput: CreateFieldInput | UpdateFieldInput; - objectMetadata: ObjectMetadataItemWithFieldMaps; + flatObjectMetadata: FlatObjectMetadata; + flatFieldMetadataMaps: FlatEntityMaps; existingFieldMetadata?: FieldMetadataEntity; }; @@ -164,7 +167,8 @@ export class FieldMetadataValidationService { async validateFieldMetadata({ fieldMetadataInput, fieldMetadataType, - objectMetadata, + flatObjectMetadata, + flatFieldMetadataMaps, existingFieldMetadata, }: ValidateFieldMetadataArgs): Promise { if (fieldMetadataInput.name) { @@ -187,7 +191,8 @@ export class FieldMetadataValidationService { try { validateFieldNameAvailabilityOrThrow({ name: fieldMetadataInput.name, - fieldMetadataMapById: objectMetadata.fieldsById, + flatObjectMetadata, + flatFieldMetadataMaps, }); } catch (error) { if (error instanceof InvalidMetadataException) { diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/compute-column-name.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/compute-column-name.util.ts index 599b17813ae..b8a107f94be 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/compute-column-name.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/compute-column-name.util.ts @@ -3,7 +3,6 @@ import { type CompositeProperty, } from 'twenty-shared/types'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataException, FieldMetadataExceptionCode, @@ -19,8 +18,8 @@ export type FieldTypeAndNameMetadata = { }; // TODO: If we need to implement custom name logic for columns, we can do it here -export function computeColumnName( - fieldMetadataOrFieldName: FieldMetadataEntity | string, +export function computeColumnName( + fieldMetadataOrFieldName: FieldTypeAndNameMetadata | string, options?: ComputeColumnNameOptions, ): string { const generateName = (name: string) => { @@ -40,11 +39,9 @@ export function computeColumnName( return generateName(fieldMetadataOrFieldName.name); } -export function computeCompositeColumnName( - fieldMetadataOrFieldName: - | FieldTypeAndNameMetadata - | FieldMetadataEntity - | string, + +export function computeCompositeColumnName( + fieldMetadataOrFieldName: FieldTypeAndNameMetadata | string, compositeProperty: CompositeProperty, ): string { const generateName = (name: string) => { diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/create-migration-actions.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/create-migration-actions.util.ts deleted file mode 100644 index a2e65ff266f..00000000000 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/create-migration-actions.util.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { FieldMetadataType } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; - -import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; - -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { - FieldMetadataException, - FieldMetadataExceptionCode, -} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; -import { - WorkspaceMigrationColumnActionType, - type WorkspaceMigrationTableAction, - WorkspaceMigrationTableActionType, -} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; -import { type WorkspaceMigrationFactory } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.factory'; -import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; -import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; - -export const createMigrationActions = async ({ - createdFieldMetadataItems, - objectMetadataMap, - isRemoteCreation, - workspaceMigrationFactory, -}: { - createdFieldMetadataItems: FieldMetadataEntity[]; - objectMetadataMap: ObjectMetadataMaps['byId']; - isRemoteCreation: boolean; - workspaceMigrationFactory: WorkspaceMigrationFactory; -}): Promise => { - if (isRemoteCreation) { - return []; - } - - const migrationActions: WorkspaceMigrationTableAction[] = []; - - for (const createdFieldMetadata of createdFieldMetadataItems) { - if ( - isFieldMetadataEntityOfType( - createdFieldMetadata, - FieldMetadataType.RELATION, - ) || - isFieldMetadataEntityOfType( - createdFieldMetadata, - FieldMetadataType.MORPH_RELATION, - ) - ) { - const relationType = createdFieldMetadata.settings?.relationType; - - if (relationType === RelationType.ONE_TO_MANY) { - continue; - } - } - - const objectMetadata = - objectMetadataMap[createdFieldMetadata.objectMetadataId]; - - if (!isDefined(objectMetadata)) { - throw new FieldMetadataException( - 'Object metadata does not exist', - FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND, - ); - } - - migrationActions.push({ - name: computeObjectTargetTable(objectMetadata), - action: WorkspaceMigrationTableActionType.ALTER, - columns: workspaceMigrationFactory.createColumnActions( - WorkspaceMigrationColumnActionType.CREATE, - createdFieldMetadata, - ), - }); - } - - return migrationActions; -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/should-exclude-field-from-agent-tool-schema.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/should-exclude-field-from-agent-tool-schema.util.ts index 91cf05658ec..a6fe4a32970 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/should-exclude-field-from-agent-tool-schema.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/should-exclude-field-from-agent-tool-schema.util.ts @@ -1,7 +1,8 @@ import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; export const shouldExcludeFieldFromAgentToolSchema = ( - field: FieldMetadataEntity, + field: FieldMetadataEntity | FlatFieldMetadata, excludeId = true, ): boolean => { const excludedFieldNames = [ diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-for-object.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util.ts similarity index 61% rename from packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-for-object.util.ts rename to packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util.ts index 9962cdde57e..a9458cbad50 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-for-object.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util.ts @@ -1,31 +1,28 @@ -import { isDefined } from 'twenty-shared/utils'; - +import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util'; import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; export type FieldMapsForObject = { fieldIdByName: Record; fieldIdByJoinColumnName: Record; }; -export const buildFieldMapsForObject = ( +export const buildFieldMapsFromFlatObjectMetadata = ( flatFieldMetadataMaps: FlatEntityMaps, - objectMetadataId: string, + flatObjectMetadata: FlatObjectMetadata, ): FieldMapsForObject => { const fieldIdByName: Record = {}; const fieldIdByJoinColumnName: Record = {}; - for (const [fieldId, field] of Object.entries(flatFieldMetadataMaps.byId)) { - if (!isDefined(field)) { - continue; - } + const objectFields = getFlatFieldsFromFlatObjectMetadata( + flatObjectMetadata, + flatFieldMetadataMaps, + ); - if (field.objectMetadataId !== objectMetadataId) { - continue; - } - - fieldIdByName[field.name] = fieldId; + for (const field of objectFields) { + fieldIdByName[field.name] = field.id; if (isMorphOrRelationFlatFieldMetadata(field)) { const joinColumnName = ( @@ -33,7 +30,7 @@ export const buildFieldMapsForObject = ( )?.joinColumnName; if (joinColumnName) { - fieldIdByJoinColumnName[joinColumnName] = fieldId; + fieldIdByJoinColumnName[joinColumnName] = field.id; } } } diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/build-object-metadata-item-with-field-maps.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/build-object-metadata-item-with-field-maps.util.ts deleted file mode 100644 index fc6dfd7ca92..00000000000 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/build-object-metadata-item-with-field-maps.util.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { isDefined } from 'twenty-shared/utils'; - -import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; -import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; -import { buildFieldMapsForObject } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-for-object.util'; -import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type'; -import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; - -export const buildObjectMetadataItemWithFieldMaps = ( - flatObjectMetadata: FlatObjectMetadata, - flatFieldMetadataMaps: FlatEntityMaps, - flatIndexMaps: FlatEntityMaps, -): ObjectMetadataItemWithFieldMaps => { - const fieldsById: Record = {}; - - for (const fieldId of flatObjectMetadata.fieldMetadataIds) { - const field = flatFieldMetadataMaps.byId[fieldId]; - - if (isDefined(field)) { - fieldsById[fieldId] = field; - } - } - - const { fieldIdByName, fieldIdByJoinColumnName } = buildFieldMapsForObject( - flatFieldMetadataMaps, - flatObjectMetadata.id, - ); - - const indexMetadatas = flatObjectMetadata.indexMetadataIds - .map((indexId) => flatIndexMaps.byId[indexId]) - .filter(isDefined) - .map((flatIndex) => ({ - ...flatIndex, - indexFieldMetadatas: flatIndex.flatIndexFieldMetadatas, - })); - - return { - ...flatObjectMetadata, - fieldsById, - fieldIdByName, - fieldIdByJoinColumnName, - indexMetadatas, - } as unknown as ObjectMetadataItemWithFieldMaps; -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata-v2.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata-v2.service.ts index 805d3667980..8c3d3dc6009 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata-v2.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata-v2.service.ts @@ -599,13 +599,8 @@ export class ObjectMetadataServiceV2 extends TypeOrmQueryService, - ) { - return this.objectMetadataRepository.find({ - relations: [ - 'fields.object', - 'fields', - 'fields.relationTargetObjectMetadata', - ], + ): Promise { + const objectMetadataEntities = await this.objectMetadataRepository.find({ ...options, where: { ...options?.where, @@ -614,6 +609,27 @@ export class ObjectMetadataServiceV2 extends TypeOrmQueryService objectMetadata.id, + ); + + const { flatObjectMetadataMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatObjectMetadataMaps'], + }, + ); + + const filteredOutAndSortedFlatObjectMetadataMaps = + findManyFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityMaps: flatObjectMetadataMaps, + flatEntityIds: objectMetadataIds, + }); + + return filteredOutAndSortedFlatObjectMetadataMaps; } } diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-migrations-for-custom-object-relations.util.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-migrations-for-custom-object-relations.util.ts deleted file mode 100644 index 0ab78ae85f1..00000000000 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-migrations-for-custom-object-relations.util.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { RelationOnDeleteAction } from 'twenty-shared/types'; - -import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { - WorkspaceMigrationColumnActionType, - type WorkspaceMigrationColumnCreate, - type WorkspaceMigrationTableAction, - WorkspaceMigrationTableActionType, -} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; -import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; - -export const buildMigrationsForCustomObjectRelations = ( - createdObjectMetadata: Pick< - ObjectMetadataItemWithFieldMaps, - 'nameSingular' | 'isCustom' - >, - relatedObjectMetadataCollection: Pick< - ObjectMetadataItemWithFieldMaps, - 'nameSingular' | 'isCustom' - >[], -): WorkspaceMigrationTableAction[] => { - const migrations: WorkspaceMigrationTableAction[] = []; - - for (const relatedObjectMetadata of relatedObjectMetadataCollection) { - migrations.push( - { - name: computeObjectTargetTable(relatedObjectMetadata), - action: WorkspaceMigrationTableActionType.ALTER, - columns: [ - { - action: WorkspaceMigrationColumnActionType.CREATE, - // TODO: When we get rid of this and use the sync metadata, columnName must be based on the joinColumnName from the field metadata settings - columnName: computeColumnName(createdObjectMetadata.nameSingular, { - isForeignKey: true, - }), - columnType: 'uuid', - isNullable: true, - defaultValue: null, - } satisfies WorkspaceMigrationColumnCreate, - ], - }, - { - name: computeObjectTargetTable(relatedObjectMetadata), - action: WorkspaceMigrationTableActionType.ALTER, - columns: [ - { - action: WorkspaceMigrationColumnActionType.CREATE_FOREIGN_KEY, - // TODO: When we get rid of this and use the sync metadata, columnName must be based on the joinColumnName from the field metadata settings - columnName: computeColumnName(createdObjectMetadata.nameSingular, { - isForeignKey: true, - }), - referencedTableName: computeObjectTargetTable( - createdObjectMetadata, - ), - referencedTableColumnName: 'id', - onDelete: RelationOnDeleteAction.CASCADE, - }, - ], - }, - ); - } - - return migrations; -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/field-permission.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/field-permission.service.ts index df02f811a3e..9165e191df2 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/field-permission.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/field-permission.service.ts @@ -14,9 +14,10 @@ import { } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { isFieldMetadataTypeRelation } from 'src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-relation.util'; -import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; -import { buildObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-metadata-item-with-field-maps.util'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { type UpsertFieldPermissionsInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-field-permissions.input'; import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity'; import { @@ -25,7 +26,6 @@ import { PermissionsExceptionMessage, } from 'src/engine/metadata-modules/permissions/permissions.exception'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; @Injectable() @@ -70,18 +70,6 @@ export class FieldPermissionService { }, ); - const objectMetadataMapsById: ObjectMetadataMaps['byId'] = {}; - - for (const [id, flatObj] of Object.entries(flatObjectMetadataMaps.byId)) { - if (isDefined(flatObj)) { - objectMetadataMapsById[id] = buildObjectMetadataItemWithFieldMaps( - flatObj, - flatFieldMetadataMaps, - createEmptyFlatEntityMaps(), - ); - } - } - const existingFieldPermissions = await this.fieldPermissionsRepository.find( { where: { @@ -97,7 +85,8 @@ export class FieldPermissionService { this.validateFieldPermission({ allFieldPermissions: input.fieldPermissions, fieldPermission, - objectMetadataMapsById, + flatObjectMetadataMaps, + flatFieldMetadataMaps, rolesPermissions, role, }); @@ -193,13 +182,15 @@ export class FieldPermissionService { private validateFieldPermission({ allFieldPermissions, fieldPermission, - objectMetadataMapsById, + flatObjectMetadataMaps, + flatFieldMetadataMaps, rolesPermissions, role, }: { allFieldPermissions: UpsertFieldPermissionsInput['fieldPermissions']; fieldPermission: UpsertFieldPermissionsInput['fieldPermissions'][0]; - objectMetadataMapsById: ObjectMetadataMaps['byId']; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; rolesPermissions: ObjectsPermissionsByRoleId; role: RoleEntity; }) { @@ -230,10 +221,10 @@ export class FieldPermissionService { ); } - const objectMetadataForFieldPermission = - objectMetadataMapsById[fieldPermission.objectMetadataId]; + const flatObjectMetadata = + flatObjectMetadataMaps.byId[fieldPermission.objectMetadataId]; - if (!isDefined(objectMetadataForFieldPermission)) { + if (!isDefined(flatObjectMetadata)) { throw new PermissionsException( PermissionsExceptionMessage.OBJECT_METADATA_NOT_FOUND, PermissionsExceptionCode.OBJECT_METADATA_NOT_FOUND, @@ -243,7 +234,7 @@ export class FieldPermissionService { ); } - if (objectMetadataForFieldPermission.isSystem === true) { + if (flatObjectMetadata.isSystem === true) { throw new PermissionsException( PermissionsExceptionMessage.CANNOT_ADD_FIELD_PERMISSION_ON_SYSTEM_OBJECT, PermissionsExceptionCode.CANNOT_ADD_FIELD_PERMISSION_ON_SYSTEM_OBJECT, @@ -253,12 +244,10 @@ export class FieldPermissionService { ); } - const fieldMetadataForFieldPermission = - objectMetadataForFieldPermission.fieldsById[ - fieldPermission.fieldMetadataId - ]; + const flatFieldMetadata = + flatFieldMetadataMaps.byId[fieldPermission.fieldMetadataId]; - if (!isDefined(fieldMetadataForFieldPermission)) { + if (!isDefined(flatFieldMetadata)) { throw new PermissionsException( PermissionsExceptionMessage.FIELD_METADATA_NOT_FOUND, PermissionsExceptionCode.FIELD_METADATA_NOT_FOUND, diff --git a/packages/twenty-server/src/engine/metadata-modules/types/object-metadata-item-with-field-maps.ts b/packages/twenty-server/src/engine/metadata-modules/types/object-metadata-item-with-field-maps.ts deleted file mode 100644 index 366ddc2ef73..00000000000 --- a/packages/twenty-server/src/engine/metadata-modules/types/object-metadata-item-with-field-maps.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { type IndexMetadataInterface } from 'src/engine/metadata-modules/index-metadata/interfaces/index-metadata.interface'; - -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { type FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; - -export type ObjectMetadataItemWithFieldMaps = Omit< - ObjectMetadataEntity, - 'fields' -> & { - fieldsById: FieldMetadataMap; - fieldIdByJoinColumnName: Record; - fieldIdByName: Record; - indexMetadatas: IndexMetadataInterface[]; -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/types/object-metadata-maps.ts b/packages/twenty-server/src/engine/metadata-modules/types/object-metadata-maps.ts deleted file mode 100644 index da74069001d..00000000000 --- a/packages/twenty-server/src/engine/metadata-modules/types/object-metadata-maps.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; - -export type ObjectMetadataMaps = { - byId: Partial>; - idByNameSingular: Partial>; -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/__tests__/get-object-metadata-map-item-by-name-singular.util.spec.ts b/packages/twenty-server/src/engine/metadata-modules/utils/__tests__/get-object-metadata-map-item-by-name-singular.util.spec.ts deleted file mode 100644 index 9eb51ef8324..00000000000 --- a/packages/twenty-server/src/engine/metadata-modules/utils/__tests__/get-object-metadata-map-item-by-name-singular.util.spec.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; -import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util'; - -describe('getObjectMetadataMapItemByNameSingular', () => { - it('should return the correct metadata item when given a valid singular name', () => { - const mockMetadataItem = { - id: 'test-id', - nameSingular: 'company', - } as ObjectMetadataItemWithFieldMaps; - - const mockObjectMetadataMaps: ObjectMetadataMaps = { - byId: { - 'test-id': mockMetadataItem, - }, - idByNameSingular: { - company: 'test-id', - }, - }; - - const result = getObjectMetadataMapItemByNameSingular( - mockObjectMetadataMaps, - 'company', - ); - - expect(result).toBe(mockMetadataItem); - }); - - it('should return undefined when the singular name does not exist', () => { - const mockObjectMetadataMaps: ObjectMetadataMaps = { - byId: {}, - idByNameSingular: {}, - }; - - const result = getObjectMetadataMapItemByNameSingular( - mockObjectMetadataMaps, - 'nonexistent', - ); - - expect(result).toBeUndefined(); - }); -}); diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/__tests__/validate-field-name-availability.spec.ts b/packages/twenty-server/src/engine/metadata-modules/utils/__tests__/validate-field-name-availability.spec.ts index 5cb9a490093..99f43d24c73 100644 --- a/packages/twenty-server/src/engine/metadata-modules/utils/__tests__/validate-field-name-availability.spec.ts +++ b/packages/twenty-server/src/engine/metadata-modules/utils/__tests__/validate-field-name-availability.spec.ts @@ -1,15 +1,113 @@ +import { FieldMetadataType } from 'twenty-shared/types'; import { type EachTestingContext } from 'twenty-shared/testing'; -import { - FIELD_ACTOR_MOCK_NAME, - FIELD_ADDRESS_MOCK_NAME, - FIELD_CURRENCY_MOCK_NAME, - FIELD_FULL_NAME_MOCK_NAME, - FIELD_LINKS_MOCK_NAME, - objectMetadataMapItemMock, -} from 'src/engine/api/__mocks__/object-metadata-item.mock'; +import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { getFlatFieldMetadataMock } from 'src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock'; +import { getFlatObjectMetadataMock } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/get-flat-object-metadata.mock'; import { validateFieldNameAvailabilityOrThrow } from 'src/engine/metadata-modules/utils/validate-field-name-availability.utils'; +const FIELD_LINKS_MOCK_NAME = 'fieldLinks'; +const FIELD_CURRENCY_MOCK_NAME = 'fieldCurrency'; +const FIELD_ADDRESS_MOCK_NAME = 'fieldAddress'; +const FIELD_ACTOR_MOCK_NAME = 'fieldActor'; +const FIELD_FULL_NAME_MOCK_NAME = 'fieldFullName'; + +const objectMetadataId = '20202020-0000-0000-0000-000000000001'; +const workspaceId = '20202020-0000-0000-0000-000000000000'; + +const createFlatFieldMetadata = ( + id: string, + name: string, + type: FieldMetadataType, +): FlatFieldMetadata => { + return getFlatFieldMetadataMock({ + id, + name, + type, + objectMetadataId, + universalIdentifier: id, + workspaceId, + }); +}; + +const fieldLinksMock = createFlatFieldMetadata( + 'fieldLinksId', + FIELD_LINKS_MOCK_NAME, + FieldMetadataType.LINKS, +); +const fieldCurrencyMock = createFlatFieldMetadata( + 'fieldCurrencyId', + FIELD_CURRENCY_MOCK_NAME, + FieldMetadataType.CURRENCY, +); +const fieldFullNameMock = createFlatFieldMetadata( + 'fieldFullNameId', + FIELD_FULL_NAME_MOCK_NAME, + FieldMetadataType.FULL_NAME, +); +const fieldActorMock = createFlatFieldMetadata( + 'fieldActorId', + FIELD_ACTOR_MOCK_NAME, + FieldMetadataType.ACTOR, +); +const fieldAddressMock = createFlatFieldMetadata( + 'fieldAddressId', + FIELD_ADDRESS_MOCK_NAME, + FieldMetadataType.ADDRESS, +); + +const FIELDS_MOCK = [ + fieldLinksMock, + fieldCurrencyMock, + fieldFullNameMock, + fieldActorMock, + fieldAddressMock, +]; + +const flatFieldMetadataMaps: FlatEntityMaps = + FIELDS_MOCK.reduce( + (acc, field) => ({ + ...acc, + byId: { + ...acc.byId, + [field.id]: field, + }, + }), + createEmptyFlatEntityMaps() as FlatEntityMaps, + ); + +const flatObjectMetadata = getFlatObjectMetadataMock({ + id: objectMetadataId, + workspaceId, + nameSingular: 'objectName', + namePlural: 'objectNames', + labelSingular: 'Object Name', + labelPlural: 'Object Names', + description: 'Object description', + icon: 'Icon123', + isCustom: false, + isRemote: false, + isActive: true, + isSystem: false, + isAuditLogged: true, + isSearchable: true, + fieldMetadataIds: FIELDS_MOCK.map((f) => f.id), + indexMetadataIds: [], + viewIds: [], + universalIdentifier: objectMetadataId, + applicationId: null, + labelIdentifierFieldMetadataId: null, + imageIdentifierFieldMetadataId: null, + shortcut: null, + isLabelSyncedWithName: true, + standardId: null, + standardOverrides: null, + targetTableName: 'DEPRECATED', + duplicateCriteria: null, +}); + type ValidateFieldNameAvailabilityTestContext = EachTestingContext<{ input: string; shouldNotThrow?: true; @@ -64,14 +162,16 @@ describe('validateFieldNameAvailabilityOrThrow', () => { expect(() => validateFieldNameAvailabilityOrThrow({ name: input, - fieldMetadataMapById: objectMetadataMapItemMock.fieldsById, + flatObjectMetadata, + flatFieldMetadataMaps, }), ).not.toThrow(); } else { expect(() => validateFieldNameAvailabilityOrThrow({ name: input, - fieldMetadataMapById: objectMetadataMapItemMock.fieldsById, + flatObjectMetadata, + flatFieldMetadataMaps, }), ).toThrowErrorMatchingSnapshot(); } diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/generate-object-metadata-maps.util.ts b/packages/twenty-server/src/engine/metadata-modules/utils/generate-object-metadata-maps.util.ts deleted file mode 100644 index bae07ae768f..00000000000 --- a/packages/twenty-server/src/engine/metadata-modules/utils/generate-object-metadata-maps.util.ts +++ /dev/null @@ -1,60 +0,0 @@ -import omit from 'lodash.omit'; -import { FieldMetadataType } from 'twenty-shared/types'; - -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { type FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; -import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; - -export const generateObjectMetadataMaps = ( - objectMetadataCollection: ObjectMetadataEntity[], -): ObjectMetadataMaps => { - const objectMetadataMaps: ObjectMetadataMaps = { - byId: {}, - idByNameSingular: {}, - }; - - for (const objectMetadata of objectMetadataCollection) { - const fieldIdByJoinColumnNameMap: Record = {}; - - for (const fieldMetadata of objectMetadata.fields) { - if ( - isFieldMetadataEntityOfType( - fieldMetadata, - FieldMetadataType.RELATION, - ) || - isFieldMetadataEntityOfType( - fieldMetadata, - FieldMetadataType.MORPH_RELATION, - ) - ) { - if (fieldMetadata.settings?.joinColumnName) { - fieldIdByJoinColumnNameMap[fieldMetadata.settings.joinColumnName] = - fieldMetadata.id; - } - } - } - - const fieldsByIdMap = objectMetadata.fields.reduce((acc, field) => { - acc[field.id] = field; - - return acc; - }, {} as FieldMetadataMap); - - const processedObjectMetadata: ObjectMetadataItemWithFieldMaps = { - ...omit(objectMetadata, 'fields'), - fieldsById: fieldsByIdMap, - fieldIdByName: Object.fromEntries( - Object.entries(fieldsByIdMap).map(([id, field]) => [field.name, id]), - ), - fieldIdByJoinColumnName: fieldIdByJoinColumnNameMap, - }; - - objectMetadataMaps.byId[objectMetadata.id] = processedObjectMetadata; - objectMetadataMaps.idByNameSingular[objectMetadata.nameSingular] = - objectMetadata.id; - } - - return objectMetadataMaps; -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/get-object-metadata-entity-from-object-metadata-item-with-fields-map.util.ts b/packages/twenty-server/src/engine/metadata-modules/utils/get-object-metadata-entity-from-object-metadata-item-with-fields-map.util.ts deleted file mode 100644 index a739afe8592..00000000000 --- a/packages/twenty-server/src/engine/metadata-modules/utils/get-object-metadata-entity-from-object-metadata-item-with-fields-map.util.ts +++ /dev/null @@ -1,17 +0,0 @@ -import omit from 'lodash.omit'; - -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; - -export const getObjectMetadataEntityFromObjectMetadataItemWithFieldsMap = ( - objectMetadataItem: ObjectMetadataItemWithFieldMaps, -): ObjectMetadataEntity => { - return { - ...omit(objectMetadataItem, [ - 'fieldsById', - 'fieldIdByName', - 'fieldIdByJoinColumnName', - ]), - fields: Object.values(objectMetadataItem.fieldsById), - }; -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps.ts b/packages/twenty-server/src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps.ts deleted file mode 100644 index 64df5d8bc8e..00000000000 --- a/packages/twenty-server/src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps.ts +++ /dev/null @@ -1,17 +0,0 @@ -import omit from 'lodash.omit'; - -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; - -export const getObjectMetadataFromObjectMetadataItemWithFieldMaps = ( - objectMetadataMapItem: ObjectMetadataItemWithFieldMaps, -): ObjectMetadataEntity => { - return { - ...omit(objectMetadataMapItem, [ - 'fieldsById', - 'fieldIdByName', - 'fieldIdByJoinColumnName', - ]), - fields: Object.values(objectMetadataMapItem.fieldsById), - }; -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-plural.util.ts b/packages/twenty-server/src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-plural.util.ts deleted file mode 100644 index 38bb9c88ce0..00000000000 --- a/packages/twenty-server/src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-plural.util.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { isDefined } from 'twenty-shared/utils'; - -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; - -export const getObjectMetadataMapItemByNamePlural = ( - objectMetadataMaps: ObjectMetadataMaps, - namePlural: string, -): ObjectMetadataItemWithFieldMaps | undefined => { - const objectMetadataItems = Object.values(objectMetadataMaps.byId).filter( - isDefined, - ); - - return objectMetadataItems.find( - (objectMetadata) => objectMetadata.namePlural === namePlural, - ); -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util.ts b/packages/twenty-server/src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util.ts deleted file mode 100644 index 9915b8a4973..00000000000 --- a/packages/twenty-server/src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { isNonEmptyString } from '@sniptt/guards'; - -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; - -export const getObjectMetadataMapItemByNameSingular = ( - objectMetadataMaps: ObjectMetadataMaps, - nameSingular: string, -): ObjectMetadataItemWithFieldMaps | undefined => { - const objectMetadataId = objectMetadataMaps.idByNameSingular[nameSingular]; - - if (!isNonEmptyString(objectMetadataId)) { - return undefined; - } - - return objectMetadataMaps.byId[objectMetadataId]; -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/validate-field-name-availability.utils.ts b/packages/twenty-server/src/engine/metadata-modules/utils/validate-field-name-availability.utils.ts index 189df27df38..b56c7783881 100644 --- a/packages/twenty-server/src/engine/metadata-modules/utils/validate-field-name-availability.utils.ts +++ b/packages/twenty-server/src/engine/metadata-modules/utils/validate-field-name-availability.utils.ts @@ -6,18 +6,27 @@ import { import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { type FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { InvalidMetadataException, InvalidMetadataExceptionCode, } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception'; const getReservedCompositeFieldNames = ( - fieldMetadataMapById: FieldMetadataMap, + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, ) => { const reservedCompositeFieldsNames: string[] = []; - for (const field of Object.values(fieldMetadataMapById)) { + for (const fieldId of flatObjectMetadata.fieldMetadataIds) { + const field = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityMaps: flatFieldMetadataMaps, + flatEntityId: fieldId, + }); + if (isCompositeFieldMetadataType(field.type)) { const base = field.name; const compositeType = compositeTypeDefinitions.get(field.type); @@ -35,30 +44,37 @@ const getReservedCompositeFieldNames = ( type ValidateFieldNameAvailabilityOrThrowArgs = { name: string; - fieldMetadataMapById: FieldMetadataMap; + flatObjectMetadata: FlatObjectMetadata; + flatFieldMetadataMaps: FlatEntityMaps; }; export const validateFieldNameAvailabilityOrThrow = ({ name, - fieldMetadataMapById, + flatObjectMetadata, + flatFieldMetadataMaps, }: ValidateFieldNameAvailabilityOrThrowArgs) => { - const reservedCompositeFieldsNames = - getReservedCompositeFieldNames(fieldMetadataMapById); + const reservedCompositeFieldsNames = getReservedCompositeFieldNames( + flatObjectMetadata, + flatFieldMetadataMaps, + ); - if ( - Object.values(fieldMetadataMapById).some( - (field) => - field.name === name || - (field.type === FieldMetadataType.RELATION && - `${field.name}Id` === name), - ) - ) { - throw new InvalidMetadataException( - `Name "${name}" is not available as it is already used by another field`, - InvalidMetadataExceptionCode.NOT_AVAILABLE, - { - userFriendlyMessage: msg`This name is not available as it is already used by another field.`, - }, - ); + for (const fieldId of flatObjectMetadata.fieldMetadataIds) { + const field = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityMaps: flatFieldMetadataMaps, + flatEntityId: fieldId, + }); + + if ( + field.name === name || + (field.type === FieldMetadataType.RELATION && `${field.name}Id` === name) + ) { + throw new InvalidMetadataException( + `Name "${name}" is not available as it is already used by another field`, + InvalidMetadataExceptionCode.NOT_AVAILABLE, + { + userFriendlyMessage: msg`This name is not available as it is already used by another field.`, + }, + ); + } } if (reservedCompositeFieldsNames.includes(name)) { diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/validate-no-other-object-with-same-name-exists-or-throw.util.ts b/packages/twenty-server/src/engine/metadata-modules/utils/validate-no-other-object-with-same-name-exists-or-throw.util.ts index 3182cd61601..b222d2f2911 100644 --- a/packages/twenty-server/src/engine/metadata-modules/utils/validate-no-other-object-with-same-name-exists-or-throw.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/utils/validate-no-other-object-with-same-name-exists-or-throw.util.ts @@ -7,13 +7,12 @@ import { ObjectMetadataException, ObjectMetadataExceptionCode, } from 'src/engine/metadata-modules/object-metadata/object-metadata.exception'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; type ValidateNoOtherObjectWithSameNameExistsOrThrowsParams = { objectMetadataNameSingular: string; objectMetadataNamePlural: string; existingObjectMetadataId?: string; - objectMetadataMaps: ObjectMetadataMaps | FlatEntityMaps; + objectMetadataMaps: FlatEntityMaps; }; export const doesOtherObjectWithSameNameExists = ({ diff --git a/packages/twenty-server/src/engine/twenty-orm/entity-manager/types/relation-connect-query-config.type.ts b/packages/twenty-server/src/engine/twenty-orm/entity-manager/types/relation-connect-query-config.type.ts index c153660455e..c1ad1200bb3 100644 --- a/packages/twenty-server/src/engine/twenty-orm/entity-manager/types/relation-connect-query-config.type.ts +++ b/packages/twenty-server/src/engine/twenty-orm/entity-manager/types/relation-connect-query-config.type.ts @@ -1,6 +1,4 @@ -import { type FieldMetadataType } from 'twenty-shared/types'; - -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; export type UniqueFieldCondition = [field: string, value: string]; @@ -11,7 +9,7 @@ export type RelationConnectQueryConfig = { recordToConnectConditions: UniqueConstraintCondition[]; relationFieldName: string; connectFieldName: string; - uniqueConstraintFields: FieldMetadataEntity[]; + uniqueConstraintFields: FlatFieldMetadata[]; recordToConnectConditionByEntityIndex: { [entityIndex: number]: UniqueConstraintCondition; }; diff --git a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts index 496bb18226c..c9a6f077e92 100644 --- a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts @@ -1,11 +1,16 @@ -import { type ObjectsPermissions } from 'twenty-shared/types'; +import { + type FieldMetadataType, + type ObjectsPermissions, +} from 'twenty-shared/types'; import { EntityManager } from 'typeorm'; import { EntityPersistExecutor } from 'typeorm/persistence/EntityPersistExecutor'; import { PlainObjectToDatabaseEntityTransformer } from 'typeorm/query-builder/transformer/PlainObjectToDatabaseEntityTransformer'; import { type WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { type WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; import { validateOperationIsPermittedOrThrow } from 'src/engine/twenty-orm/repository/permissions.utils'; @@ -79,47 +84,105 @@ describe('WorkspaceEntityManager', () => { }; beforeEach(() => { + const mockFlatObjectMetadata: FlatObjectMetadata = { + id: 'test-entity-id', + nameSingular: 'test-entity', + namePlural: 'test-entities', + labelSingular: 'Test Entity', + labelPlural: 'Test Entities', + workspaceId: 'test-workspace-id', + icon: 'test-icon', + isCustom: false, + isRemote: false, + isAuditLogged: false, + isSearchable: false, + isSystem: false, + isActive: true, + targetTableName: 'test_entity', + fieldMetadataIds: ['field-id'], + indexMetadataIds: [], + viewIds: [], + universalIdentifier: 'test-entity-id', + description: null, + imageIdentifierFieldMetadataId: null, + labelIdentifierFieldMetadataId: null, + shortcut: null, + standardId: null, + standardOverrides: null, + applicationId: null, + isLabelSyncedWithName: false, + isUIReadOnly: false, + duplicateCriteria: null, + createdAt: new Date(), + updatedAt: new Date(), + }; + + const mockFlatFieldMetadata: FlatFieldMetadata = { + id: 'field-id', + type: 'TEXT' as FieldMetadataType, + name: 'fieldName', + label: 'Field Name', + objectMetadataId: 'test-entity-id', + isNullable: true, + isLabelSyncedWithName: false, + createdAt: new Date(), + updatedAt: new Date(), + universalIdentifier: 'field-id', + defaultValue: null, + description: null, + icon: null, + isActive: true, + isCustom: false, + isSystem: false, + isUIReadOnly: false, + isUnique: false, + options: null, + settings: null, + standardId: null, + standardOverrides: null, + workspaceId: 'test-workspace-id', + viewFieldIds: [], + viewFilterIds: [], + viewGroupIds: [], + kanbanAggregateOperationViewIds: [], + calendarViewIds: [], + relationTargetFieldMetadataId: null, + relationTargetObjectMetadataId: null, + morphId: null, + applicationId: null, + }; + + const flatObjectMetadataMaps: FlatEntityMaps = { + byId: { + 'test-entity-id': mockFlatObjectMetadata, + }, + idByUniversalIdentifier: { + 'test-entity-id': 'test-entity-id', + }, + universalIdentifiersByApplicationId: {}, + }; + + const flatFieldMetadataMaps: FlatEntityMaps = { + byId: { + 'field-id': mockFlatFieldMetadata, + }, + idByUniversalIdentifier: { + 'field-id': 'field-id', + }, + universalIdentifiersByApplicationId: {}, + }; + mockInternalContext = { workspaceId: 'test-workspace-id', - objectMetadataMaps: { - byId: { - 'test-entity-id': { - id: 'test-entity-id', - nameSingular: 'test-entity', - namePlural: 'test-entities', - labelSingular: 'Test Entity', - labelPlural: 'Test Entities', - workspaceId: 'test-workspace-id', - icon: 'test-icon', - color: 'test-color', - isCustom: false, - isRemote: false, - isAuditLogged: false, - isSearchable: false, - isSystem: false, - isActive: true, - targetTableName: 'test_entity', - indexMetadatas: [], - fieldsById: { - 'field-id': { - id: 'field-id', - type: 'TEXT', - name: 'fieldName', - label: 'Field Name', - objectMetadataId: 'test-entity-id', - isNullable: true, - isLabelSyncedWithName: false, - createdAt: new Date(), - updatedAt: new Date(), - }, - }, - fieldIdByName: { fieldName: 'field-id' }, - fieldIdByJoinColumnName: {}, - } as unknown as ObjectMetadataItemWithFieldMaps, - }, - idByNameSingular: { - 'test-entity': 'test-entity-id', - }, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatIndexMaps: { + byId: {}, + idByUniversalIdentifier: {}, + universalIdentifiersByApplicationId: {}, + }, + objectIdByNameSingular: { + 'test-entity': 'test-entity-id', }, featureFlagsMap: { IS_AIRTABLE_INTEGRATION_ENABLED: false, @@ -333,7 +396,9 @@ describe('WorkspaceEntityManager', () => { expect(validateOperationIsPermittedOrThrow).toHaveBeenCalledWith({ entityName: 'test-entity', operationType: 'update', - objectMetadataMaps: mockInternalContext.objectMetadataMaps, + flatObjectMetadataMaps: mockInternalContext.flatObjectMetadataMaps, + flatFieldMetadataMaps: mockInternalContext.flatFieldMetadataMaps, + objectIdByNameSingular: mockInternalContext.objectIdByNameSingular, objectsPermissions: mockPermissionOptions.objectRecordsPermissions, selectedColumns: [], allFieldsSelected: false, @@ -366,7 +431,9 @@ describe('WorkspaceEntityManager', () => { expect(validateOperationIsPermittedOrThrow).toHaveBeenCalledWith({ entityName: 'test-entity', operationType: 'delete', - objectMetadataMaps: mockInternalContext.objectMetadataMaps, + flatObjectMetadataMaps: mockInternalContext.flatObjectMetadataMaps, + flatFieldMetadataMaps: mockInternalContext.flatFieldMetadataMaps, + objectIdByNameSingular: mockInternalContext.objectIdByNameSingular, objectsPermissions: mockPermissionOptions.objectRecordsPermissions, selectedColumns: [], allFieldsSelected: false, diff --git a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.ts b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.ts index 0f48536d6b3..36ccb5ccf46 100644 --- a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.ts +++ b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.ts @@ -39,11 +39,11 @@ import { type WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/ import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; import { InternalServerError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { PermissionsException, PermissionsExceptionCode, } from 'src/engine/metadata-modules/permissions/permissions.exception'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { type WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; import { type DeepPartialWithNestedRelationFields } from 'src/engine/twenty-orm/entity-manager/types/deep-partial-entity-with-nested-relation-fields.type'; import { type QueryDeepPartialEntityWithNestedRelationFields } from 'src/engine/twenty-orm/entity-manager/types/query-deep-partial-entity-with-nested-relation-fields.type'; @@ -442,7 +442,9 @@ export class WorkspaceEntityManager extends EntityManager { entityName, operationType, objectsPermissions: permissionOptions?.objectRecordsPermissions ?? {}, - objectMetadataMaps: this.internalContext.objectMetadataMaps, + flatObjectMetadataMaps: this.internalContext.flatObjectMetadataMaps, + flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps, + objectIdByNameSingular: this.internalContext.objectIdByNameSingular, selectedColumns, allFieldsSelected: false, updatedColumns, @@ -956,7 +958,11 @@ export class WorkspaceEntityManager extends EntityManager { this.internalContext, ); - const formattedEntityLike = formatData(entityLike, objectMetadataItem); + const formattedEntityLike = formatData( + entityLike, + objectMetadataItem, + this.internalContext.flatFieldMetadataMaps, + ); const managerWithPermissionOptions = Object.assign( Object.create(Object.getPrototypeOf(this)), @@ -1190,6 +1196,7 @@ export class WorkspaceEntityManager extends EntityManager { const formattedEntityOrEntities = formatData( entityWithConnectedRelations, objectMetadataItem, + this.internalContext.flatFieldMetadataMaps, ); const updatedColumns = formattedEntityOrEntities @@ -1221,7 +1228,8 @@ export class WorkspaceEntityManager extends EntityManager { let formattedResult = formatResult( resultArray, objectMetadataItem, - this.internalContext.objectMetadataMaps, + this.internalContext.flatObjectMetadataMaps, + this.internalContext.flatFieldMetadataMaps, ); const updatedEntities = formattedResult.filter( @@ -1235,6 +1243,7 @@ export class WorkspaceEntityManager extends EntityManager { formatTwentyOrmEventToDatabaseBatchEvent({ action: DatabaseEventAction.UPDATED, objectMetadataItem, + flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps, workspaceId: this.internalContext.workspaceId, entities: updatedEntities, beforeEntities: updatedEntities.map( @@ -1247,6 +1256,7 @@ export class WorkspaceEntityManager extends EntityManager { formatTwentyOrmEventToDatabaseBatchEvent({ action: DatabaseEventAction.CREATED, objectMetadataItem, + flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps, workspaceId: this.internalContext.workspaceId, entities: createdEntities, }), @@ -1283,7 +1293,7 @@ export class WorkspaceEntityManager extends EntityManager { permissionOptionsFromArgs, }: { formattedResult: Entity[]; - objectMetadataItem: ObjectMetadataItemWithFieldMaps; + objectMetadataItem: FlatObjectMetadata; permissionOptionsFromArgs: PermissionOptions | undefined; }): Entity[] { if (permissionOptionsFromArgs?.shouldBypassPermissionChecks === true) { @@ -1303,15 +1313,12 @@ export class WorkspaceEntityManager extends EntityManager { return formattedResult; } - const objectMetadataItemWithFieldMaps = - this.internalContext.objectMetadataMaps.byId[objectMetadataItem.id]; - const restrictedFieldNames = new Set( Object.entries(restrictedFields) .filter(([_, fieldPermissions]) => fieldPermissions.canRead === false) .map(([fieldMetadataId]) => { const fieldMetadata = - objectMetadataItemWithFieldMaps?.fieldsById[fieldMetadataId]; + this.internalContext.flatFieldMetadataMaps.byId[fieldMetadataId]; if (!isDefined(fieldMetadata)) { throw new InternalServerError( @@ -1407,7 +1414,11 @@ export class WorkspaceEntityManager extends EntityManager { this.internalContext, ); - const formattedEntity = formatData(entity, objectMetadataItem); + const formattedEntity = formatData( + entity, + objectMetadataItem, + this.internalContext.flatFieldMetadataMaps, + ); const result = new EntityPersistExecutor( this.connection, @@ -1424,13 +1435,15 @@ export class WorkspaceEntityManager extends EntityManager { const formattedResult = formatResult( result, objectMetadataItem, - this.internalContext.objectMetadataMaps, + this.internalContext.flatObjectMetadataMaps, + this.internalContext.flatFieldMetadataMaps, ); this.internalContext.eventEmitterService.emitDatabaseBatchEvent( formatTwentyOrmEventToDatabaseBatchEvent({ action: DatabaseEventAction.DESTROYED, objectMetadataItem, + flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps, workspaceId: this.internalContext.workspaceId, entities: formattedResult, }), @@ -1522,7 +1535,11 @@ export class WorkspaceEntityManager extends EntityManager { this.internalContext, ); - const formattedEntity = formatData(entity, objectMetadataItem); + const formattedEntity = formatData( + entity, + objectMetadataItem, + this.internalContext.flatFieldMetadataMaps, + ); const result = new EntityPersistExecutor( this.connection, @@ -1539,13 +1556,15 @@ export class WorkspaceEntityManager extends EntityManager { const formattedResult = formatResult( result, objectMetadataItem, - this.internalContext.objectMetadataMaps, + this.internalContext.flatObjectMetadataMaps, + this.internalContext.flatFieldMetadataMaps, ); this.internalContext.eventEmitterService.emitDatabaseBatchEvent( formatTwentyOrmEventToDatabaseBatchEvent({ action: DatabaseEventAction.DELETED, objectMetadataItem, + flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps, workspaceId: this.internalContext.workspaceId, entities: formattedResult, }), @@ -1633,7 +1652,11 @@ export class WorkspaceEntityManager extends EntityManager { this.internalContext, ); - const formattedEntity = formatData(entity, objectMetadataItem); + const formattedEntity = formatData( + entity, + objectMetadataItem, + this.internalContext.flatFieldMetadataMaps, + ); const result = new EntityPersistExecutor( this.connection, @@ -1650,13 +1673,15 @@ export class WorkspaceEntityManager extends EntityManager { const formattedResult = formatResult( result, objectMetadataItem, - this.internalContext.objectMetadataMaps, + this.internalContext.flatObjectMetadataMaps, + this.internalContext.flatFieldMetadataMaps, ); this.internalContext.eventEmitterService.emitDatabaseBatchEvent( formatTwentyOrmEventToDatabaseBatchEvent({ action: DatabaseEventAction.RESTORED, objectMetadataItem, + flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps, workspaceId: this.internalContext.workspaceId, entities: formattedResult, }), diff --git a/packages/twenty-server/src/engine/twenty-orm/error-handling/compute-twenty-orm-exception.ts b/packages/twenty-server/src/engine/twenty-orm/error-handling/compute-twenty-orm-exception.ts index 02682bf91ca..71bc93c269d 100644 --- a/packages/twenty-server/src/engine/twenty-orm/error-handling/compute-twenty-orm-exception.ts +++ b/packages/twenty-server/src/engine/twenty-orm/error-handling/compute-twenty-orm-exception.ts @@ -5,7 +5,7 @@ import { QueryFailedError } from 'typeorm'; import { POSTGRESQL_ERROR_CODES } from 'src/engine/api/graphql/workspace-query-runner/constants/postgres-error-codes.constants'; import { handleDuplicateKeyError } from 'src/engine/api/graphql/workspace-query-runner/utils/handle-duplicate-key-error.util'; import { PostgresException } from 'src/engine/api/graphql/workspace-query-runner/utils/postgres-exception'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { TwentyORMException, TwentyORMExceptionCode, @@ -17,7 +17,7 @@ interface QueryFailedErrorWithCode extends QueryFailedError { export const computeTwentyORMException = ( error: Error, - objectMetadata?: ObjectMetadataItemWithFieldMaps, + objectMetadata?: FlatObjectMetadata, ) => { if (error instanceof QueryFailedError) { if (error.message.includes('Query read timeout')) { diff --git a/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-column.factory.ts b/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-column.factory.ts index d00ba2a7894..ce47e843da6 100644 --- a/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-column.factory.ts +++ b/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-column.factory.ts @@ -9,12 +9,14 @@ import { type ColumnType, type EntitySchemaColumnOptions } from 'typeorm'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util'; import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; import { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util'; import { serializeDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-default-value'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util'; import { TwentyORMException, @@ -29,12 +31,14 @@ type EntitySchemaColumnMap = { @Injectable() export class EntitySchemaColumnFactory { create( - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, ): EntitySchemaColumnMap { let entitySchemaColumnMap: EntitySchemaColumnMap = {}; - const fieldMetadataCollection = Object.values( - objectMetadataItemWithFieldMaps.fieldsById, + const fieldMetadataCollection = getFlatFieldsFromFlatObjectMetadata( + flatObjectMetadata, + flatFieldMetadataMaps, ); for (const fieldMetadata of fieldMetadataCollection) { @@ -117,7 +121,7 @@ export class EntitySchemaColumnFactory { } private createCompositeColumns( - fieldMetadata: FieldMetadataEntity, + fieldMetadata: FlatFieldMetadata, ): EntitySchemaColumnMap { const entitySchemaColumnMap: EntitySchemaColumnMap = {}; const compositeType = compositeTypeDefinitions.get(fieldMetadata.type); diff --git a/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-relation.factory.ts b/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-relation.factory.ts index 194f6bb3b93..1cbd6e3b216 100644 --- a/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-relation.factory.ts +++ b/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-relation.factory.ts @@ -1,12 +1,13 @@ import { Injectable } from '@nestjs/common'; -import { FieldMetadataType } from 'twenty-shared/types'; import { type EntitySchemaRelationOptions } from 'typeorm'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { determineSchemaRelationDetails } from 'src/engine/twenty-orm/utils/determine-schema-relation-details.util'; -import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; type EntitySchemaRelationMap = { [key: string]: EntitySchemaRelationOptions; @@ -16,52 +17,47 @@ type EntitySchemaRelationMap = { export class EntitySchemaRelationFactory { constructor() {} - async create( - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, - objectMetadataMaps: ObjectMetadataMaps, - ): Promise { + create( + flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, + ): EntitySchemaRelationMap { const entitySchemaRelationMap: EntitySchemaRelationMap = {}; - const fieldMetadataCollection = Object.values( - objectMetadataItemWithFieldMaps.fieldsById, + const flatFieldMetadatas = getFlatFieldsFromFlatObjectMetadata( + flatObjectMetadata, + flatFieldMetadataMaps, ); - for (const fieldMetadata of fieldMetadataCollection) { - const isRelation = - isFieldMetadataEntityOfType( - fieldMetadata, - FieldMetadataType.RELATION, - ) || - isFieldMetadataEntityOfType( - fieldMetadata, - FieldMetadataType.MORPH_RELATION, - ); - - if (!isRelation) { + for (const flatFieldMetadata of flatFieldMetadatas) { + if (!isMorphOrRelationFlatFieldMetadata(flatFieldMetadata)) { continue; } - if (!fieldMetadata.settings) { + if (!flatFieldMetadata.settings) { throw new Error( - `Field metadata settings are missing for field ${fieldMetadata.name}`, + `Field metadata settings are missing for field ${flatFieldMetadata.name}`, ); } - const schemaRelationDetails = await determineSchemaRelationDetails( - fieldMetadata, - objectMetadataMaps, + const schemaRelationDetails = determineSchemaRelationDetails( + flatFieldMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, ); const targetObjectMetadata = - objectMetadataMaps.byId[fieldMetadata.relationTargetObjectMetadataId]; + flatObjectMetadataMaps.byId[ + flatFieldMetadata.relationTargetObjectMetadataId + ]; if (!targetObjectMetadata) { throw new Error( - `Target object metadata not found for field ${fieldMetadata.name}`, + `Target object metadata not found for field ${flatFieldMetadata.name}`, ); } - entitySchemaRelationMap[fieldMetadata.name] = { + entitySchemaRelationMap[flatFieldMetadata.name] = { type: schemaRelationDetails.relationType, target: schemaRelationDetails.target, inverseSide: schemaRelationDetails.inverseSide, diff --git a/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema.factory.ts b/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema.factory.ts index 540455377a6..064c41fabd1 100644 --- a/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema.factory.ts +++ b/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema.factory.ts @@ -2,8 +2,9 @@ import { Injectable } from '@nestjs/common'; import { EntitySchema } from 'typeorm'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { EntitySchemaColumnFactory } from 'src/engine/twenty-orm/factories/entity-schema-column.factory'; import { EntitySchemaRelationFactory } from 'src/engine/twenty-orm/factories/entity-schema-relation.factory'; import { WorkspaceEntitiesStorage } from 'src/engine/twenty-orm/storage/workspace-entities.storage'; @@ -17,25 +18,30 @@ export class EntitySchemaFactory { private readonly entitySchemaRelationFactory: EntitySchemaRelationFactory, ) {} - async create( + create( workspaceId: string, - objectMetadata: ObjectMetadataItemWithFieldMaps, - objectMetadataMaps: ObjectMetadataMaps, - ): Promise { - const columns = this.entitySchemaColumnFactory.create(objectMetadata); + flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, + ): EntitySchema { + const columns = this.entitySchemaColumnFactory.create( + flatObjectMetadata, + flatFieldMetadataMaps, + ); - const relations = await this.entitySchemaRelationFactory.create( - objectMetadata, - objectMetadataMaps, + const relations = this.entitySchemaRelationFactory.create( + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, ); const schemaName = getWorkspaceSchemaName(workspaceId); const entitySchema = new EntitySchema({ - name: objectMetadata.nameSingular, + name: flatObjectMetadata.nameSingular, tableName: computeTableName( - objectMetadata.nameSingular, - objectMetadata.isCustom, + flatObjectMetadata.nameSingular, + flatObjectMetadata.isCustom, ), columns, relations, @@ -44,7 +50,7 @@ export class EntitySchemaFactory { WorkspaceEntitiesStorage.setEntitySchema( workspaceId, - objectMetadata.nameSingular, + flatObjectMetadata.nameSingular, entitySchema, ); diff --git a/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts b/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts index b1231623bf7..2692ff1935f 100644 --- a/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts +++ b/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts @@ -12,8 +12,6 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util'; -import { buildObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-metadata-item-with-field-maps.util'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { WorkspaceFeatureFlagsMapCacheService } from 'src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.service'; import { WorkspacePermissionsCacheStorageService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache-storage.service'; import { @@ -144,26 +142,8 @@ export class WorkspaceDatasourceFactory { }, ); - const { idByNameSingular } = buildObjectIdByNameMaps( - flatObjectMetadataMaps, - ); - const cachedObjectMetadataMaps: ObjectMetadataMaps = { - byId: {}, - idByNameSingular, - }; - - for (const [id, flatObj] of Object.entries( - flatObjectMetadataMaps.byId, - )) { - if (isDefined(flatObj)) { - cachedObjectMetadataMaps.byId[id] = - buildObjectMetadataItemWithFieldMaps( - flatObj, - flatFieldMetadataMaps, - flatIndexMaps, - ); - } - } + const { idByNameSingular: objectIdByNameSingular } = + buildObjectIdByNameMaps(flatObjectMetadataMaps); const metadataVersionForFinalUpToDateCheck = await this.workspaceCacheStorageService.getMetadataVersion( @@ -184,17 +164,16 @@ export class WorkspaceDatasourceFactory { (option) => new EntitySchema(option), ); } else { - const entitySchemas = await Promise.all( - Object.values(cachedObjectMetadataMaps.byId) - .filter(isDefined) - .map((objectMetadata) => - this.entitySchemaFactory.create( - workspaceId, - objectMetadata, - cachedObjectMetadataMaps, - ), + const entitySchemas = Object.values(flatObjectMetadataMaps.byId) + .filter(isDefined) + .map((flatObjectMetadata) => + this.entitySchemaFactory.create( + workspaceId, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, ), - ); + ); await this.workspaceCacheStorageService.setORMEntitySchema( workspaceId, @@ -208,7 +187,10 @@ export class WorkspaceDatasourceFactory { const workspaceDataSource = new WorkspaceDataSource( { workspaceId, - objectMetadataMaps: cachedObjectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatIndexMaps, + objectIdByNameSingular, featureFlagsMap: cachedFeatureFlagMap, eventEmitterService: this.workspaceEventEmitter, }, diff --git a/packages/twenty-server/src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource.ts b/packages/twenty-server/src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource.ts index c6dd77c4d2f..b8854fea89b 100644 --- a/packages/twenty-server/src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource.ts +++ b/packages/twenty-server/src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource.ts @@ -112,7 +112,10 @@ export class GlobalWorkspaceDataSource extends DataSource { return new WorkspaceEntityManager( { workspaceId: context.authContext.workspace.id, - objectMetadataMaps: context.objectMetadataMaps, + flatObjectMetadataMaps: context.flatObjectMetadataMaps, + flatFieldMetadataMaps: context.flatFieldMetadataMaps, + flatIndexMaps: context.flatIndexMaps, + objectIdByNameSingular: context.objectIdByNameSingular, featureFlagsMap: context.featureFlagsMap, eventEmitterService: this.eventEmitterService, }, diff --git a/packages/twenty-server/src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager.ts b/packages/twenty-server/src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager.ts index cd4bbbde8c7..b684159a8da 100644 --- a/packages/twenty-server/src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager.ts +++ b/packages/twenty-server/src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager.ts @@ -6,9 +6,10 @@ import { EntitySchema, ObjectLiteral } from 'typeorm'; import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util'; -import { buildObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-metadata-item-with-field-maps.util'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { WorkspaceFeatureFlagsMapCacheService } from 'src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.service'; import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; import { EntitySchemaFactory } from 'src/engine/twenty-orm/factories/entity-schema.factory'; @@ -99,23 +100,8 @@ export class GlobalWorkspaceOrmManager { }, ); - const { idByNameSingular } = buildObjectIdByNameMaps( - flatObjectMetadataMaps, - ); - const objectMetadataMaps: ObjectMetadataMaps = { - byId: {}, - idByNameSingular, - }; - - for (const [id, flatObj] of Object.entries(flatObjectMetadataMaps.byId)) { - if (isDefined(flatObj)) { - objectMetadataMaps.byId[id] = buildObjectMetadataItemWithFieldMaps( - flatObj, - flatFieldMetadataMaps, - flatIndexMaps, - ); - } - } + const { idByNameSingular: objectIdByNameSingular } = + buildObjectIdByNameMaps(flatObjectMetadataMaps); const metadataVersion = await this.workspaceCacheStorageService.getMetadataVersion(workspaceId); @@ -139,12 +125,16 @@ export class GlobalWorkspaceOrmManager { const entitySchemas = await this.buildEntitySchemas( workspaceId, metadataVersion, - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, ); return { authContext, - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatIndexMaps, + objectIdByNameSingular, metadataVersion, featureFlagsMap, permissionsPerRoleId, @@ -155,7 +145,8 @@ export class GlobalWorkspaceOrmManager { private async buildEntitySchemas( workspaceId: string, dataSourceMetadataVersion: number, - objectMetadataMaps: ObjectMetadataMaps, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, ) { const cachedEntitySchemaOptions = await this.workspaceCacheStorageService.getORMEntitySchema( @@ -170,17 +161,16 @@ export class GlobalWorkspaceOrmManager { (option) => new EntitySchema(option), ); } else { - const entitySchemas = await Promise.all( - Object.values(objectMetadataMaps.byId) - .filter(isDefined) - .map((objectMetadata) => - this.entitySchemaFactory.create( - workspaceId, - objectMetadata, - objectMetadataMaps, - ), + const entitySchemas = Object.values(flatObjectMetadataMaps.byId) + .filter(isDefined) + .map((flatObjectMetadata) => + this.entitySchemaFactory.create( + workspaceId, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, ), - ); + ); await this.workspaceCacheStorageService.setORMEntitySchema( workspaceId, diff --git a/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-internal-context.interface.ts b/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-internal-context.interface.ts index 1f93da6149a..57a66bdf80f 100644 --- a/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-internal-context.interface.ts +++ b/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-internal-context.interface.ts @@ -1,10 +1,16 @@ import { type FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { type WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; export interface WorkspaceInternalContext { workspaceId: string; - objectMetadataMaps: ObjectMetadataMaps; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; + flatIndexMaps: FlatEntityMaps; + objectIdByNameSingular: Record; featureFlagsMap: Record; eventEmitterService: WorkspaceEventEmitter; } diff --git a/packages/twenty-server/src/engine/twenty-orm/relation-nested-queries/relation-nested-queries.ts b/packages/twenty-server/src/engine/twenty-orm/relation-nested-queries/relation-nested-queries.ts index f98bb5441eb..da1ef88fdd2 100644 --- a/packages/twenty-server/src/engine/twenty-orm/relation-nested-queries/relation-nested-queries.ts +++ b/packages/twenty-server/src/engine/twenty-orm/relation-nested-queries/relation-nested-queries.ts @@ -72,12 +72,12 @@ export class RelationNestedQueries { this.internalContext, ); - const objectMetadataMap = this.internalContext.objectMetadataMaps; - const relationConnectQueryConfigs = computeRelationConnectQueryConfigs( entities, objectMetadata, - objectMetadataMap, + this.internalContext.flatObjectMetadataMaps, + this.internalContext.flatFieldMetadataMaps, + this.internalContext.flatIndexMaps, relationConnectQueryFieldsByEntityIndex, ); diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/__tests__/workspace.repository.spec.ts b/packages/twenty-server/src/engine/twenty-orm/repository/__tests__/workspace.repository.spec.ts index 8f4b601d60d..f577735d964 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/__tests__/workspace.repository.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/__tests__/workspace.repository.spec.ts @@ -1,4 +1,7 @@ -import { type ObjectsPermissions } from 'twenty-shared/types'; +import { + FieldMetadataType, + type ObjectsPermissions, +} from 'twenty-shared/types'; import { type DeepPartial, type FindManyOptions, @@ -12,6 +15,7 @@ import { type FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interf import { type WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface'; import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; @@ -57,13 +61,64 @@ describe('WorkspaceRepository', () => { clear: jest.fn(), } as unknown as jest.Mocked; + const mockFieldMetadata: FlatFieldMetadata = { + id: 'test-field-id', + name: 'id', + type: FieldMetadataType.UUID, + objectMetadataId: 'test-metadata-id', + isActive: true, + isNullable: false, + isUnique: true, + isSystem: true, + isCustom: false, + isUIReadOnly: false, + isLabelSyncedWithName: false, + label: 'ID', + description: 'Record ID', + icon: 'IconKey', + workspaceId: 'test-workspace-id', + universalIdentifier: 'id-field-universal-id', + createdAt: new Date(), + updatedAt: new Date(), + defaultValue: null, + options: null, + settings: null, + morphId: null, + standardId: null, + standardOverrides: null, + applicationId: null, + relationTargetFieldMetadataId: null, + relationTargetObjectMetadataId: null, + calendarViewIds: [], + viewFilterIds: [], + viewGroupIds: [], + kanbanAggregateOperationViewIds: [], + viewFieldIds: [], + }; + mockInternalContext = { workspaceId: 'test-workspace-id', - objectMetadataMaps: { - idByNameSingular: {}, + flatObjectMetadataMaps: { + byId: {}, + idByUniversalIdentifier: {}, + universalIdentifiersByApplicationId: {}, }, + flatFieldMetadataMaps: { + byId: { + 'test-field-id': mockFieldMetadata, + }, + idByUniversalIdentifier: {}, + universalIdentifiersByApplicationId: {}, + }, + flatIndexMaps: { + byId: {}, + idByUniversalIdentifier: {}, + universalIdentifiersByApplicationId: {}, + }, + objectIdByNameSingular: {}, featureFlagsMap: {}, - } as WorkspaceInternalContext; + eventEmitterService: {} as unknown, + } as unknown as WorkspaceInternalContext; mockFeatureFlagMap = Object.values(FeatureFlagKey).reduce( (acc, key) => ({ ...acc, [key]: false }), @@ -97,6 +152,7 @@ describe('WorkspaceRepository', () => { id: 'test-metadata-id', nameSingular: 'test-entity', namePlural: 'test-entities', + fieldMetadataIds: ['test-field-id'], fieldIdByName: { id: 'test-field-id', }, diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/permissions.utils.ts b/packages/twenty-server/src/engine/twenty-orm/repository/permissions.utils.ts index f0390284a50..cfc68558895 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/permissions.utils.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/permissions.utils.ts @@ -9,12 +9,14 @@ import { type QueryExpressionMap } from 'typeorm/query-builder/QueryExpressionMa import { ProcessAggregateHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper'; import { InternalServerError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { PermissionsException, PermissionsExceptionCode, PermissionsExceptionMessage, } from 'src/engine/metadata-modules/permissions/permissions.exception'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { getColumnNameToFieldMetadataIdMap } from 'src/engine/twenty-orm/utils/get-column-name-to-field-metadata-id.util'; const getTargetEntityAndOperationType = ( @@ -66,7 +68,9 @@ type ValidateOperationIsPermittedOrThrowArgs = { entityName: string; operationType: OperationType; objectsPermissions: ObjectsPermissions; - objectMetadataMaps: ObjectMetadataMaps; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; + objectIdByNameSingular: Record; selectedColumns: string[] | '*'; allFieldsSelected: boolean; updatedColumns: string[]; @@ -76,13 +80,14 @@ export const validateOperationIsPermittedOrThrow = ({ entityName, operationType, objectsPermissions, - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, selectedColumns, allFieldsSelected, updatedColumns, }: ValidateOperationIsPermittedOrThrowArgs) => { - const objectMetadataIdForEntity = - objectMetadataMaps.idByNameSingular[entityName]; + const objectMetadataIdForEntity = objectIdByNameSingular[entityName]; if (!isNonEmptyString(objectMetadataIdForEntity)) { throw new PermissionsException( @@ -91,7 +96,7 @@ export const validateOperationIsPermittedOrThrow = ({ ); } - const objectMetadata = objectMetadataMaps.byId[objectMetadataIdForEntity]; + const objectMetadata = flatObjectMetadataMaps.byId[objectMetadataIdForEntity]; if (!isDefined(objectMetadata)) { throw new PermissionsException( @@ -106,8 +111,10 @@ export const validateOperationIsPermittedOrThrow = ({ return; } - const columnNameToFieldMetadataIdMap = - getColumnNameToFieldMetadataIdMap(objectMetadata); + const columnNameToFieldMetadataIdMap = getColumnNameToFieldMetadataIdMap( + objectMetadata, + flatFieldMetadataMaps, + ); const permissionsForEntity = objectsPermissions[objectMetadataIdForEntity]; @@ -194,14 +201,18 @@ export const validateOperationIsPermittedOrThrow = ({ type ValidateQueryIsPermittedOrThrowArgs = { expressionMap: QueryExpressionMap; objectsPermissions: ObjectsPermissions; - objectMetadataMaps: ObjectMetadataMaps; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; + objectIdByNameSingular: Record; shouldBypassPermissionChecks: boolean; }; export const validateQueryIsPermittedOrThrow = ({ expressionMap, objectsPermissions, - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, shouldBypassPermissionChecks, }: ValidateQueryIsPermittedOrThrowArgs) => { if (shouldBypassPermissionChecks) { @@ -222,7 +233,9 @@ export const validateQueryIsPermittedOrThrow = ({ validatePermissionsForJoinsAndReturnSelectsWithoutJoins({ expressionMap, objectsPermissions, - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, }); expressionMapSelectsOnMainEntity = selectsWithoutJoinedAliases; @@ -266,7 +279,9 @@ export const validateQueryIsPermittedOrThrow = ({ entityName: mainEntity, operationType: operationType as OperationType, objectsPermissions, - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, selectedColumns, allFieldsSelected, updatedColumns, @@ -276,11 +291,15 @@ export const validateQueryIsPermittedOrThrow = ({ const validatePermissionsForJoinsAndReturnSelectsWithoutJoins = ({ expressionMap, objectsPermissions, - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, }: { expressionMap: QueryExpressionMap; objectsPermissions: ObjectsPermissions; - objectMetadataMaps: ObjectMetadataMaps; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; + objectIdByNameSingular: Record; }) => { const joinAttributesAliases = new Set( expressionMap.joinAttributes.map((join) => join.alias.name), @@ -313,7 +332,9 @@ const validatePermissionsForJoinsAndReturnSelectsWithoutJoins = ({ entityName: entity.name, operationType: 'select' as OperationType, objectsPermissions, - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, selectedColumns, allFieldsSelected: false, updatedColumns: [], diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-delete-query-builder.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-delete-query-builder.ts index 54774d07c03..50545135569 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-delete-query-builder.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-delete-query-builder.ts @@ -72,7 +72,9 @@ export class WorkspaceDeleteQueryBuilder< validateQueryIsPermittedOrThrow({ expressionMap: this.expressionMap, objectsPermissions: this.objectRecordsPermissions, - objectMetadataMaps: this.internalContext.objectMetadataMaps, + flatObjectMetadataMaps: this.internalContext.flatObjectMetadataMaps, + flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps, + objectIdByNameSingular: this.internalContext.objectIdByNameSingular, shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, }); @@ -110,19 +112,22 @@ export class WorkspaceDeleteQueryBuilder< const formattedResult = formatResult( result.raw, objectMetadata, - this.internalContext.objectMetadataMaps, + this.internalContext.flatObjectMetadataMaps, + this.internalContext.flatFieldMetadataMaps, ); const formattedBefore = formatResult( before, objectMetadata, - this.internalContext.objectMetadataMaps, + this.internalContext.flatObjectMetadataMaps, + this.internalContext.flatFieldMetadataMaps, ); this.internalContext.eventEmitterService.emitDatabaseBatchEvent( formatTwentyOrmEventToDatabaseBatchEvent({ action: DatabaseEventAction.DESTROYED, objectMetadataItem: objectMetadata, + flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps, workspaceId: this.internalContext.workspaceId, entities: formattedBefore, authContext: this.authContext, diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-insert-query-builder.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-insert-query-builder.ts index 2e48cfe8b2a..8281ce618ee 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-insert-query-builder.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-insert-query-builder.ts @@ -95,7 +95,11 @@ export class WorkspaceInsertQueryBuilder< this.internalContext, ); - const formattedValues = formatData(values, objectMetadata); + const formattedValues = formatData( + values, + objectMetadata, + this.internalContext.flatFieldMetadataMaps, + ); return super.values(formattedValues); } @@ -105,7 +109,9 @@ export class WorkspaceInsertQueryBuilder< validateQueryIsPermittedOrThrow({ expressionMap: this.expressionMap, objectsPermissions: this.objectRecordsPermissions, - objectMetadataMaps: this.internalContext.objectMetadataMaps, + flatObjectMetadataMaps: this.internalContext.flatObjectMetadataMaps, + flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps, + objectIdByNameSingular: this.internalContext.objectIdByNameSingular, shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, }); @@ -159,13 +165,15 @@ export class WorkspaceInsertQueryBuilder< const formattedResultForEvent = formatResult( afterResult, objectMetadata, - this.internalContext.objectMetadataMaps, + this.internalContext.flatObjectMetadataMaps, + this.internalContext.flatFieldMetadataMaps, ); this.internalContext.eventEmitterService.emitDatabaseBatchEvent( formatTwentyOrmEventToDatabaseBatchEvent({ action: DatabaseEventAction.CREATED, objectMetadataItem: objectMetadata, + flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps, workspaceId: this.internalContext.workspaceId, entities: formattedResultForEvent, authContext: this.authContext, @@ -176,6 +184,7 @@ export class WorkspaceInsertQueryBuilder< formatTwentyOrmEventToDatabaseBatchEvent({ action: DatabaseEventAction.UPSERTED, objectMetadataItem: objectMetadata, + flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps, workspaceId: this.internalContext.workspaceId, entities: formattedResultForEvent, authContext: this.authContext, @@ -202,7 +211,8 @@ export class WorkspaceInsertQueryBuilder< const formattedResult = formatResult( resultWithoutInsertionExtraColumns, objectMetadata, - this.internalContext.objectMetadataMaps, + this.internalContext.flatObjectMetadataMaps, + this.internalContext.flatFieldMetadataMaps, ); return { diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-select-query-builder.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-select-query-builder.ts index 975fe0c2264..643bcada01d 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-select-query-builder.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-select-query-builder.ts @@ -85,7 +85,8 @@ export class WorkspaceSelectQueryBuilder< const formattedResult = formatResult( result, objectMetadata, - this.internalContext.objectMetadataMaps, + this.internalContext.flatObjectMetadataMaps, + this.internalContext.flatFieldMetadataMaps, ); return { @@ -114,7 +115,8 @@ export class WorkspaceSelectQueryBuilder< const formattedResult = formatResult( result, objectMetadata, - this.internalContext.objectMetadataMaps, + this.internalContext.flatObjectMetadataMaps, + this.internalContext.flatFieldMetadataMaps, ); return formattedResult; @@ -163,7 +165,8 @@ export class WorkspaceSelectQueryBuilder< const formattedResult = formatResult( result, objectMetadata, - this.internalContext.objectMetadataMaps, + this.internalContext.flatObjectMetadataMaps, + this.internalContext.flatFieldMetadataMaps, ); return formattedResult; @@ -188,7 +191,8 @@ export class WorkspaceSelectQueryBuilder< const formattedResult = formatResult( result, objectMetadata, - this.internalContext.objectMetadataMaps, + this.internalContext.flatObjectMetadataMaps, + this.internalContext.flatFieldMetadataMaps, ); return formattedResult[0]; @@ -230,7 +234,8 @@ export class WorkspaceSelectQueryBuilder< const formattedResult = formatResult( result, objectMetadata, - this.internalContext.objectMetadataMaps, + this.internalContext.flatObjectMetadataMaps, + this.internalContext.flatFieldMetadataMaps, ); return [formattedResult, count]; @@ -325,7 +330,9 @@ export class WorkspaceSelectQueryBuilder< validateQueryIsPermittedOrThrow({ expressionMap: this.expressionMap, objectsPermissions: this.objectRecordsPermissions, - objectMetadataMaps: this.internalContext.objectMetadataMaps, + flatObjectMetadataMaps: this.internalContext.flatObjectMetadataMaps, + flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps, + objectIdByNameSingular: this.internalContext.objectIdByNameSingular, shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, }); } diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-soft-delete-query-builder.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-soft-delete-query-builder.ts index 73ffe7c4393..28e911ca304 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-soft-delete-query-builder.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-soft-delete-query-builder.ts @@ -72,7 +72,9 @@ export class WorkspaceSoftDeleteQueryBuilder< validateQueryIsPermittedOrThrow({ expressionMap: this.expressionMap, objectsPermissions: this.objectRecordsPermissions, - objectMetadataMaps: this.internalContext.objectMetadataMaps, + flatObjectMetadataMaps: this.internalContext.flatObjectMetadataMaps, + flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps, + objectIdByNameSingular: this.internalContext.objectIdByNameSingular, shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, }); @@ -110,19 +112,22 @@ export class WorkspaceSoftDeleteQueryBuilder< const formattedAfter = formatResult( after.raw, objectMetadata, - this.internalContext.objectMetadataMaps, + this.internalContext.flatObjectMetadataMaps, + this.internalContext.flatFieldMetadataMaps, ); const formattedBefore = formatResult( before, objectMetadata, - this.internalContext.objectMetadataMaps, + this.internalContext.flatObjectMetadataMaps, + this.internalContext.flatFieldMetadataMaps, ); this.internalContext.eventEmitterService.emitDatabaseBatchEvent( formatTwentyOrmEventToDatabaseBatchEvent({ action: DatabaseEventAction.DELETED, objectMetadataItem: objectMetadata, + flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps, workspaceId: this.internalContext.workspaceId, entities: formattedBefore, authContext: this.authContext, diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-update-query-builder.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-update-query-builder.ts index e87e1fd1f1a..e4a83908df9 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-update-query-builder.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-update-query-builder.ts @@ -95,7 +95,9 @@ export class WorkspaceUpdateQueryBuilder< validateQueryIsPermittedOrThrow({ expressionMap: this.expressionMap, objectsPermissions: this.objectRecordsPermissions, - objectMetadataMaps: this.internalContext.objectMetadataMaps, + flatObjectMetadataMaps: this.internalContext.flatObjectMetadataMaps, + flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps, + objectIdByNameSingular: this.internalContext.objectIdByNameSingular, shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, }); @@ -164,7 +166,8 @@ export class WorkspaceUpdateQueryBuilder< const formattedBefore = formatResult( before, objectMetadata, - this.internalContext.objectMetadataMaps, + this.internalContext.flatObjectMetadataMaps, + this.internalContext.flatFieldMetadataMaps, ); const result = await super.execute(); @@ -174,13 +177,15 @@ export class WorkspaceUpdateQueryBuilder< const formattedAfter = formatResult( after, objectMetadata, - this.internalContext.objectMetadataMaps, + this.internalContext.flatObjectMetadataMaps, + this.internalContext.flatFieldMetadataMaps, ); this.internalContext.eventEmitterService.emitDatabaseBatchEvent( formatTwentyOrmEventToDatabaseBatchEvent({ action: DatabaseEventAction.UPDATED, objectMetadataItem: objectMetadata, + flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps, workspaceId: this.internalContext.workspaceId, entities: formattedAfter, beforeEntities: formattedBefore, @@ -192,6 +197,7 @@ export class WorkspaceUpdateQueryBuilder< formatTwentyOrmEventToDatabaseBatchEvent({ action: DatabaseEventAction.UPSERTED, objectMetadataItem: objectMetadata, + flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps, workspaceId: this.internalContext.workspaceId, entities: formattedAfter, beforeEntities: formattedBefore, @@ -202,7 +208,8 @@ export class WorkspaceUpdateQueryBuilder< const formattedResult = formatResult( result.raw, objectMetadata, - this.internalContext.objectMetadataMaps, + this.internalContext.flatObjectMetadataMaps, + this.internalContext.flatFieldMetadataMaps, ); return { @@ -235,7 +242,9 @@ export class WorkspaceUpdateQueryBuilder< validateQueryIsPermittedOrThrow({ expressionMap: fakeExpressionMapToValidatePermissions, objectsPermissions: this.objectRecordsPermissions, - objectMetadataMaps: this.internalContext.objectMetadataMaps, + flatObjectMetadataMaps: this.internalContext.flatObjectMetadataMaps, + flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps, + objectIdByNameSingular: this.internalContext.objectIdByNameSingular, shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, }); } @@ -265,7 +274,8 @@ export class WorkspaceUpdateQueryBuilder< const formattedBefore = formatResult( beforeRecords, objectMetadata, - this.internalContext.objectMetadataMaps, + this.internalContext.flatObjectMetadataMaps, + this.internalContext.flatFieldMetadataMaps, ); const results: UpdateResult[] = []; @@ -317,13 +327,15 @@ export class WorkspaceUpdateQueryBuilder< const formattedAfter = formatResult( afterRecords, objectMetadata, - this.internalContext.objectMetadataMaps, + this.internalContext.flatObjectMetadataMaps, + this.internalContext.flatFieldMetadataMaps, ); this.internalContext.eventEmitterService.emitDatabaseBatchEvent( formatTwentyOrmEventToDatabaseBatchEvent({ action: DatabaseEventAction.UPDATED, objectMetadataItem: objectMetadata, + flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps, workspaceId: this.internalContext.workspaceId, entities: formattedAfter, beforeEntities: formattedBefore, @@ -335,6 +347,7 @@ export class WorkspaceUpdateQueryBuilder< formatTwentyOrmEventToDatabaseBatchEvent({ action: DatabaseEventAction.UPSERTED, objectMetadataItem: objectMetadata, + flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps, workspaceId: this.internalContext.workspaceId, entities: formattedAfter, beforeEntities: formattedBefore, @@ -345,7 +358,8 @@ export class WorkspaceUpdateQueryBuilder< const formattedResults = formatResult( results.flatMap((result) => result.raw), objectMetadata, - this.internalContext.objectMetadataMaps, + this.internalContext.flatObjectMetadataMaps, + this.internalContext.flatFieldMetadataMaps, ); return { @@ -395,7 +409,11 @@ export class WorkspaceUpdateQueryBuilder< mainAliasTarget, ); - const formattedUpdateSet = formatData(_values, objectMetadata); + const formattedUpdateSet = formatData( + _values, + objectMetadata, + this.internalContext.flatFieldMetadataMaps, + ); return super.set(formattedUpdateSet as QueryDeepPartialEntity); } @@ -456,7 +474,11 @@ export class WorkspaceUpdateQueryBuilder< this.manyInputs = inputs.map((input) => ({ criteria: input.criteria, - partialEntity: formatData(input.partialEntity, objectMetadata), + partialEntity: formatData( + input.partialEntity, + objectMetadata, + this.internalContext.flatFieldMetadataMaps, + ), })); return this; diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts index 25cc5064b68..508fe747daa 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts @@ -37,13 +37,17 @@ import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/g export class WorkspaceRepository< T extends ObjectLiteral, > extends Repository { - private readonly internalContext: WorkspaceInternalContext; + private readonly _internalContext: WorkspaceInternalContext; private shouldBypassPermissionChecks: boolean; private featureFlagMap: FeatureFlagMap; public readonly objectRecordsPermissions?: ObjectsPermissions; private authContext?: AuthContext; declare manager: WorkspaceEntityManager; + get internalContext(): WorkspaceInternalContext { + return this._internalContext; + } + constructor( internalContext: WorkspaceInternalContext, target: EntityTarget, @@ -55,7 +59,7 @@ export class WorkspaceRepository< authContext?: AuthContext, ) { super(target, manager, queryRunner); - this.internalContext = internalContext; + this._internalContext = internalContext; this.featureFlagMap = featureFlagMap; this.objectRecordsPermissions = objectRecordsPermissions; this.shouldBypassPermissionChecks = shouldBypassPermissionChecks; @@ -79,7 +83,7 @@ export class WorkspaceRepository< return new WorkspaceSelectQueryBuilder( queryBuilder, this.objectRecordsPermissions, - this.internalContext, + this._internalContext, this.shouldBypassPermissionChecks, this.authContext ?? {}, this.featureFlagMap, @@ -944,7 +948,10 @@ export class WorkspaceRepository< * PRIVATE METHODS */ private async getObjectMetadataFromTarget() { - return getObjectMetadataFromEntityTarget(this.target, this.internalContext); + return getObjectMetadataFromEntityTarget( + this.target, + this._internalContext, + ); } private async transformOptions< @@ -968,6 +975,10 @@ export class WorkspaceRepository< private async formatData(data: T): Promise { const objectMetadata = await this.getObjectMetadataFromTarget(); - return formatData(data, objectMetadata) as T; + return formatData( + data, + objectMetadata, + this._internalContext.flatFieldMetadataMaps, + ) as T; } } diff --git a/packages/twenty-server/src/engine/twenty-orm/storage/workspace-context.storage.ts b/packages/twenty-server/src/engine/twenty-orm/storage/workspace-context.storage.ts index 97ed0d55caf..fa71149d6f3 100644 --- a/packages/twenty-server/src/engine/twenty-orm/storage/workspace-context.storage.ts +++ b/packages/twenty-server/src/engine/twenty-orm/storage/workspace-context.storage.ts @@ -6,11 +6,17 @@ import { type EntitySchema } from 'typeorm'; import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface'; import { type FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; export type WorkspaceContext = { authContext: WorkspaceAuthContext; - objectMetadataMaps: ObjectMetadataMaps; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; + flatIndexMaps: FlatEntityMaps; + objectIdByNameSingular: Record; metadataVersion: number; featureFlagsMap: Record; permissionsPerRoleId: ObjectsPermissionsByRoleId; diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/compute-relation-connect-query-configs.util.spec.ts b/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/compute-relation-connect-query-configs.util.spec.ts index 198a2bef9df..0de37cf278a 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/compute-relation-connect-query-configs.util.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/compute-relation-connect-query-configs.util.spec.ts @@ -2,147 +2,359 @@ import { FieldMetadataType } from 'twenty-shared/types'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { computeRelationConnectQueryConfigs } from 'src/engine/twenty-orm/utils/compute-relation-connect-query-configs.util'; describe('computeRelationConnectQueryConfigs', () => { - const personMetadata = { + const personFields: FlatFieldMetadata[] = [ + { + id: 'person-id-field-id', + name: 'id', + type: FieldMetadataType.UUID, + label: 'id', + objectMetadataId: 'person-object-metadata-id', + isNullable: false, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), + universalIdentifier: 'person-id-field-id', + viewFieldIds: [], + viewFilterIds: [], + viewGroupIds: [], + kanbanAggregateOperationViewIds: [], + calendarViewIds: [], + applicationId: null, + } as unknown as FlatFieldMetadata, + { + id: 'person-name-field-id', + name: 'name', + type: FieldMetadataType.FULL_NAME, + label: 'name', + objectMetadataId: 'person-object-metadata-id', + isNullable: true, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), + universalIdentifier: 'person-name-field-id', + viewFieldIds: [], + viewFilterIds: [], + viewGroupIds: [], + kanbanAggregateOperationViewIds: [], + calendarViewIds: [], + applicationId: null, + } as unknown as FlatFieldMetadata, + { + id: 'person-company-1-field-id', + name: 'company-related-to-1', + type: FieldMetadataType.RELATION, + label: 'company-related-to-1', + objectMetadataId: 'person-object-metadata-id', + isNullable: true, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), + universalIdentifier: 'person-company-1-field-id', + viewFieldIds: [], + viewFilterIds: [], + viewGroupIds: [], + kanbanAggregateOperationViewIds: [], + calendarViewIds: [], + applicationId: null, + relationTargetObjectMetadataId: 'company-object-metadata-id', + relationTargetFieldMetadataId: 'company-id-field-id', + settings: { + relationType: RelationType.MANY_TO_ONE, + joinColumnName: 'company-related-to-1Id', + }, + } as unknown as FlatFieldMetadata, + { + id: 'person-company-2-field-id', + name: 'company-related-to-2', + type: FieldMetadataType.RELATION, + label: 'company-related-to-2', + objectMetadataId: 'person-object-metadata-id', + isNullable: true, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), + universalIdentifier: 'person-company-2-field-id', + viewFieldIds: [], + viewFilterIds: [], + viewGroupIds: [], + kanbanAggregateOperationViewIds: [], + calendarViewIds: [], + applicationId: null, + relationTargetObjectMetadataId: 'company-object-metadata-id', + relationTargetFieldMetadataId: 'company-id-field-id', + settings: { + relationType: RelationType.MANY_TO_ONE, + joinColumnName: 'company-related-to-2Id', + }, + } as unknown as FlatFieldMetadata, + ]; + + const companyFields: FlatFieldMetadata[] = [ + { + id: 'company-id-field-id', + name: 'id', + type: FieldMetadataType.UUID, + label: 'id', + objectMetadataId: 'company-object-metadata-id', + isNullable: false, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), + universalIdentifier: 'company-id-field-id', + viewFieldIds: [], + viewFilterIds: [], + viewGroupIds: [], + kanbanAggregateOperationViewIds: [], + calendarViewIds: [], + applicationId: null, + } as unknown as FlatFieldMetadata, + { + id: 'company-name-field-id', + name: 'name', + type: FieldMetadataType.TEXT, + label: 'name', + objectMetadataId: 'company-object-metadata-id', + isNullable: true, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), + universalIdentifier: 'company-name-field-id', + viewFieldIds: [], + viewFilterIds: [], + viewGroupIds: [], + kanbanAggregateOperationViewIds: [], + calendarViewIds: [], + applicationId: null, + } as unknown as FlatFieldMetadata, + { + id: 'company-description-field-id', + name: 'description', + type: FieldMetadataType.TEXT, + label: 'description', + objectMetadataId: 'company-object-metadata-id', + isNullable: true, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), + universalIdentifier: 'company-description-field-id', + viewFieldIds: [], + viewFilterIds: [], + viewGroupIds: [], + kanbanAggregateOperationViewIds: [], + calendarViewIds: [], + applicationId: null, + } as unknown as FlatFieldMetadata, + { + id: 'company-domain-name-field-id', + name: 'domainName', + type: FieldMetadataType.LINKS, + label: 'domainName', + objectMetadataId: 'company-object-metadata-id', + isNullable: true, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), + universalIdentifier: 'company-domain-name-field-id', + viewFieldIds: [], + viewFilterIds: [], + viewGroupIds: [], + kanbanAggregateOperationViewIds: [], + calendarViewIds: [], + applicationId: null, + } as unknown as FlatFieldMetadata, + { + id: 'company-address-field-id', + name: 'address', + type: FieldMetadataType.TEXT, + label: 'address', + objectMetadataId: 'company-object-metadata-id', + isNullable: true, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), + universalIdentifier: 'company-address-field-id', + viewFieldIds: [], + viewFilterIds: [], + viewGroupIds: [], + kanbanAggregateOperationViewIds: [], + calendarViewIds: [], + applicationId: null, + } as unknown as FlatFieldMetadata, + ]; + + const allFields = [...personFields, ...companyFields]; + + const flatFieldMetadataMaps: FlatEntityMaps = { + byId: allFields.reduce( + (acc, field) => { + acc[field.id] = field; + + return acc; + }, + {} as Record, + ), + idByUniversalIdentifier: allFields.reduce( + (acc, field) => { + acc[field.universalIdentifier] = field.id; + + return acc; + }, + {} as Record, + ), + universalIdentifiersByApplicationId: {}, + }; + + const createFlatObjectMetadata = ( + partial: Partial & { + id: string; + nameSingular: string; + fieldMetadataIds: string[]; + indexMetadataIds: string[]; + }, + ): FlatObjectMetadata => + ({ + namePlural: `${partial.nameSingular}s`, + labelSingular: partial.nameSingular, + labelPlural: `${partial.nameSingular}s`, + icon: 'Icon', + targetTableName: partial.nameSingular, + workspaceId: 'workspace-id', + isCustom: false, + isRemote: false, + isActive: true, + isSystem: false, + isAuditLogged: true, + isSearchable: true, + universalIdentifier: partial.id, + viewIds: [], + applicationId: null, + isLabelSyncedWithName: false, + createdAt: new Date(), + updatedAt: new Date(), + shortcut: null, + description: null, + standardOverrides: null, + isUIReadOnly: false, + standardId: null, + labelIdentifierFieldMetadataId: null, + imageIdentifierFieldMetadataId: null, + duplicateCriteria: null, + ...partial, + }) as FlatObjectMetadata; + + const personMetadata = createFlatObjectMetadata({ id: 'person-object-metadata-id', nameSingular: 'person', - fieldsById: { - 'person-id-field-id': { - id: 'person-id-field-id', - name: 'id', - type: FieldMetadataType.UUID, - label: 'id', - }, - 'person-name-field-id': { - id: 'person-name-field-id', - name: 'name', - type: FieldMetadataType.FULL_NAME, - label: 'name', - }, - 'person-company-1-field-id': { - id: 'person-company-1-field-id', - name: 'company-related-to-1', - type: FieldMetadataType.RELATION, - label: 'company-related-to-1', - relationTargetObjectMetadataId: 'company-object-metadata-id', - relationTargetFieldMetadataId: 'company-id-field-id', - settings: { - relationType: RelationType.MANY_TO_ONE, - }, - }, - 'person-company-2-field-id': { - id: 'person-company-2-field-id', - name: 'company-related-to-2', - type: FieldMetadataType.RELATION, - label: 'company-related-to-2', - relationTargetObjectMetadataId: 'company-object-metadata-id', - relationTargetFieldMetadataId: 'company-id-field-id', - settings: { - relationType: RelationType.MANY_TO_ONE, - }, - }, - }, - fieldIdByName: { - id: 'person-id-field-id', - name: 'person-name-field-id', - 'company-related-to-1': 'person-company-1-field-id', - 'company-related-to-2': 'person-company-2-field-id', - }, - } as unknown as ObjectMetadataItemWithFieldMaps; + indexMetadataIds: [], + fieldMetadataIds: personFields.map((f) => f.id), + }); - const companyMetadata = { + const companyMetadata = createFlatObjectMetadata({ id: 'company-object-metadata-id', nameSingular: 'company', - indexMetadatas: [ - { - id: 'company-id-index-metadata-id', - name: 'company-id-index-metadata-name', - indexFieldMetadatas: [ - { - fieldMetadataId: 'company-id-field-id', - }, - ], - isUnique: true, - }, - { - id: 'company-domain-index-metadata-id', - name: 'company-domain-index-metadata-name', - indexFieldMetadatas: [ - { - fieldMetadataId: 'company-domain-name-field-id', - }, - ], - isUnique: true, - }, - { - id: 'company-composite-index-metadata-id', - name: 'company-composite-index-metadata-name', - indexFieldMetadatas: [ - { - fieldMetadataId: 'company-name-field-id', - }, - { - fieldMetadataId: 'company-description-field-id', - }, - ], - isUnique: true, - }, + indexMetadataIds: [ + 'company-id-index-metadata-id', + 'company-domain-index-metadata-id', + 'company-composite-index-metadata-id', ], - fieldsById: { - 'company-id-field-id': { - id: 'company-id-field-id', - name: 'id', - type: FieldMetadataType.UUID, - label: 'id', - }, - 'company-name-field-id': { - id: 'company-name-field-id', - name: 'name', - type: FieldMetadataType.TEXT, - label: 'name', - }, - 'company-description-field-id': { - id: 'company-description-field-id', - name: 'description', - type: FieldMetadataType.TEXT, - label: 'description', - }, - 'company-domain-name-field-id': { - id: 'company-domain-name-field-id', - name: 'domainName', - type: FieldMetadataType.LINKS, - label: 'domainName', - }, - 'company-address-field-id': { - id: 'company-address-field-id', - name: 'address', - type: FieldMetadataType.TEXT, - label: 'address', - }, - }, - fieldIdByName: { - id: 'company-id-field-id', - name: 'company-name-field-id', - description: 'company-description-field-id', - domainName: 'company-domain-name-field-id', - address: 'company-address-field-id', - }, - } as unknown as ObjectMetadataItemWithFieldMaps; + fieldMetadataIds: companyFields.map((f) => f.id), + }); - const objectMetadataMaps = { + const flatObjectMetadataMaps: FlatEntityMaps = { byId: { 'person-object-metadata-id': personMetadata, 'company-object-metadata-id': companyMetadata, }, - idByNameSingular: { - person: 'person-object-metadata-id', - company: 'company-object-metadata-id', + idByUniversalIdentifier: { + 'person-object-metadata-id': 'person-object-metadata-id', + 'company-object-metadata-id': 'company-object-metadata-id', }, - } as ObjectMetadataMaps; + universalIdentifiersByApplicationId: {}, + }; + + const createFlatIndexFieldMetadata = ( + id: string, + fieldMetadataId: string, + indexMetadataId: string, + order: number, + ) => ({ + id, + fieldMetadataId, + indexMetadataId, + order, + createdAt: new Date(), + updatedAt: new Date(), + }); + + const flatIndexMaps: FlatEntityMaps = { + byId: { + 'company-id-index-metadata-id': { + id: 'company-id-index-metadata-id', + name: 'company-id-index-metadata-name', + isUnique: true, + objectMetadataId: 'company-object-metadata-id', + universalIdentifier: 'company-id-index-metadata-id', + flatIndexFieldMetadatas: [ + createFlatIndexFieldMetadata( + 'company-id-index-field-metadata-id', + 'company-id-field-id', + 'company-id-index-metadata-id', + 0, + ), + ], + } as unknown as FlatIndexMetadata, + 'company-domain-index-metadata-id': { + id: 'company-domain-index-metadata-id', + name: 'company-domain-index-metadata-name', + isUnique: true, + objectMetadataId: 'company-object-metadata-id', + universalIdentifier: 'company-domain-index-metadata-id', + flatIndexFieldMetadatas: [ + createFlatIndexFieldMetadata( + 'company-domain-index-field-metadata-id', + 'company-domain-name-field-id', + 'company-domain-index-metadata-id', + 0, + ), + ], + } as unknown as FlatIndexMetadata, + 'company-composite-index-metadata-id': { + id: 'company-composite-index-metadata-id', + name: 'company-composite-index-metadata-name', + isUnique: true, + objectMetadataId: 'company-object-metadata-id', + universalIdentifier: 'company-composite-index-metadata-id', + flatIndexFieldMetadatas: [ + createFlatIndexFieldMetadata( + 'company-name-index-field-metadata-id', + 'company-name-field-id', + 'company-composite-index-metadata-id', + 0, + ), + createFlatIndexFieldMetadata( + 'company-description-index-field-metadata-id', + 'company-description-field-id', + 'company-composite-index-metadata-id', + 1, + ), + ], + } as unknown as FlatIndexMetadata, + }, + idByUniversalIdentifier: { + 'company-id-index-metadata-id': 'company-id-index-metadata-id', + 'company-domain-index-metadata-id': 'company-domain-index-metadata-id', + 'company-composite-index-metadata-id': + 'company-composite-index-metadata-id', + }, + universalIdentifiersByApplicationId: {}, + }; it('should return an empty object if no connect fields are found', () => { const peopleEntityInputs = [ @@ -159,7 +371,9 @@ describe('computeRelationConnectQueryConfigs', () => { const result = computeRelationConnectQueryConfigs( peopleEntityInputs, personMetadata, - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatIndexMaps, {}, ); @@ -187,7 +401,9 @@ describe('computeRelationConnectQueryConfigs', () => { computeRelationConnectQueryConfigs( peopleEntityInputs, personMetadata, - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatIndexMaps, relationConnectQueryFieldsByEntityIndex, ); }).toThrow('Connect is not allowed for name on person'); @@ -213,7 +429,9 @@ describe('computeRelationConnectQueryConfigs', () => { computeRelationConnectQueryConfigs( peopleEntityInputs, personMetadata, - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatIndexMaps, relationConnectQueryFieldsByEntityIndex, ); }).toThrow( @@ -255,7 +473,9 @@ describe('computeRelationConnectQueryConfigs', () => { computeRelationConnectQueryConfigs( peopleEntityInputs, personMetadata, - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatIndexMaps, relationConnectQueryFieldsByEntityIndex, ); }).toThrow( @@ -304,7 +524,9 @@ describe('computeRelationConnectQueryConfigs', () => { computeRelationConnectQueryConfigs( peopleEntityInputs, personMetadata, - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatIndexMaps, relationConnectQueryFieldsByEntityIndex, ); }).toThrow( @@ -374,7 +596,9 @@ describe('computeRelationConnectQueryConfigs', () => { const result = computeRelationConnectQueryConfigs( peopleEntityInputs, personMetadata, - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatIndexMaps, relationConnectQueryFieldsByEntityIndex, ); @@ -392,12 +616,11 @@ describe('computeRelationConnectQueryConfigs', () => { relationFieldName: 'company-related-to-1Id', targetObjectName: 'company', uniqueConstraintFields: [ - { + expect.objectContaining({ id: 'company-domain-name-field-id', - label: 'domainName', name: 'domainName', type: FieldMetadataType.LINKS, - }, + }), ], }, { @@ -410,12 +633,11 @@ describe('computeRelationConnectQueryConfigs', () => { relationFieldName: 'company-related-to-2Id', targetObjectName: 'company', uniqueConstraintFields: [ - { + expect.objectContaining({ id: 'company-id-field-id', - label: 'id', name: 'id', type: FieldMetadataType.UUID, - }, + }), ], }, ]); diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/format-twenty-orm-event-to-database-batch-event.util.spec.ts b/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/format-twenty-orm-event-to-database-batch-event.util.spec.ts index 485b89cf19f..5d805624045 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/format-twenty-orm-event-to-database-batch-event.util.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/format-twenty-orm-event-to-database-batch-event.util.spec.ts @@ -2,43 +2,80 @@ import { FieldMetadataType } from 'twenty-shared/types'; import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; import { type ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { TwentyORMException, TwentyORMExceptionCode, } from 'src/engine/twenty-orm/exceptions/twenty-orm.exception'; import { formatTwentyOrmEventToDatabaseBatchEvent } from 'src/engine/twenty-orm/utils/format-twenty-orm-event-to-database-batch-event.util'; -import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock'; -import { getMockObjectMetadataItemWithFieldsMaps } from 'src/utils/__test__/get-object-metadata-item-with-fields-maps.mock'; describe('formatTwentyOrmEventToDatabaseBatchEvent', () => { - const objectMetadataItemWithFieldMaps = - getMockObjectMetadataItemWithFieldsMaps({ - id: 'object-id', - workspaceId: 'workspace-id', - nameSingular: 'person', - namePlural: 'people', - indexMetadatas: [], - fieldIdByJoinColumnName: {}, - fieldIdByName: { - name: 'name-id', - age: 'age-id', - fullName: 'fullname-id', - }, - fieldsById: { - 'name-id': getMockFieldMetadataEntity({ - workspaceId: 'workspace-id', - objectMetadataId: 'object-id', - id: 'name-id', - type: FieldMetadataType.TEXT, - name: 'name', - label: 'Name', - isLabelSyncedWithName: true, - isNullable: true, - createdAt: new Date(), - updatedAt: new Date(), - }), - }, - }); + const workspaceId = 'workspace-id'; + const objectMetadataId = 'object-id'; + + const createMockField = ( + overrides: Partial & { + id: string; + name: string; + type: FieldMetadataType; + }, + ): FlatFieldMetadata => + ({ + workspaceId, + objectMetadataId, + isNullable: true, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), + universalIdentifier: overrides.id, + viewFieldIds: [], + viewFilterIds: [], + viewGroupIds: [], + kanbanAggregateOperationViewIds: [], + calendarViewIds: [], + applicationId: null, + label: overrides.name, + ...overrides, + }) as FlatFieldMetadata; + + const nameField = createMockField({ + id: 'name-id', + type: FieldMetadataType.TEXT, + name: 'name', + label: 'Name', + }); + + const flatFieldMetadataMaps: FlatEntityMaps = { + byId: { + 'name-id': nameField, + }, + idByUniversalIdentifier: { + 'name-id': 'name-id', + }, + universalIdentifiersByApplicationId: {}, + }; + + const flatObjectMetadata: FlatObjectMetadata = { + id: objectMetadataId, + workspaceId, + nameSingular: 'person', + namePlural: 'people', + labelSingular: 'Person', + labelPlural: 'People', + isCustom: false, + isRemote: false, + isActive: true, + isSystem: false, + createdAt: new Date(), + updatedAt: new Date(), + universalIdentifier: objectMetadataId, + fieldMetadataIds: ['name-id'], + indexMetadataIds: [], + viewIds: [], + applicationId: null, + } as unknown as FlatObjectMetadata; const mockWorkspaceId = 'workspace-id'; const mockAuthContext = { @@ -73,7 +110,8 @@ describe('formatTwentyOrmEventToDatabaseBatchEvent', () => { try { formatTwentyOrmEventToDatabaseBatchEvent({ action: DatabaseEventAction.UPDATED, - objectMetadataItem: objectMetadataItemWithFieldMaps, + objectMetadataItem: flatObjectMetadata, + flatFieldMetadataMaps, workspaceId: mockWorkspaceId, authContext: mockAuthContext, entities: afterEntities, @@ -115,7 +153,8 @@ describe('formatTwentyOrmEventToDatabaseBatchEvent', () => { const result = formatTwentyOrmEventToDatabaseBatchEvent({ action: DatabaseEventAction.UPDATED, - objectMetadataItem: objectMetadataItemWithFieldMaps, + objectMetadataItem: flatObjectMetadata, + flatFieldMetadataMaps, workspaceId: mockWorkspaceId, authContext: mockAuthContext, entities: afterEntities, @@ -155,7 +194,8 @@ describe('formatTwentyOrmEventToDatabaseBatchEvent', () => { const result = formatTwentyOrmEventToDatabaseBatchEvent({ action: DatabaseEventAction.UPDATED, - objectMetadataItem: objectMetadataItemWithFieldMaps, + objectMetadataItem: flatObjectMetadata, + flatFieldMetadataMaps, workspaceId: mockWorkspaceId, authContext: mockAuthContext, entities: afterEntity, diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/get-column-name-to-field-metadata-id.util.spec.ts b/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/get-column-name-to-field-metadata-id.util.spec.ts index f671f48632c..469f35f44cd 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/get-column-name-to-field-metadata-id.util.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/get-column-name-to-field-metadata-id.util.spec.ts @@ -1,19 +1,20 @@ import { FieldMetadataType } from 'twenty-shared/types'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { getColumnNameToFieldMetadataIdMap } from 'src/engine/twenty-orm/utils/get-column-name-to-field-metadata-id.util'; describe('getColumnNameToFieldMetadataIdMap', () => { - const createMockObjectMetadataItemWithFieldMaps = ( - fieldsById: Record, - ): ObjectMetadataItemWithFieldMaps => + const createMockFlatObjectMetadata = ( + fieldMetadataIds: string[], + ): FlatObjectMetadata => ({ id: 'test-object-id', nameSingular: 'test', namePlural: 'tests', labelSingular: 'Test', labelPlural: 'Tests', - description: 'Test object', icon: 'IconTest', targetTableName: 'test', isCustom: false, @@ -22,53 +23,87 @@ describe('getColumnNameToFieldMetadataIdMap', () => { isSystem: false, isAuditLogged: false, isSearchable: false, - labelIdentifierFieldMetadataId: '', - imageIdentifierFieldMetadataId: '', workspaceId: 'test-workspace-id', - indexMetadatas: [], - fieldsById, - fieldIdByName: {}, - fieldIdByJoinColumnName: {}, - }) as unknown as ObjectMetadataItemWithFieldMaps; + universalIdentifier: 'test-object-id', + indexMetadataIds: [], + fieldMetadataIds, + viewIds: [], + applicationId: null, + isLabelSyncedWithName: false, + createdAt: new Date(), + updatedAt: new Date(), + shortcut: null, + description: null, + standardOverrides: null, + isUIReadOnly: false, + standardId: null, + labelIdentifierFieldMetadataId: null, + imageIdentifierFieldMetadataId: null, + duplicateCriteria: null, + }) as FlatObjectMetadata; - const createMockFieldMetadata = ( + const createMockFlatFieldMetadata = ( id: string, name: string, type: FieldMetadataType, - ) => ({ - id, - name, - type, - label: name, - objectMetadataId: 'test-object-id', - isLabelSyncedWithName: true, - isNullable: true, - isUnique: false, - workspaceId: 'test-workspace-id', - createdAt: new Date(), - updatedAt: new Date(), + settings?: Record, + ): FlatFieldMetadata => + ({ + id, + name, + type, + label: name, + objectMetadataId: 'test-object-id', + isLabelSyncedWithName: true, + isNullable: true, + createdAt: new Date(), + updatedAt: new Date(), + universalIdentifier: id, + viewFieldIds: [], + viewFilterIds: [], + viewGroupIds: [], + kanbanAggregateOperationViewIds: [], + calendarViewIds: [], + applicationId: null, + ...(settings ? { settings } : {}), + }) as unknown as FlatFieldMetadata; + + const buildFlatFieldMetadataMaps = ( + fields: FlatFieldMetadata[], + ): FlatEntityMaps => ({ + byId: fields.reduce( + (acc, field) => { + acc[field.id] = field; + + return acc; + }, + {} as Record, + ), + idByUniversalIdentifier: fields.reduce( + (acc, field) => { + acc[field.universalIdentifier] = field.id; + + return acc; + }, + {} as Record, + ), + universalIdentifiersByApplicationId: {}, }); describe('with simple field types', () => { it('should return a map with column name to field metadata id for simple field types', () => { - const fieldsById = { - 'field-1': createMockFieldMetadata( - 'field-1', - 'name', - FieldMetadataType.TEXT, - ), - 'field-2': createMockFieldMetadata( - 'field-2', - 'age', - FieldMetadataType.NUMBER, - ), - }; - - const objectMetadataItemWithFieldMaps = - createMockObjectMetadataItemWithFieldMaps(fieldsById); + const fields = [ + createMockFlatFieldMetadata('field-1', 'name', FieldMetadataType.TEXT), + createMockFlatFieldMetadata('field-2', 'age', FieldMetadataType.NUMBER), + ]; + const flatObjectMetadata = createMockFlatObjectMetadata( + fields.map((f) => f.id), + ); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields); const result = getColumnNameToFieldMetadataIdMap( - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ); expect(result['name']).toBe('field-1'); @@ -79,19 +114,21 @@ describe('getColumnNameToFieldMetadataIdMap', () => { describe('with composite field types', () => { it('should return column names to field metadata id for FULL_NAME composite type', () => { - const fieldsById = { - 'field-1': createMockFieldMetadata( + const fields = [ + createMockFlatFieldMetadata( 'field-1', 'fullName', FieldMetadataType.FULL_NAME, ), - }; - - const objectMetadataItemWithFieldMaps = - createMockObjectMetadataItemWithFieldMaps(fieldsById); + ]; + const flatObjectMetadata = createMockFlatObjectMetadata( + fields.map((f) => f.id), + ); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields); const result = getColumnNameToFieldMetadataIdMap( - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ); expect(result['fullNameFirstName']).toBe('field-1'); @@ -100,19 +137,21 @@ describe('getColumnNameToFieldMetadataIdMap', () => { }); it('should return column names to field metadata id for CURRENCY composite type', () => { - const fieldsById = { - 'field-1': createMockFieldMetadata( + const fields = [ + createMockFlatFieldMetadata( 'field-1', 'price', FieldMetadataType.CURRENCY, ), - }; - - const objectMetadataItemWithFieldMaps = - createMockObjectMetadataItemWithFieldMaps(fieldsById); + ]; + const flatObjectMetadata = createMockFlatObjectMetadata( + fields.map((f) => f.id), + ); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields); const result = getColumnNameToFieldMetadataIdMap( - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ); expect(result['priceAmountMicros']).toBe('field-1'); @@ -121,29 +160,27 @@ describe('getColumnNameToFieldMetadataIdMap', () => { }); it('should handle multiple composite fields', () => { - const fieldsById = { - 'field-1': createMockFieldMetadata( + const fields = [ + createMockFlatFieldMetadata( 'field-1', 'fullName', FieldMetadataType.FULL_NAME, ), - 'field-2': createMockFieldMetadata( + createMockFlatFieldMetadata( 'field-2', 'price', FieldMetadataType.CURRENCY, ), - 'field-3': createMockFieldMetadata( - 'field-3', - 'name', - FieldMetadataType.TEXT, - ), - }; - - const objectMetadataItemWithFieldMaps = - createMockObjectMetadataItemWithFieldMaps(fieldsById); + createMockFlatFieldMetadata('field-3', 'name', FieldMetadataType.TEXT), + ]; + const flatObjectMetadata = createMockFlatObjectMetadata( + fields.map((f) => f.id), + ); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields); const result = getColumnNameToFieldMetadataIdMap( - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ); expect(result['fullNameFirstName']).toBe('field-1'); @@ -157,34 +194,28 @@ describe('getColumnNameToFieldMetadataIdMap', () => { describe('with mixed field types', () => { it('should handle both simple and composite field types', () => { - const fieldsById = { - 'field-1': createMockFieldMetadata( - 'field-1', - 'name', - FieldMetadataType.TEXT, - ), - 'field-2': createMockFieldMetadata( + const fields = [ + createMockFlatFieldMetadata('field-1', 'name', FieldMetadataType.TEXT), + createMockFlatFieldMetadata( 'field-2', 'fullName', FieldMetadataType.FULL_NAME, ), - 'field-3': createMockFieldMetadata( - 'field-3', - 'age', - FieldMetadataType.NUMBER, - ), - 'field-4': createMockFieldMetadata( + createMockFlatFieldMetadata('field-3', 'age', FieldMetadataType.NUMBER), + createMockFlatFieldMetadata( 'field-4', 'price', FieldMetadataType.CURRENCY, ), - }; - - const objectMetadataItemWithFieldMaps = - createMockObjectMetadataItemWithFieldMaps(fieldsById); + ]; + const flatObjectMetadata = createMockFlatObjectMetadata( + fields.map((f) => f.id), + ); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields); const result = getColumnNameToFieldMetadataIdMap( - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ); expect(result['name']).toBe('field-1'); @@ -199,30 +230,26 @@ describe('getColumnNameToFieldMetadataIdMap', () => { describe('with relation field types', () => { it('should handle relation field types with join column name', () => { - const fieldsById = { - 'field-1': { - ...createMockFieldMetadata( - 'field-1', - 'company', - FieldMetadataType.RELATION, - ), - settings: { + const fields = [ + createMockFlatFieldMetadata( + 'field-1', + 'company', + FieldMetadataType.RELATION, + { relationType: 'ONE_TO_ONE', joinColumnName: 'companyId', }, - }, - 'field-2': createMockFieldMetadata( - 'field-2', - 'name', - FieldMetadataType.TEXT, ), - }; - - const objectMetadataItemWithFieldMaps = - createMockObjectMetadataItemWithFieldMaps(fieldsById); + createMockFlatFieldMetadata('field-2', 'name', FieldMetadataType.TEXT), + ]; + const flatObjectMetadata = createMockFlatObjectMetadata( + fields.map((f) => f.id), + ); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields); const result = getColumnNameToFieldMetadataIdMap( - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ); expect(result['companyId']).toBe('field-1'); @@ -232,30 +259,26 @@ describe('getColumnNameToFieldMetadataIdMap', () => { }); it('should skip ONE_TO_MANY relation field types', () => { - const fieldsById = { - 'field-1': { - ...createMockFieldMetadata( - 'field-1', - 'employees', - FieldMetadataType.RELATION, - ), - settings: { + const fields = [ + createMockFlatFieldMetadata( + 'field-1', + 'employees', + FieldMetadataType.RELATION, + { relationType: 'ONE_TO_MANY', joinColumnName: 'companyId', }, - }, - 'field-2': createMockFieldMetadata( - 'field-2', - 'name', - FieldMetadataType.TEXT, ), - }; - - const objectMetadataItemWithFieldMaps = - createMockObjectMetadataItemWithFieldMaps(fieldsById); + createMockFlatFieldMetadata('field-2', 'name', FieldMetadataType.TEXT), + ]; + const flatObjectMetadata = createMockFlatObjectMetadata( + fields.map((f) => f.id), + ); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields); const result = getColumnNameToFieldMetadataIdMap( - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ); expect(result['name']).toBe('field-2'); diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/get-field-metadata-id-to-column-names-map.util.spec.ts b/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/get-field-metadata-id-to-column-names-map.util.spec.ts index 9e7cc6e8519..6cc35623c97 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/get-field-metadata-id-to-column-names-map.util.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/get-field-metadata-id-to-column-names-map.util.spec.ts @@ -1,19 +1,20 @@ import { FieldMetadataType } from 'twenty-shared/types'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { getFieldMetadataIdToColumnNamesMap } from 'src/engine/twenty-orm/utils/get-field-metadata-id-to-column-names-map.util'; describe('getFieldMetadataIdToColumnNamesMap', () => { - const createMockObjectMetadataItemWithFieldMaps = ( - fieldsById: Record, - ): ObjectMetadataItemWithFieldMaps => + const createMockFlatObjectMetadata = ( + fieldMetadataIds: string[], + ): FlatObjectMetadata => ({ id: 'test-object-id', nameSingular: 'test', namePlural: 'tests', labelSingular: 'Test', labelPlural: 'Tests', - description: 'Test object', icon: 'IconTest', targetTableName: 'test', isCustom: false, @@ -22,53 +23,85 @@ describe('getFieldMetadataIdToColumnNamesMap', () => { isSystem: false, isAuditLogged: false, isSearchable: false, - labelIdentifierFieldMetadataId: '', - imageIdentifierFieldMetadataId: '', workspaceId: 'test-workspace-id', - indexMetadatas: [], - fieldsById, - fieldIdByName: {}, - fieldIdByJoinColumnName: {}, - }) as unknown as ObjectMetadataItemWithFieldMaps; + universalIdentifier: 'test-object-id', + indexMetadataIds: [], + fieldMetadataIds, + viewIds: [], + applicationId: null, + isLabelSyncedWithName: false, + createdAt: new Date(), + updatedAt: new Date(), + shortcut: null, + description: null, + standardOverrides: null, + isUIReadOnly: false, + standardId: null, + labelIdentifierFieldMetadataId: null, + imageIdentifierFieldMetadataId: null, + duplicateCriteria: null, + }) as FlatObjectMetadata; - const createMockFieldMetadata = ( + const createMockFlatFieldMetadata = ( id: string, name: string, type: FieldMetadataType, - ) => ({ - id, - name, - type, - label: name, - objectMetadataId: 'test-object-id', - isLabelSyncedWithName: true, - isNullable: true, - isUnique: false, - workspaceId: 'test-workspace-id', - createdAt: new Date(), - updatedAt: new Date(), + ): FlatFieldMetadata => + ({ + id, + name, + type, + label: name, + objectMetadataId: 'test-object-id', + isLabelSyncedWithName: true, + isNullable: true, + createdAt: new Date(), + updatedAt: new Date(), + universalIdentifier: id, + viewFieldIds: [], + viewFilterIds: [], + viewGroupIds: [], + kanbanAggregateOperationViewIds: [], + calendarViewIds: [], + applicationId: null, + }) as unknown as FlatFieldMetadata; + + const buildFlatFieldMetadataMaps = ( + fields: FlatFieldMetadata[], + ): FlatEntityMaps => ({ + byId: fields.reduce( + (acc, field) => { + acc[field.id] = field; + + return acc; + }, + {} as Record, + ), + idByUniversalIdentifier: fields.reduce( + (acc, field) => { + acc[field.universalIdentifier] = field.id; + + return acc; + }, + {} as Record, + ), + universalIdentifiersByApplicationId: {}, }); describe('with simple field types', () => { it('should return a map with single column name for simple field types', () => { - const fieldsById = { - 'field-1': createMockFieldMetadata( - 'field-1', - 'name', - FieldMetadataType.TEXT, - ), - 'field-2': createMockFieldMetadata( - 'field-2', - 'age', - FieldMetadataType.NUMBER, - ), - }; - - const objectMetadataItemWithFieldMaps = - createMockObjectMetadataItemWithFieldMaps(fieldsById); + const fields = [ + createMockFlatFieldMetadata('field-1', 'name', FieldMetadataType.TEXT), + createMockFlatFieldMetadata('field-2', 'age', FieldMetadataType.NUMBER), + ]; + const flatObjectMetadata = createMockFlatObjectMetadata( + fields.map((f) => f.id), + ); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields); const result = getFieldMetadataIdToColumnNamesMap( - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ); expect(result.get('field-1')).toEqual(['name']); @@ -79,19 +112,21 @@ describe('getFieldMetadataIdToColumnNamesMap', () => { describe('with composite field types', () => { it('should return multiple column names for FULL_NAME composite type', () => { - const fieldsById = { - 'field-1': createMockFieldMetadata( + const fields = [ + createMockFlatFieldMetadata( 'field-1', 'fullName', FieldMetadataType.FULL_NAME, ), - }; - - const objectMetadataItemWithFieldMaps = - createMockObjectMetadataItemWithFieldMaps(fieldsById); + ]; + const flatObjectMetadata = createMockFlatObjectMetadata( + fields.map((f) => f.id), + ); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields); const result = getFieldMetadataIdToColumnNamesMap( - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ); expect(result.get('field-1')).toEqual([ @@ -102,19 +137,21 @@ describe('getFieldMetadataIdToColumnNamesMap', () => { }); it('should return multiple column names for CURRENCY composite type', () => { - const fieldsById = { - 'field-1': createMockFieldMetadata( + const fields = [ + createMockFlatFieldMetadata( 'field-1', 'price', FieldMetadataType.CURRENCY, ), - }; - - const objectMetadataItemWithFieldMaps = - createMockObjectMetadataItemWithFieldMaps(fieldsById); + ]; + const flatObjectMetadata = createMockFlatObjectMetadata( + fields.map((f) => f.id), + ); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields); const result = getFieldMetadataIdToColumnNamesMap( - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ); expect(result.get('field-1')).toEqual([ @@ -125,29 +162,27 @@ describe('getFieldMetadataIdToColumnNamesMap', () => { }); it('should handle multiple composite fields', () => { - const fieldsById = { - 'field-1': createMockFieldMetadata( + const fields = [ + createMockFlatFieldMetadata( 'field-1', 'fullName', FieldMetadataType.FULL_NAME, ), - 'field-2': createMockFieldMetadata( + createMockFlatFieldMetadata( 'field-2', 'price', FieldMetadataType.CURRENCY, ), - 'field-3': createMockFieldMetadata( - 'field-3', - 'name', - FieldMetadataType.TEXT, - ), - }; - - const objectMetadataItemWithFieldMaps = - createMockObjectMetadataItemWithFieldMaps(fieldsById); + createMockFlatFieldMetadata('field-3', 'name', FieldMetadataType.TEXT), + ]; + const flatObjectMetadata = createMockFlatObjectMetadata( + fields.map((f) => f.id), + ); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields); const result = getFieldMetadataIdToColumnNamesMap( - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ); expect(result.get('field-1')).toEqual([ @@ -165,34 +200,28 @@ describe('getFieldMetadataIdToColumnNamesMap', () => { describe('with mixed field types', () => { it('should handle both simple and composite field types', () => { - const fieldsById = { - 'field-1': createMockFieldMetadata( - 'field-1', - 'name', - FieldMetadataType.TEXT, - ), - 'field-2': createMockFieldMetadata( + const fields = [ + createMockFlatFieldMetadata('field-1', 'name', FieldMetadataType.TEXT), + createMockFlatFieldMetadata( 'field-2', 'fullName', FieldMetadataType.FULL_NAME, ), - 'field-3': createMockFieldMetadata( - 'field-3', - 'age', - FieldMetadataType.NUMBER, - ), - 'field-4': createMockFieldMetadata( + createMockFlatFieldMetadata('field-3', 'age', FieldMetadataType.NUMBER), + createMockFlatFieldMetadata( 'field-4', 'price', FieldMetadataType.CURRENCY, ), - }; - - const objectMetadataItemWithFieldMaps = - createMockObjectMetadataItemWithFieldMaps(fieldsById); + ]; + const flatObjectMetadata = createMockFlatObjectMetadata( + fields.map((f) => f.id), + ); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields); const result = getFieldMetadataIdToColumnNamesMap( - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ); expect(result.get('field-1')).toEqual(['name']); diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/compute-relation-connect-query-configs.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/compute-relation-connect-query-configs.util.ts index 1fc5390d0fa..908e27975e1 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/compute-relation-connect-query-configs.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/compute-relation-connect-query-configs.util.ts @@ -5,11 +5,13 @@ import { getUniqueConstraintsFields, isDefined } from 'twenty-shared/utils'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; +import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { type ConnectObject } from 'src/engine/twenty-orm/entity-manager/types/query-deep-partial-entity-with-nested-relation-fields.type'; import { type RelationConnectQueryConfig, @@ -26,8 +28,10 @@ import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata- export const computeRelationConnectQueryConfigs = ( entities: Record[], - objectMetadata: ObjectMetadataItemWithFieldMaps, - objectMetadataMap: ObjectMetadataMaps, + flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, + flatIndexMaps: FlatEntityMaps, relationConnectQueryFieldsByEntityIndex: RelationConnectQueryFieldsByEntityIndex, ) => { const allConnectQueryConfigs: Record = {}; @@ -48,8 +52,10 @@ export const computeRelationConnectQueryConfigs = ( } = computeRecordToConnectCondition( connectFieldName, connectObject, - objectMetadata, - objectMetadataMap, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatIndexMaps, entity, ); @@ -102,7 +108,7 @@ const updateConnectQueryConfigs = ( const createConnectQueryConfig = ( connectFieldName: string, recordToConnectCondition: UniqueConstraintCondition, - uniqueConstraintFields: FieldMetadataEntity[], + uniqueConstraintFields: FlatFieldMetadata[], targetObjectNameSingular: string, entityIndex: number, ) => { @@ -121,26 +127,33 @@ const createConnectQueryConfig = ( const computeRecordToConnectCondition = ( connectFieldName: string, connectObject: ConnectObject, - objectMetadata: ObjectMetadataItemWithFieldMaps, - objectMetadataMap: ObjectMetadataMaps, + flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, + flatIndexMaps: FlatEntityMaps, entity: Record, ): { recordToConnectCondition: UniqueConstraintCondition; - uniqueConstraintFields: FieldMetadataEntity[]; + uniqueConstraintFields: FlatFieldMetadata[]; targetObjectNameSingular: string; } => { - const field = - objectMetadata.fieldsById[objectMetadata.fieldIdByName[connectFieldName]]; + const { fieldIdByName } = buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + flatObjectMetadata, + ); + + const field = flatFieldMetadataMaps.byId[fieldIdByName[connectFieldName]]; if ( + !isDefined(field) || (!isFieldMetadataEntityOfType(field, FieldMetadataType.RELATION) && !isFieldMetadataEntityOfType(field, FieldMetadataType.MORPH_RELATION)) || field.settings?.relationType !== RelationType.MANY_TO_ONE ) { - const objectMetadataNameSingular = objectMetadata.nameSingular; + const objectMetadataNameSingular = flatObjectMetadata.nameSingular; throw new TwentyORMException( - `Connect is not allowed for ${connectFieldName} on ${objectMetadata.nameSingular}`, + `Connect is not allowed for ${connectFieldName} on ${flatObjectMetadata.nameSingular}`, TwentyORMExceptionCode.CONNECT_NOT_ALLOWED, { userFriendlyMessage: msg`Connect is not allowed for ${connectFieldName} on ${objectMetadataNameSingular}`, @@ -150,7 +163,7 @@ const computeRecordToConnectCondition = ( checkNoRelationFieldConflictOrThrow(entity, connectFieldName); const targetObjectMetadata = - objectMetadataMap.byId[field.relationTargetObjectMetadataId || '']; + flatObjectMetadataMaps.byId[field.relationTargetObjectMetadataId || '']; if (!isDefined(targetObjectMetadata)) { throw new TwentyORMException( @@ -164,6 +177,8 @@ const computeRecordToConnectCondition = ( const uniqueConstraintFields = checkUniqueConstraintFullyPopulated( targetObjectMetadata, + flatFieldMetadataMaps, + flatIndexMaps, connectObject, connectFieldName, ); @@ -179,16 +194,41 @@ const computeRecordToConnectCondition = ( }; const checkUniqueConstraintFullyPopulated = ( - objectMetadata: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, + flatIndexMaps: FlatEntityMaps, connectObject: ConnectObject, connectFieldName: string, ) => { + const fields = getFlatFieldsFromFlatObjectMetadata( + flatObjectMetadata, + flatFieldMetadataMaps, + ); + + const indexMetadatas = flatObjectMetadata.indexMetadataIds + .map((indexId) => flatIndexMaps.byId[indexId]) + .filter(isDefined) + .map((index) => ({ + id: index.id, + isUnique: index.isUnique, + indexFieldMetadatas: index.flatIndexFieldMetadatas.map( + (fieldMetadata) => ({ + fieldMetadataId: fieldMetadata.fieldMetadataId, + }), + ), + })); + const uniqueConstraintsFields = getUniqueConstraintsFields< - FieldMetadataEntity, - ObjectMetadataEntity + FlatFieldMetadata, + { + id: string; + indexMetadatas: typeof indexMetadatas; + fields: FlatFieldMetadata[]; + } >({ - ...objectMetadata, - fields: Object.values(objectMetadata.fieldsById), + id: flatObjectMetadata.id, + indexMetadatas, + fields, }); const hasUniqueConstraintFieldFullyPopulated = uniqueConstraintsFields.some( @@ -243,7 +283,7 @@ const checkNoRelationFieldConflictOrThrow = ( }; const computeUniqueConstraintCondition = ( - uniqueConstraintFields: FieldMetadataEntity[], + uniqueConstraintFields: FlatFieldMetadata[], connectObject: ConnectObject, ): UniqueConstraintCondition => { return uniqueConstraintFields.reduce((acc, uniqueConstraintField) => { @@ -271,7 +311,7 @@ const computeUniqueConstraintCondition = ( const checkUniqueConstraintsAreSameOrThrow = ( relationConnectQueryConfig: RelationConnectQueryConfig, - uniqueConstraintFields: FieldMetadataEntity[], + uniqueConstraintFields: FlatFieldMetadata[], ) => { if ( !deepEqual( diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/determine-schema-relation-details.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/determine-schema-relation-details.util.ts index 7025e6e45cb..8bba119c924 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/determine-schema-relation-details.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/determine-schema-relation-details.util.ts @@ -1,8 +1,9 @@ import { type FieldMetadataType } from 'twenty-shared/types'; import { type RelationType } from 'typeorm/metadata/types/RelationTypes'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { RelationException, RelationExceptionCode, @@ -16,12 +17,13 @@ interface RelationDetails { joinColumn: { name: string } | undefined; } -export async function determineSchemaRelationDetails( - fieldMetadata: FieldMetadataEntity< +export function determineSchemaRelationDetails( + fieldMetadata: FlatFieldMetadata< FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION >, - objectMetadataMaps: ObjectMetadataMaps, -): Promise { + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, +): RelationDetails { if (!fieldMetadata.settings) { throw new Error('Field metadata settings are missing'); } @@ -38,9 +40,9 @@ export async function determineSchemaRelationDetails( } const sourceObjectMetadata = - objectMetadataMaps.byId[fieldMetadata.objectMetadataId]; + flatObjectMetadataMaps.byId[fieldMetadata.objectMetadataId]; const targetObjectMetadata = - objectMetadataMaps.byId[fieldMetadata.relationTargetObjectMetadataId]; + flatObjectMetadataMaps.byId[fieldMetadata.relationTargetObjectMetadataId]; if (!sourceObjectMetadata || !targetObjectMetadata) { throw new RelationException( @@ -57,9 +59,7 @@ export async function determineSchemaRelationDetails( } const targetFieldMetadata = - targetObjectMetadata.fieldsById[ - fieldMetadata.relationTargetFieldMetadataId - ]; + flatFieldMetadataMaps.byId[fieldMetadata.relationTargetFieldMetadataId]; if (!targetFieldMetadata) { throw new Error('Target field metadata not found'); diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/format-data.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/format-data.util.ts index 3730cd42fb2..fe15bc24546 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/format-data.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/format-data.util.ts @@ -4,14 +4,17 @@ import { } from 'twenty-shared/types'; import { capitalize } from 'twenty-shared/utils'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { type CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory'; export function formatData( data: T, - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, ): T { if (!data) { return data; @@ -19,24 +22,27 @@ export function formatData( if (Array.isArray(data)) { return data.map((item) => - formatData(item, objectMetadataItemWithFieldMaps), + formatData(item, flatObjectMetadata, flatFieldMetadataMaps), ) as T; } + const { fieldIdByName, fieldIdByJoinColumnName } = + buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + flatObjectMetadata, + ); + // eslint-disable-next-line @typescript-eslint/no-explicit-any const newData: Record = {}; for (const [key, value] of Object.entries(data)) { - const fieldMetadataId = - objectMetadataItemWithFieldMaps.fieldIdByName[key] || - objectMetadataItemWithFieldMaps.fieldIdByJoinColumnName[key]; + const fieldMetadataId = fieldIdByName[key] || fieldIdByJoinColumnName[key]; - const fieldMetadata = - objectMetadataItemWithFieldMaps.fieldsById[fieldMetadataId]; + const fieldMetadata = flatFieldMetadataMaps.byId[fieldMetadataId]; if (!fieldMetadata) { throw new Error( - `Field metadata for field "${key}" is missing in object metadata ${objectMetadataItemWithFieldMaps.nameSingular}`, + `Field metadata for field "${key}" is missing in object metadata ${flatObjectMetadata.nameSingular}`, ); } @@ -58,7 +64,7 @@ export function formatData( export function formatCompositeField( // eslint-disable-next-line @typescript-eslint/no-explicit-any value: any, - fieldMetadata: FieldMetadataEntity, + fieldMetadata: FlatFieldMetadata, // eslint-disable-next-line @typescript-eslint/no-explicit-any ): Record { const compositeType = compositeTypeDefinitions.get( @@ -81,7 +87,7 @@ export function formatCompositeField( if (value && value[subFieldKey] !== undefined) { formattedCompositeField[fullFieldName] = formatFieldMetadataValue( value[subFieldKey], - property as unknown as FieldMetadataEntity, + property as unknown as FlatFieldMetadata, ); } } @@ -92,7 +98,7 @@ export function formatCompositeField( function formatFieldMetadataValue( // eslint-disable-next-line @typescript-eslint/no-explicit-any value: any, - fieldMetadata: FieldMetadataEntity, + fieldMetadata: FlatFieldMetadata, ) { if ( fieldMetadata.type === FieldMetadataType.RAW_JSON && diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts index 70949f216e4..c04f9a15316 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts @@ -12,18 +12,21 @@ import { DEFAULT_COMPOSITE_FIELDS_NULL_EQUIVALENT_VALUE, DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE, } from 'src/engine/api/common/common-args-processors/data-arg-processor/constants/null-equivalent-values.constant'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util'; import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { getCompositeFieldMetadataCollection } from 'src/engine/twenty-orm/utils/get-composite-field-metadata-collection'; import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; export function formatResult( // eslint-disable-next-line @typescript-eslint/no-explicit-any data: any, - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps | undefined, - objectMetadataMaps: ObjectMetadataMaps, + flatObjectMetadata: FlatObjectMetadata | undefined, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, ): T { if (!isDefined(data)) { return data; @@ -31,7 +34,12 @@ export function formatResult( if (Array.isArray(data)) { return data.map((item) => - formatResult(item, objectMetadataItemWithFieldMaps, objectMetadataMaps), + formatResult( + item, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + ), ) as T; } @@ -39,12 +47,18 @@ export function formatResult( return data; } - if (!objectMetadataItemWithFieldMaps) { + if (!flatObjectMetadata) { throw new Error('Object metadata is missing'); } + const { fieldIdByName } = buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + flatObjectMetadata, + ); + const compositeFieldMetadataMap = getCompositeFieldMetadataMap( - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ); const newData: object = {}; @@ -53,14 +67,12 @@ export function formatResult( const compositePropertyArgs = compositeFieldMetadataMap.get(key); const fieldMetadataId = - objectMetadataItemWithFieldMaps.fieldIdByName[key] || - objectMetadataItemWithFieldMaps.fieldIdByName[ - compositePropertyArgs?.parentField ?? '' - ]; + fieldIdByName[key] || + fieldIdByName[compositePropertyArgs?.parentField ?? '']; - const fieldMetadata = objectMetadataItemWithFieldMaps.fieldsById[ - fieldMetadataId - ] as FieldMetadataEntity | undefined; + const fieldMetadata = flatFieldMetadataMaps.byId[fieldMetadataId] as + | FlatFieldMetadata + | undefined; const isRelation = fieldMetadata ? isFieldMetadataEntityOfType(fieldMetadata, FieldMetadataType.RELATION) @@ -71,8 +83,9 @@ export function formatResult( // @ts-expect-error legacy noImplicitAny newData[key] = formatResult( value, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, ); } else if (fieldMetadata) { // @ts-expect-error legacy noImplicitAny @@ -93,7 +106,9 @@ export function formatResult( } const targetObjectMetadata = - objectMetadataMaps.byId[fieldMetadata.relationTargetObjectMetadataId]; + flatObjectMetadataMaps.byId[ + fieldMetadata.relationTargetObjectMetadataId + ]; if (!targetObjectMetadata) { throw new Error( @@ -105,7 +120,8 @@ export function formatResult( newData[key] = formatResult( value, targetObjectMetadata, - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, ); } @@ -131,8 +147,9 @@ export function formatResult( : value; } - const fieldMetadataItemsOfTypeDateOnly = Object.values( - objectMetadataItemWithFieldMaps.fieldsById, + const fieldMetadataItemsOfTypeDateOnly = getFlatFieldsFromFlatObjectMetadata( + flatObjectMetadata, + flatFieldMetadataMaps, ).filter((field) => field.type === FieldMetadataType.DATE); for (const dateField of fieldMetadataItemsOfTypeDateOnly) { @@ -151,10 +168,12 @@ export function formatResult( } export function getCompositeFieldMetadataMap( - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, ) { const compositeFieldMetadataCollection = getCompositeFieldMetadataCollection( - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ); return new Map( @@ -211,7 +230,7 @@ function formatFieldMetadataValue( function transformCompositeFieldNullValue( value: unknown, compositePropertyName: string, - fieldMetadata: FieldMetadataEntity, + fieldMetadata: FlatFieldMetadata, ) { if (!isNull(value)) return value; diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/format-twenty-orm-event-to-database-batch-event.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/format-twenty-orm-event-to-database-batch-event.util.ts index 76b75c05cf3..f9402a20657 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/format-twenty-orm-event-to-database-batch-event.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/format-twenty-orm-event-to-database-batch-event.util.ts @@ -12,7 +12,9 @@ import type { ObjectRecordDiff } from 'src/engine/core-modules/event-emitter/typ import { ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event'; import { ObjectRecordUpsertEvent } from 'src/engine/core-modules/event-emitter/types/object-record-upsert.event'; import { objectRecordChangedValues } from 'src/engine/core-modules/event-emitter/utils/object-record-changed-values'; -import type { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import type { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { TwentyORMException, TwentyORMExceptionCode, @@ -24,13 +26,15 @@ export const formatTwentyOrmEventToDatabaseBatchEvent = < >({ action, objectMetadataItem, + flatFieldMetadataMaps, workspaceId, authContext, entities, beforeEntities, }: { action: DatabaseEventAction; - objectMetadataItem: ObjectMetadataItemWithFieldMaps; + objectMetadataItem: FlatObjectMetadata; + flatFieldMetadataMaps: FlatEntityMaps; workspaceId: string; authContext?: AuthContext; entities: T | T[]; @@ -41,7 +45,6 @@ export const formatTwentyOrmEventToDatabaseBatchEvent = < } const objectMetadataNameSingular = objectMetadataItem.nameSingular; - const fields = Object.values(objectMetadataItem.fieldsById ?? {}); const entityArray = isDefined(entities) ? Array.isArray(entities) ? entities @@ -89,6 +92,7 @@ export const formatTwentyOrmEventToDatabaseBatchEvent = < before, after, objectMetadataItem, + flatFieldMetadataMaps, ) as Partial>; const updatedFields = Object.keys(diff); @@ -158,6 +162,7 @@ export const formatTwentyOrmEventToDatabaseBatchEvent = < before ?? {}, after, objectMetadataItem, + flatFieldMetadataMaps, ) as Partial>; updatedFields = Object.keys(diff); @@ -183,7 +188,7 @@ export const formatTwentyOrmEventToDatabaseBatchEvent = < objectMetadataNameSingular, action, events, - objectMetadata: { ...objectMetadataItem, fields }, + objectMetadata: objectMetadataItem, workspaceId, }; }; diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/get-column-name-to-field-metadata-id.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/get-column-name-to-field-metadata-id.util.ts index cc2c8269ce6..54f01222d8a 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/get-column-name-to-field-metadata-id.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/get-column-name-to-field-metadata-id.util.ts @@ -1,16 +1,18 @@ -import { isDefined } from 'twenty-shared/utils'; import { type CompositeType } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { type ColumnNameProcessor, processFieldMetadataForColumnNameMapping, } from 'src/engine/twenty-orm/utils/process-field-metadata-for-column-name-mapping.util'; export function getColumnNameToFieldMetadataIdMap( - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, ) { const columnNameToFieldMetadataIdMap: Record = {}; @@ -21,7 +23,7 @@ export function getColumnNameToFieldMetadataIdMap( compositeType, }: { fieldMetadataId: string; - fieldMetadata: FieldMetadataEntity; + fieldMetadata: FlatFieldMetadata; compositeType: CompositeType; }) => { compositeType.properties.forEach((compositeProperty) => { @@ -59,7 +61,8 @@ export function getColumnNameToFieldMetadataIdMap( }; processFieldMetadataForColumnNameMapping( - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, processor, ); diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/get-composite-field-metadata-collection.ts b/packages/twenty-server/src/engine/twenty-orm/utils/get-composite-field-metadata-collection.ts index 4822de779f8..6092699e344 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/get-composite-field-metadata-collection.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/get-composite-field-metadata-collection.ts @@ -1,11 +1,16 @@ +import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; export function getCompositeFieldMetadataCollection( - ObjectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, ) { - const compositeFieldMetadataCollection = Object.values( - ObjectMetadataItemWithFieldMaps.fieldsById, + const compositeFieldMetadataCollection = getFlatFieldsFromFlatObjectMetadata( + flatObjectMetadata, + flatFieldMetadataMaps, ).filter((fieldMetadataItem) => isCompositeFieldMetadataType(fieldMetadataItem.type), ); diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/get-field-metadata-id-to-column-names-map.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/get-field-metadata-id-to-column-names-map.util.ts index 2b5b9893a09..1adf6bf2af3 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/get-field-metadata-id-to-column-names-map.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/get-field-metadata-id-to-column-names-map.util.ts @@ -1,15 +1,17 @@ import { type CompositeType } from 'twenty-shared/types'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { type ColumnNameProcessor, processFieldMetadataForColumnNameMapping, } from 'src/engine/twenty-orm/utils/process-field-metadata-for-column-name-mapping.util'; export function getFieldMetadataIdToColumnNamesMap( - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, ) { const fieldMetadataToColumnNamesMap = new Map(); @@ -20,7 +22,7 @@ export function getFieldMetadataIdToColumnNamesMap( compositeType, }: { fieldMetadataId: string; - fieldMetadata: FieldMetadataEntity; + fieldMetadata: FlatFieldMetadata; compositeType: CompositeType; }) => { compositeType.properties.forEach((compositeProperty) => { @@ -52,7 +54,7 @@ export function getFieldMetadataIdToColumnNamesMap( columnName, }: { fieldMetadataId: string; - fieldMetadata: FieldMetadataEntity; + fieldMetadata: FlatFieldMetadata; columnName: string; }) => { fieldMetadataToColumnNamesMap.set(fieldMetadataId, [columnName]); @@ -60,7 +62,8 @@ export function getFieldMetadataIdToColumnNamesMap( }; processFieldMetadataForColumnNameMapping( - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, processor, ); diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util.ts index b58c1a4231a..bd08b43526d 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util.ts @@ -6,8 +6,7 @@ import { import { type WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { TwentyORMException, TwentyORMExceptionCode, @@ -17,7 +16,7 @@ import { WorkspaceEntitiesStorage } from 'src/engine/twenty-orm/storage/workspac export const getObjectMetadataFromEntityTarget = ( entityTarget: EntityTarget, internalContext: WorkspaceInternalContext, -): ObjectMetadataItemWithFieldMaps => { +): FlatObjectMetadata => { const objectMetadataName = typeof entityTarget === 'string' ? entityTarget @@ -33,22 +32,29 @@ export const getObjectMetadataFromEntityTarget = ( ); } - const objectMetadata = getObjectMetadataMapItemByNameSingular( - internalContext.objectMetadataMaps, - objectMetadataName, - ); + const objectMetadataId = + internalContext.objectIdByNameSingular[objectMetadataName]; - if (!objectMetadata) { + if (!objectMetadataId) { throw new TwentyORMException( `Object metadata for object "${objectMetadataName}" is missing ` + `in workspace "${internalContext.workspaceId}" ` + `with object metadata collection length: ${ - Object.keys(internalContext.objectMetadataMaps.idByNameSingular) - .length + Object.keys(internalContext.objectIdByNameSingular).length }`, TwentyORMExceptionCode.MALFORMED_METADATA, ); } + const objectMetadata = + internalContext.flatObjectMetadataMaps.byId[objectMetadataId]; + + if (!objectMetadata) { + throw new TwentyORMException( + `Object metadata for object "${objectMetadataName}" (id: ${objectMetadataId}) is missing`, + TwentyORMExceptionCode.MALFORMED_METADATA, + ); + } + return objectMetadata; }; diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/process-field-metadata-for-column-name-mapping.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/process-field-metadata-for-column-name-mapping.util.ts index dc0c103d1f5..bb815ad5972 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/process-field-metadata-for-column-name-mapping.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/process-field-metadata-for-column-name-mapping.util.ts @@ -1,5 +1,4 @@ import { - type FieldMetadataRelationSettings, type CompositeType, compositeTypeDefinitions, } from 'twenty-shared/types'; @@ -7,15 +6,17 @@ import { import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { extractGraphQLRelationFieldNames } from 'src/engine/api/graphql/workspace-schema-builder/utils/extract-graphql-relation-field-names.util'; -import { isFieldMetadataRelationOrMorphRelation } from 'src/engine/api/graphql/workspace-schema-builder/utils/is-field-metadata-relation-or-morph-relation.utils'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { PermissionsException, PermissionsExceptionCode, } from 'src/engine/metadata-modules/permissions/permissions.exception'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; export type ColumnNameProcessor = { processCompositeField: ({ @@ -24,7 +25,7 @@ export type ColumnNameProcessor = { compositeType, }: { fieldMetadataId: string; - fieldMetadata: FieldMetadataEntity; + fieldMetadata: FlatFieldMetadata; compositeType: CompositeType; }) => void; processRelationField: ({ @@ -34,7 +35,7 @@ export type ColumnNameProcessor = { connectFieldName, }: { fieldMetadataId: string; - fieldMetadata: FieldMetadataEntity; + fieldMetadata: FlatFieldMetadata; joinColumnName: string; connectFieldName?: string; }) => void; @@ -44,18 +45,22 @@ export type ColumnNameProcessor = { columnName, }: { fieldMetadataId: string; - fieldMetadata: FieldMetadataEntity; + fieldMetadata: FlatFieldMetadata; columnName: string; }) => void; }; export function processFieldMetadataForColumnNameMapping( - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, processor: ColumnNameProcessor, ) { - for (const [fieldMetadataId, fieldMetadata] of Object.entries( - objectMetadataItemWithFieldMaps.fieldsById, - )) { + for (const fieldMetadataId of flatObjectMetadata.fieldMetadataIds) { + const fieldMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityMaps: flatFieldMetadataMaps, + flatEntityId: fieldMetadataId, + }); + if (isCompositeFieldMetadataType(fieldMetadata.type)) { const compositeType = compositeTypeDefinitions.get(fieldMetadata.type); @@ -72,9 +77,8 @@ export function processFieldMetadataForColumnNameMapping( compositeType, }); } else { - if (isFieldMetadataRelationOrMorphRelation(fieldMetadata)) { - const fieldMetadataSettings = - fieldMetadata.settings as FieldMetadataRelationSettings; + if (isMorphOrRelationFlatFieldMetadata(fieldMetadata)) { + const fieldMetadataSettings = fieldMetadata.settings; if (fieldMetadataSettings?.relationType === RelationType.ONE_TO_MANY) { continue; diff --git a/packages/twenty-server/src/engine/workspace-cache/types/workspace-context-data.type.ts b/packages/twenty-server/src/engine/workspace-cache/types/workspace-context-data.type.ts deleted file mode 100644 index 9d552bb8fbf..00000000000 --- a/packages/twenty-server/src/engine/workspace-cache/types/workspace-context-data.type.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { type ObjectsPermissionsByRoleId } from 'twenty-shared/types'; - -import { type FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; - -export type WorkspaceContextData = { - objectMetadataMaps: ObjectMetadataMaps; - metadataVersion: number; - featureFlagsMap: Record; - permissionsPerRoleId: ObjectsPermissionsByRoleId; -}; diff --git a/packages/twenty-server/src/engine/workspace-event-emitter/types/workspace-event-batch.type.ts b/packages/twenty-server/src/engine/workspace-event-emitter/types/workspace-event-batch.type.ts index 9d313629cf3..5ab971cfcbb 100644 --- a/packages/twenty-server/src/engine/workspace-event-emitter/types/workspace-event-batch.type.ts +++ b/packages/twenty-server/src/engine/workspace-event-emitter/types/workspace-event-batch.type.ts @@ -1,8 +1,8 @@ -import type { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import type { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; export type WorkspaceEventBatch = { name: string; workspaceId: string; - objectMetadata: Omit; + objectMetadata: FlatObjectMetadata; events: WorkspaceEvent[]; }; diff --git a/packages/twenty-server/src/engine/workspace-event-emitter/workspace-event-emitter.ts b/packages/twenty-server/src/engine/workspace-event-emitter/workspace-event-emitter.ts index a6fca9ba13d..6beda6e6d62 100644 --- a/packages/twenty-server/src/engine/workspace-event-emitter/workspace-event-emitter.ts +++ b/packages/twenty-server/src/engine/workspace-event-emitter/workspace-event-emitter.ts @@ -10,7 +10,7 @@ import { ObjectRecordDestroyEvent } from 'src/engine/core-modules/event-emitter/ import { type ObjectRecordRestoreEvent } from 'src/engine/core-modules/event-emitter/types/object-record-restore.event'; import { ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event'; import { ObjectRecordUpsertEvent } from 'src/engine/core-modules/event-emitter/types/object-record-upsert.event'; -import type { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import type { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { type CustomEventName } from 'src/engine/workspace-event-emitter/types/custom-event-name.type'; import { CustomWorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/custom-workspace-batch-event.type'; import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type'; @@ -29,7 +29,7 @@ export type DatabaseBatchEventInput> = { objectMetadataNameSingular: string; action: A; events: ActionEventMap[A][]; - objectMetadata: Omit; + objectMetadata: FlatObjectMetadata; workspaceId: string; }; diff --git a/packages/twenty-server/src/engine/workspace-manager/__tests__/workspace-manager.service.spec.ts b/packages/twenty-server/src/engine/workspace-manager/__tests__/workspace-manager.service.spec.ts index a4d03ea1446..406ed5fa505 100644 --- a/packages/twenty-server/src/engine/workspace-manager/__tests__/workspace-manager.service.spec.ts +++ b/packages/twenty-server/src/engine/workspace-manager/__tests__/workspace-manager.service.spec.ts @@ -11,6 +11,7 @@ import { AgentService } from 'src/engine/metadata-modules/ai/ai-agent/agent.serv import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service'; @@ -145,6 +146,12 @@ describe('WorkspaceManagerService', () => { }), }, }, + { + provide: WorkspaceManyOrAllFlatEntityMapsCacheService, + useValue: { + invalidateEntireCache: jest.fn(), + }, + }, ], }).compile(); diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/services/dev-seeder-data.service.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/services/dev-seeder-data.service.ts index bdceb99b252..edd78a63c1e 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/services/dev-seeder-data.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/services/dev-seeder-data.service.ts @@ -8,8 +8,9 @@ import { DataSource } from 'typeorm'; import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; +import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; +import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; import { computeTableName } from 'src/engine/utils/compute-table-name.util'; import { @@ -268,6 +269,7 @@ export class DevSeederDataService { private readonly objectMetadataService: ObjectMetadataServiceV2, private readonly timelineActivitySeederService: TimelineActivitySeederService, private readonly fileStorageService: FileStorageService, + private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, ) {} public async seed({ @@ -282,6 +284,14 @@ export class DevSeederDataService { const objectMetadataItems = await this.objectMetadataService.findManyWithinWorkspace(workspaceId); + const { flatObjectMetadataMaps, flatFieldMetadataMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatObjectMetadataMaps', 'flatFieldMetadataMaps'], + }, + ); + await this.coreDataSource.transaction( async (entityManager: WorkspaceEntityManager) => { await this.seedRecordsInBatches({ @@ -300,7 +310,12 @@ export class DevSeederDataService { await this.seedAttachmentFiles(workspaceId); - await prefillWorkflows(entityManager, schemaName, objectMetadataItems); + await prefillWorkflows( + entityManager, + schemaName, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + ); }, ); } @@ -316,7 +331,7 @@ export class DevSeederDataService { schemaName: string; workspaceId: string; featureFlags?: Record; - objectMetadataItems: ObjectMetadataEntity[]; + objectMetadataItems: FlatObjectMetadata[]; }) { const batches = getRecordSeedsBatches(workspaceId, featureFlags); diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/metadata/services/dev-seeder-metadata.service.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/metadata/services/dev-seeder-metadata.service.ts index 383e0b9f103..ef79e12307d 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/metadata/services/dev-seeder-metadata.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/metadata/services/dev-seeder-metadata.service.ts @@ -193,13 +193,19 @@ export class DevSeederMetadataService { featureFlags?: Record; twentyStandardFlatApplication: FlatApplication; }): Promise { - const createdObjectMetadata = - await this.objectMetadataServiceV2.findManyWithinWorkspace(workspaceId); + const { flatObjectMetadataMaps, flatFieldMetadataMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatObjectMetadataMaps', 'flatFieldMetadataMaps'], + }, + ); await prefillCoreViews({ coreDataSource: this.coreDataSource, workspaceId, - objectMetadataItems: createdObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, workspaceSchemaName: dataSourceMetadata.schema, featureFlags, twentyStandardFlatApplication, diff --git a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/prefill-core-views.ts b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/prefill-core-views.ts index 8abac5fb77f..dbc93d65827 100644 --- a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/prefill-core-views.ts +++ b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/prefill-core-views.ts @@ -1,7 +1,11 @@ import { isString } from '@sniptt/guards'; import { type DataSource, type QueryRunner } from 'typeorm'; +import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util'; import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity'; import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity'; @@ -37,19 +41,46 @@ import { workspaceMembersAllView } from 'src/engine/workspace-manager/standard-o type PrefillCoreViewsArgs = { coreDataSource: DataSource; workspaceId: string; - objectMetadataItems: ObjectMetadataEntity[]; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; featureFlags?: Record; workspaceSchemaName: string; twentyStandardFlatApplication: FlatApplication; }; +// This is a temporary function to build the object metadata items from the flat maps. +// We should use the maps in the seeders instead. +const buildObjectMetadataItemsFromFlatMaps = ( + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, +): ObjectMetadataEntity[] => { + return Object.values(flatObjectMetadataMaps.byId) + .filter((flatObjectMetadata) => flatObjectMetadata !== undefined) + .map((flatObjectMetadata) => { + const fields = getFlatFieldsFromFlatObjectMetadata( + flatObjectMetadata, + flatFieldMetadataMaps, + ); + + return { + ...flatObjectMetadata, + fields, + } as unknown as ObjectMetadataEntity; + }); +}; + export const prefillCoreViews = async ({ coreDataSource, workspaceId, - objectMetadataItems, + flatObjectMetadataMaps, + flatFieldMetadataMaps, workspaceSchemaName, twentyStandardFlatApplication, }: PrefillCoreViewsArgs): Promise => { + const objectMetadataItems = buildObjectMetadataItemsFromFlatMaps( + flatObjectMetadataMaps, + flatFieldMetadataMaps, + ); const views = [ companiesAllView, peopleAllView, diff --git a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflows.ts b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflows.ts index 10f76c69e6a..a00ba6455b1 100644 --- a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflows.ts +++ b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflows.ts @@ -1,9 +1,11 @@ +import { FieldActorSource } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { type EntityManager } from 'typeorm'; -import { FieldActorSource } from 'twenty-shared/types'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { generateObjectMetadataMaps } from 'src/engine/metadata-modules/utils/generate-object-metadata-maps.util'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; +import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util'; import { generateObjectRecordFields } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields'; const QUICK_LEAD_WORKFLOW_ID = '8b213cac-a68b-4ffe-817a-3ec994e9932d'; @@ -12,13 +14,15 @@ const QUICK_LEAD_WORKFLOW_VERSION_ID = 'ac67974f-c524-4288-9d88-af8515400b68'; export const prefillWorkflows = async ( entityManager: EntityManager, schemaName: string, - objectMetadataItems: ObjectMetadataEntity[], + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, ) => { - const objectMetadataMaps = generateObjectMetadataMaps(objectMetadataItems); - const companyObjectMetadataId = - objectMetadataMaps.idByNameSingular['company']; + const { idByNameSingular: objectIdByNameSingular } = buildObjectIdByNameMaps( + flatObjectMetadataMaps, + ); - const personObjectMetadataId = objectMetadataMaps.idByNameSingular['person']; + const companyObjectMetadataId = objectIdByNameSingular['company']; + const personObjectMetadataId = objectIdByNameSingular['person']; if ( !isDefined(companyObjectMetadataId) || @@ -28,9 +32,10 @@ export const prefillWorkflows = async ( } const companyObjectMetadata = - objectMetadataMaps.byId[companyObjectMetadataId]; + flatObjectMetadataMaps.byId[companyObjectMetadataId]; - const personObjectMetadata = objectMetadataMaps.byId[personObjectMetadataId]; + const personObjectMetadata = + flatObjectMetadataMaps.byId[personObjectMetadataId]; if (!isDefined(companyObjectMetadata) || !isDefined(personObjectMetadata)) { throw new Error('Company or person object metadata not found'); @@ -220,8 +225,9 @@ export const prefillWorkflows = async ( _outputSchemaType: 'RECORD', fields: generateObjectRecordFields({ objectMetadataInfo: { - objectMetadataItemWithFieldsMaps: companyObjectMetadata, - objectMetadataMaps, + flatObjectMetadata: companyObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }, depth: 0, }), @@ -262,8 +268,9 @@ export const prefillWorkflows = async ( outputSchema: { fields: generateObjectRecordFields({ objectMetadataInfo: { - objectMetadataItemWithFieldsMaps: personObjectMetadata, - objectMetadataMaps, + flatObjectMetadata: personObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }, depth: 0, }), diff --git a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/standard-objects-prefill-data.ts b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/standard-objects-prefill-data.ts index 038bfc18084..8a6af0955d5 100644 --- a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/standard-objects-prefill-data.ts +++ b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/standard-objects-prefill-data.ts @@ -1,6 +1,8 @@ import { type DataSource, type EntityManager } from 'typeorm'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { prefillCompanies } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-companies'; import { prefillPeople } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-people'; import { prefillWorkflows } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflows'; @@ -8,13 +10,19 @@ import { prefillWorkflows } from 'src/engine/workspace-manager/standard-objects- export const standardObjectsPrefillData = async ( dataSource: DataSource, schemaName: string, - objectMetadataItems: ObjectMetadataEntity[], + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, ) => { dataSource.transaction(async (entityManager: EntityManager) => { await prefillCompanies(entityManager, schemaName); await prefillPeople(entityManager, schemaName); - await prefillWorkflows(entityManager, schemaName, objectMetadataItems); + await prefillWorkflows( + entityManager, + schemaName, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + ); }); }; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/commands/workspace-health-command.module.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/commands/workspace-health-command.module.ts deleted file mode 100644 index 06bb4cb45b5..00000000000 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/commands/workspace-health-command.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Module } from '@nestjs/common'; - -import { WorkspaceHealthCommand } from 'src/engine/workspace-manager/workspace-health/commands/workspace-health.command'; -import { WorkspaceHealthModule } from 'src/engine/workspace-manager/workspace-health/workspace-health.module'; - -@Module({ - imports: [WorkspaceHealthModule], - providers: [WorkspaceHealthCommand], -}) -export class WorkspaceHealthCommandModule {} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/commands/workspace-health.command.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/commands/workspace-health.command.ts deleted file mode 100644 index c67a2632272..00000000000 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/commands/workspace-health.command.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { Logger } from '@nestjs/common'; - -import chalk from 'chalk'; -import { Command, CommandRunner, Option } from 'nest-commander'; - -import { WorkspaceHealthFixKind } from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-health-fix-kind.interface'; -import { WorkspaceHealthMode } from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-health-options.interface'; - -import { CommandLogger } from 'src/command/command-logger'; -import { WorkspaceHealthService } from 'src/engine/workspace-manager/workspace-health/workspace-health.service'; - -interface WorkspaceHealthCommandOptions { - workspaceId: string; - mode?: WorkspaceHealthMode; - fix?: WorkspaceHealthFixKind; - dryRun?: boolean; -} - -@Command({ - name: 'workspace:health', - description: 'Check health of the given workspace.', -}) -export class WorkspaceHealthCommand extends CommandRunner { - private readonly logger = new Logger(WorkspaceHealthCommand.name); - private readonly commandLogger = new CommandLogger( - WorkspaceHealthCommand.name, - ); - - constructor(private readonly workspaceHealthService: WorkspaceHealthService) { - super(); - } - - async run( - _passedParam: string[], - options: WorkspaceHealthCommandOptions, - ): Promise { - const issues = await this.workspaceHealthService.healthCheck( - options.workspaceId, - { - mode: options.mode ?? WorkspaceHealthMode.All, - }, - ); - - if (issues.length === 0) { - this.logger.log(chalk.green('Workspace is healthy')); - } else { - this.logger.log( - chalk.red(`Workspace is not healthy, found ${issues.length} issues`), - ); - - for (let issueIndex = 0; issueIndex < issues.length; issueIndex++) { - this.logger.log( - chalk.red(`Issue #${issueIndex + 1} : ${issues[issueIndex].message}`), - ); - } - - const logFilePath = await this.commandLogger.writeLog( - `workspace-health-issues-${options.workspaceId}`, - issues, - ); - - this.logger.log( - chalk.yellow(`Issues written to log at : ${logFilePath}`), - ); - } - - if (options.fix) { - this.logger.log(chalk.yellow('Fixing issues')); - - const { workspaceMigrations, metadataEntities } = - await this.workspaceHealthService.fixIssues( - options.workspaceId, - issues, - { - type: options.fix, - applyChanges: !options.dryRun, - }, - ); - const totalCount = workspaceMigrations.length + metadataEntities.length; - - if (options.dryRun) { - await this.commandLogger.writeLog( - `workspace-health-${options.fix}-migrations`, - workspaceMigrations, - ); - - await this.commandLogger.writeLog( - `workspace-health-${options.fix}-metadata-entities`, - metadataEntities, - ); - } else { - this.logger.log( - chalk.green(`Fixed ${totalCount}/${issues.length} issues`), - ); - } - } - } - - @Option({ - flags: '-w, --workspace-id [workspace_id]', - description: 'workspace id', - required: true, - }) - parseWorkspaceId(value: string): string { - return value; - } - - @Option({ - flags: '-f, --fix [kind]', - description: 'fix issues', - required: false, - }) - fix(value: string): WorkspaceHealthFixKind { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - if (!Object.values(WorkspaceHealthFixKind).includes(value as any)) { - throw new Error(`Invalid fix kind ${value}`); - } - - return value as WorkspaceHealthFixKind; - } - - @Option({ - flags: '-m, --mode [mode]', - description: 'Mode of the health check [structure, metadata, all]', - required: false, - defaultValue: WorkspaceHealthMode.All, - }) - parseMode(value: string): WorkspaceHealthMode { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - if (!Object.values(WorkspaceHealthMode).includes(value as any)) { - throw new Error(`Invalid mode ${value}`); - } - - return value as WorkspaceHealthMode; - } - - @Option({ - flags: '-d, --dry-run', - description: 'Dry run without applying changes', - required: false, - }) - dryRun(): boolean { - return true; - } -} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/abstract-workspace.fixer.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/abstract-workspace.fixer.ts deleted file mode 100644 index 65a42045248..00000000000 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/abstract-workspace.fixer.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { type EntityManager } from 'typeorm'; - -import { - type WorkspaceHealthIssue, - type WorkspaceHealthIssueType, - type WorkspaceIssueTypeToInterface, -} from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-health-issue.interface'; - -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { type WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; - -export class CompareEntity { - current: T | null; - altered: T | null; -} - -export abstract class AbstractWorkspaceFixer< - IssueTypes extends WorkspaceHealthIssueType, - UpdateRecordEntities = unknown, -> { - private issueTypes: IssueTypes[]; - - protected constructor(...issueTypes: IssueTypes[]) { - this.issueTypes = issueTypes; - } - - filterIssues( - issues: WorkspaceHealthIssue[], - ): WorkspaceIssueTypeToInterface[] { - return issues.filter( - (issue): issue is WorkspaceIssueTypeToInterface => - this.issueTypes.includes(issue.type as IssueTypes), - ); - } - - protected splitIssuesByType( - issues: WorkspaceIssueTypeToInterface[], - ): Record[]> { - return issues.reduce( - ( - acc: Record[]>, - issue, - ) => { - const type = issue.type as IssueTypes; - - if (!acc[type]) { - acc[type] = []; - } - acc[type].push(issue); - - return acc; - }, - {} as Record[]>, - ); - } - - async createWorkspaceMigrations?( - manager: EntityManager, - objectMetadataCollection: ObjectMetadataEntity[], - issues: WorkspaceIssueTypeToInterface[], - ): Promise[]>; - - async createMetadataUpdates?( - manager: EntityManager, - objectMetadataCollection: ObjectMetadataEntity[], - issues: WorkspaceIssueTypeToInterface[], - ): Promise[]>; -} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/index.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/index.ts deleted file mode 100644 index ad32c60fc3d..00000000000 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { WorkspaceMissingColumnFixer } from 'src/engine/workspace-manager/workspace-health/fixer/workspace-missing-column.fixer'; - -import { WorkspaceNullableFixer } from './workspace-nullable.fixer'; -import { WorkspaceDefaultValueFixer } from './workspace-default-value.fixer'; -import { WorkspaceTypeFixer } from './workspace-type.fixer'; - -export const workspaceFixers = [ - WorkspaceNullableFixer, - WorkspaceDefaultValueFixer, - WorkspaceTypeFixer, - WorkspaceMissingColumnFixer, -]; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/workspace-default-value.fixer.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/workspace-default-value.fixer.ts deleted file mode 100644 index 3e3514a2dbe..00000000000 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/workspace-default-value.fixer.ts +++ /dev/null @@ -1,229 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { type EntityManager } from 'typeorm'; - -import { type FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface'; -import { - type WorkspaceHealthColumnIssue, - WorkspaceHealthIssueType, -} from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-health-issue.interface'; -import { WorkspaceMigrationBuilderAction } from 'src/engine/workspace-manager/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface'; - -import { - type FieldMetadataDefaultValueFunctionNames, - fieldMetadataDefaultValueFunctionName, -} from 'src/engine/metadata-modules/field-metadata/dtos/default-value.input'; -import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { type WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; -import { WorkspaceMigrationFieldFactory } from 'src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field.factory'; - -import { - AbstractWorkspaceFixer, - type CompareEntity, -} from './abstract-workspace.fixer'; - -type WorkspaceDefaultValueFixerType = - | WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT - | WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_NOT_VALID; - -@Injectable() -export class WorkspaceDefaultValueFixer extends AbstractWorkspaceFixer { - constructor( - private readonly workspaceMigrationFieldFactory: WorkspaceMigrationFieldFactory, - ) { - super( - WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT, - WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_NOT_VALID, - ); - } - - async createWorkspaceMigrations( - _manager: EntityManager, - _objectMetadataCollection: ObjectMetadataEntity[], - issues: WorkspaceHealthColumnIssue[], - ): Promise[]> { - if (issues.length <= 0) { - return []; - } - const splittedIssues = this.splitIssuesByType(issues); - const issueNeedingMigration = - splittedIssues[WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT] ?? - []; - - return this.fixColumnDefaultValueConflictIssues( - _objectMetadataCollection, - issueNeedingMigration as WorkspaceHealthColumnIssue[], - ); - } - - async createMetadataUpdates( - _manager: EntityManager, - _objectMetadataCollection: ObjectMetadataEntity[], - issues: WorkspaceHealthColumnIssue[], - ): Promise[]> { - if (issues.length <= 0) { - return []; - } - - const splittedIssues = this.splitIssuesByType(issues); - const issueNeedingMetadataUpdate = - splittedIssues[WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_NOT_VALID] ?? - []; - - return this.fixColumnDefaultValueNotValidIssues( - _manager, - issueNeedingMetadataUpdate as WorkspaceHealthColumnIssue[], - ); - } - - private async fixColumnDefaultValueConflictIssues( - objectMetadataCollection: ObjectMetadataEntity[], - issues: WorkspaceHealthColumnIssue[], - ): Promise[]> { - const fieldMetadataUpdateCollection = issues.map((issue) => { - const oldDefaultValue = - this.computeFieldMetadataDefaultValueFromColumnDefault( - issue.columnStructure?.columnDefault, - ); - - return { - current: { - ...issue.fieldMetadata, - defaultValue: oldDefaultValue, - }, - altered: issue.fieldMetadata, - }; - }); - - return this.workspaceMigrationFieldFactory.create( - objectMetadataCollection, - fieldMetadataUpdateCollection, - WorkspaceMigrationBuilderAction.UPDATE, - ); - } - - private async fixColumnDefaultValueNotValidIssues( - manager: EntityManager, - issues: WorkspaceHealthColumnIssue[], - ): Promise[]> { - const fieldMetadataRepository = manager.getRepository(FieldMetadataEntity); - const updatedEntities: CompareEntity[] = []; - - for (const issue of issues) { - const currentDefaultValue: - | FieldMetadataDefaultValue - // Old format for default values - // TODO: Remove this after all workspaces are migrated - | { type: FieldMetadataDefaultValueFunctionNames } - | null = issue.fieldMetadata.defaultValue; - let alteredDefaultValue: FieldMetadataDefaultValue | null = null; - - // Check if it's an old function default value - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - if (currentDefaultValue && 'type' in currentDefaultValue) { - alteredDefaultValue = - currentDefaultValue.type as FieldMetadataDefaultValueFunctionNames; - } - - // Check if it's an old string default value - if (currentDefaultValue) { - for (const key of Object.keys(currentDefaultValue)) { - if (key === 'type') { - continue; - } - - // @ts-expect-error legacy noImplicitAny - const value = currentDefaultValue[key]; - - const newValue = - typeof value === 'string' && - !value.startsWith("'") && - !Object.values(fieldMetadataDefaultValueFunctionName).includes( - value as FieldMetadataDefaultValueFunctionNames, - ) - ? `'${value}'` - : value; - - alteredDefaultValue = { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ...(currentDefaultValue as any), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ...(alteredDefaultValue as any), - [key]: newValue, - }; - } - } - - // Old formart default values - if ( - alteredDefaultValue && - typeof alteredDefaultValue === 'object' && - 'value' in alteredDefaultValue - ) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - alteredDefaultValue = alteredDefaultValue.value; - } - - if (alteredDefaultValue === null) { - continue; - } - - await fieldMetadataRepository.update(issue.fieldMetadata.id, { - defaultValue: alteredDefaultValue, - }); - const alteredEntity = await fieldMetadataRepository.findOne({ - where: { - id: issue.fieldMetadata.id, - }, - }); - - updatedEntities.push({ - current: issue.fieldMetadata, - altered: alteredEntity as FieldMetadataEntity | null, - }); - } - - return updatedEntities; - } - - private computeFieldMetadataDefaultValueFromColumnDefault( - columnDefault: string | undefined, - ): FieldMetadataDefaultValue { - if ( - columnDefault === undefined || - columnDefault === null || - columnDefault === 'NULL' - ) { - return null; - } - - if (!isNaN(Number(columnDefault))) { - return +columnDefault; - } - - if (columnDefault === 'true') { - return true; - } - - if (columnDefault === 'false') { - return false; - } - - if (columnDefault === '') { - return "''"; - } - - if (columnDefault === 'now()') { - return 'now'; - } - - if (columnDefault.startsWith('public.uuid_generate_v4')) { - return 'uuid'; - } - - return columnDefault; - } -} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/workspace-missing-column.fixer.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/workspace-missing-column.fixer.ts deleted file mode 100644 index 15d8783ad7b..00000000000 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/workspace-missing-column.fixer.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { type EntityManager } from 'typeorm'; - -import { - type WorkspaceHealthColumnIssue, - WorkspaceHealthIssueType, -} from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-health-issue.interface'; -import { WorkspaceMigrationBuilderAction } from 'src/engine/workspace-manager/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface'; - -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { type WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; -import { - type FieldMetadataUpdate, - WorkspaceMigrationFieldFactory, -} from 'src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field.factory'; - -import { AbstractWorkspaceFixer } from './abstract-workspace.fixer'; - -@Injectable() -export class WorkspaceMissingColumnFixer extends AbstractWorkspaceFixer { - constructor( - private readonly workspaceMigrationFieldFactory: WorkspaceMigrationFieldFactory, - ) { - super(WorkspaceHealthIssueType.MISSING_COLUMN); - } - - async createWorkspaceMigrations( - _manager: EntityManager, - objectMetadataCollection: ObjectMetadataEntity[], - issues: WorkspaceHealthColumnIssue[], - ): Promise[]> { - if (issues.length <= 0) { - return []; - } - - return this.fixMissingColumnIssues(objectMetadataCollection, issues); - } - - private async fixMissingColumnIssues( - objectMetadataCollection: ObjectMetadataEntity[], - issues: WorkspaceHealthColumnIssue[], - ): Promise[]> { - const fieldMetadataUpdateCollection: FieldMetadataUpdate[] = []; - - for (const issue of issues) { - if (!issue.columnStructures) { - continue; - } - - /** - * Check if the column is prefixed with an underscore as it was the old convention - */ - const oldColumnName = `_${issue.fieldMetadata.name}`; - const oldColumnStructure = issue.columnStructures.find( - (columnStructure) => columnStructure.columnName === oldColumnName, - ); - - if (!oldColumnStructure) { - continue; - } - - fieldMetadataUpdateCollection.push({ - current: { - ...issue.fieldMetadata, - name: oldColumnName, - }, - altered: issue.fieldMetadata, - }); - } - - if (fieldMetadataUpdateCollection.length <= 0) { - return []; - } - - return this.workspaceMigrationFieldFactory.create( - objectMetadataCollection, - fieldMetadataUpdateCollection, - WorkspaceMigrationBuilderAction.UPDATE, - ); - } -} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/workspace-nullable.fixer.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/workspace-nullable.fixer.ts deleted file mode 100644 index e5bbc514f19..00000000000 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/workspace-nullable.fixer.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { type EntityManager } from 'typeorm'; - -import { - type WorkspaceHealthColumnIssue, - WorkspaceHealthIssueType, -} from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-health-issue.interface'; -import { WorkspaceMigrationBuilderAction } from 'src/engine/workspace-manager/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface'; - -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { type WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; -import { WorkspaceMigrationFieldFactory } from 'src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field.factory'; - -import { AbstractWorkspaceFixer } from './abstract-workspace.fixer'; - -@Injectable() -export class WorkspaceNullableFixer extends AbstractWorkspaceFixer { - constructor( - private readonly workspaceMigrationFieldFactory: WorkspaceMigrationFieldFactory, - ) { - super(WorkspaceHealthIssueType.COLUMN_NULLABILITY_CONFLICT); - } - - async createWorkspaceMigrations( - _manager: EntityManager, - objectMetadataCollection: ObjectMetadataEntity[], - issues: WorkspaceHealthColumnIssue[], - ): Promise[]> { - if (issues.length <= 0) { - return []; - } - - return this.fixColumnNullabilityIssues(objectMetadataCollection, issues); - } - - private async fixColumnNullabilityIssues( - objectMetadataCollection: ObjectMetadataEntity[], - issues: WorkspaceHealthColumnIssue[], - ): Promise[]> { - const fieldMetadataUpdateCollection = issues.map((issue) => { - return { - current: { - ...issue.fieldMetadata, - isNullable: issue.columnStructure?.isNullable ?? false, - }, - altered: issue.fieldMetadata, - }; - }); - - return this.workspaceMigrationFieldFactory.create( - objectMetadataCollection, - fieldMetadataUpdateCollection, - WorkspaceMigrationBuilderAction.UPDATE, - ); - } -} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/workspace-type.fixer.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/workspace-type.fixer.ts deleted file mode 100644 index 927747a6abf..00000000000 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/workspace-type.fixer.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; - -import { type EntityManager } from 'typeorm'; - -import { - type WorkspaceHealthColumnIssue, - WorkspaceHealthIssueType, -} from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-health-issue.interface'; -import { WorkspaceMigrationBuilderAction } from 'src/engine/workspace-manager/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface'; - -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { type WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; -import { DatabaseStructureService } from 'src/engine/workspace-manager/workspace-health/services/database-structure.service'; -import { - type FieldMetadataUpdate, - WorkspaceMigrationFieldFactory, -} from 'src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field.factory'; - -import { AbstractWorkspaceFixer } from './abstract-workspace.fixer'; - -const oldDataTypes = ['integer']; - -@Injectable() -export class WorkspaceTypeFixer extends AbstractWorkspaceFixer { - private readonly logger = new Logger(WorkspaceTypeFixer.name); - - constructor( - private readonly workspaceMigrationFieldFactory: WorkspaceMigrationFieldFactory, - private readonly databaseStructureService: DatabaseStructureService, - ) { - super(WorkspaceHealthIssueType.COLUMN_DATA_TYPE_CONFLICT); - } - - async createWorkspaceMigrations( - _manager: EntityManager, - objectMetadataCollection: ObjectMetadataEntity[], - issues: WorkspaceHealthColumnIssue[], - ): Promise[]> { - if (issues.length <= 0) { - return []; - } - - return this.fixColumnTypeIssues(objectMetadataCollection, issues); - } - - private async fixColumnTypeIssues( - objectMetadataCollection: ObjectMetadataEntity[], - issues: WorkspaceHealthColumnIssue[], - ): Promise[]> { - const fieldMetadataUpdateCollection: FieldMetadataUpdate[] = []; - - for (const issue of issues) { - const dataType = issue.columnStructure?.dataType; - - if (!dataType) { - throw new Error('Column structure data type is missing'); - } - - const type = - this.databaseStructureService.getFieldMetadataTypeFromPostgresDataType( - dataType, - ); - - if (oldDataTypes.includes(dataType)) { - this.logger.warn( - `Old data type detected for column ${issue.columnStructure?.columnName} with data type ${dataType}. Please update the column data type manually.`, - ); - continue; - } - - if (!type) { - throw new Error("Can't find field metadata type from column structure"); - } - - fieldMetadataUpdateCollection.push({ - current: { - ...issue.fieldMetadata, - type, - }, - altered: issue.fieldMetadata, - }); - } - - return this.workspaceMigrationFieldFactory.create( - objectMetadataCollection, - fieldMetadataUpdateCollection, - WorkspaceMigrationBuilderAction.UPDATE, - ); - } -} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/interfaces/workspace-health-fix-kind.interface.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/interfaces/workspace-health-fix-kind.interface.ts deleted file mode 100644 index 907c02f2eed..00000000000 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/interfaces/workspace-health-fix-kind.interface.ts +++ /dev/null @@ -1,6 +0,0 @@ -export enum WorkspaceHealthFixKind { - Nullable = 'nullable', - Type = 'type', - DefaultValue = 'default-value', - MissingColumn = 'missing-column', -} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/interfaces/workspace-health-issue.interface.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/interfaces/workspace-health-issue.interface.ts deleted file mode 100644 index 77b50d59c8d..00000000000 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/interfaces/workspace-health-issue.interface.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { type WorkspaceTableStructure } from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-table-definition.interface'; - -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; - -export enum WorkspaceHealthIssueType { - MISSING_TABLE = 'MISSING_TABLE', - TABLE_NAME_SHOULD_BE_CUSTOM = 'TABLE_NAME_SHOULD_BE_CUSTOM', - TABLE_TARGET_TABLE_NAME_NOT_VALID = 'TABLE_TARGET_TABLE_NAME_NOT_VALID', - TABLE_DATA_SOURCE_ID_NOT_VALID = 'TABLE_DATA_SOURCE_ID_NOT_VALID', - TABLE_NAME_NOT_VALID = 'TABLE_NAME_NOT_VALID', - MISSING_COLUMN = 'MISSING_COLUMN', - MISSING_INDEX = 'MISSING_INDEX', - MISSING_FOREIGN_KEY = 'MISSING_FOREIGN_KEY', - MISSING_COMPOSITE_TYPE = 'MISSING_COMPOSITE_TYPE', - COLUMN_NAME_SHOULD_NOT_BE_PREFIXED = 'COLUMN_NAME_SHOULD_NOT_BE_PREFIXED', - COLUMN_NAME_SHOULD_NOT_BE_CUSTOM = 'COLUMN_NAME_SHOULD_NOT_BE_CUSTOM', - COLUMN_OBJECT_REFERENCE_INVALID = 'COLUMN_OBJECT_REFERENCE_INVALID', - COLUMN_NAME_NOT_VALID = 'COLUMN_NAME_NOT_VALID', - COLUMN_TYPE_NOT_VALID = 'COLUMN_TYPE_NOT_VALID', - COLUMN_DATA_TYPE_CONFLICT = 'COLUMN_DATA_TYPE_CONFLICT', - COLUMN_NULLABILITY_CONFLICT = 'COLUMN_NULLABILITY_CONFLICT', - COLUMN_DEFAULT_VALUE_CONFLICT = 'COLUMN_DEFAULT_VALUE_CONFLICT', - COLUMN_DEFAULT_VALUE_NOT_VALID = 'COLUMN_DEFAULT_VALUE_NOT_VALID', - COLUMN_OPTIONS_NOT_VALID = 'COLUMN_OPTIONS_NOT_VALID', - RELATION_METADATA_NOT_VALID = 'RELATION_METADATA_NOT_VALID', - RELATION_FROM_OR_TO_FIELD_METADATA_NOT_VALID = 'RELATION_FROM_OR_TO_FIELD_METADATA_NOT_VALID', - RELATION_FOREIGN_KEY_NOT_VALID = 'RELATION_FOREIGN_KEY_NOT_VALID', - RELATION_FOREIGN_KEY_CONFLICT = 'RELATION_FOREIGN_KEY_CONFLICT', - RELATION_FOREIGN_KEY_ON_DELETE_ACTION_CONFLICT = 'RELATION_FOREIGN_KEY_ON_DELETE_ACTION_CONFLICT', - RELATION_TYPE_NOT_VALID = 'RELATION_TYPE_NOT_VALID', -} - -/** - * Table issues - */ -export type WorkspaceTableIssueTypes = - | WorkspaceHealthIssueType.MISSING_TABLE - | WorkspaceHealthIssueType.TABLE_NAME_SHOULD_BE_CUSTOM - | WorkspaceHealthIssueType.TABLE_TARGET_TABLE_NAME_NOT_VALID - | WorkspaceHealthIssueType.TABLE_DATA_SOURCE_ID_NOT_VALID - | WorkspaceHealthIssueType.TABLE_NAME_NOT_VALID; - -export interface WorkspaceHealthTableIssue { - type: T; - objectMetadata: ObjectMetadataEntity; - message: string; -} - -/** - * Column issues - */ -export type WorkspaceColumnIssueTypes = - | WorkspaceHealthIssueType.MISSING_COLUMN - | WorkspaceHealthIssueType.MISSING_INDEX - | WorkspaceHealthIssueType.MISSING_FOREIGN_KEY - | WorkspaceHealthIssueType.MISSING_COMPOSITE_TYPE - | WorkspaceHealthIssueType.COLUMN_NAME_SHOULD_NOT_BE_PREFIXED - | WorkspaceHealthIssueType.COLUMN_NAME_SHOULD_NOT_BE_CUSTOM - | WorkspaceHealthIssueType.COLUMN_OBJECT_REFERENCE_INVALID - | WorkspaceHealthIssueType.COLUMN_NAME_NOT_VALID - | WorkspaceHealthIssueType.COLUMN_TYPE_NOT_VALID - | WorkspaceHealthIssueType.COLUMN_DATA_TYPE_CONFLICT - | WorkspaceHealthIssueType.COLUMN_NULLABILITY_CONFLICT - | WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT - | WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_NOT_VALID - | WorkspaceHealthIssueType.COLUMN_OPTIONS_NOT_VALID; - -export interface WorkspaceHealthColumnIssue< - T extends WorkspaceColumnIssueTypes, -> { - type: T; - fieldMetadata: FieldMetadataEntity; - columnStructure?: WorkspaceTableStructure; - columnStructures?: WorkspaceTableStructure[]; - message: string; -} - -/** - * Relation issues - */ -export type WorkspaceRelationIssueTypes = - | WorkspaceHealthIssueType.RELATION_METADATA_NOT_VALID - | WorkspaceHealthIssueType.RELATION_FROM_OR_TO_FIELD_METADATA_NOT_VALID - | WorkspaceHealthIssueType.RELATION_FOREIGN_KEY_NOT_VALID - | WorkspaceHealthIssueType.RELATION_FOREIGN_KEY_CONFLICT - | WorkspaceHealthIssueType.RELATION_FOREIGN_KEY_ON_DELETE_ACTION_CONFLICT - | WorkspaceHealthIssueType.RELATION_TYPE_NOT_VALID; - -/** - * Get the interface for the issue type - */ -export type WorkspaceIssueTypeToInterface = - T extends WorkspaceTableIssueTypes - ? WorkspaceHealthTableIssue - : T extends WorkspaceColumnIssueTypes - ? WorkspaceHealthColumnIssue - : never; - -/** - * Union of all issues - */ -export type WorkspaceHealthIssue = - WorkspaceIssueTypeToInterface; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/interfaces/workspace-health-options.interface.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/interfaces/workspace-health-options.interface.ts deleted file mode 100644 index afe21015676..00000000000 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/interfaces/workspace-health-options.interface.ts +++ /dev/null @@ -1,9 +0,0 @@ -export enum WorkspaceHealthMode { - Structure = 'structure', - Metadata = 'metadata', - All = 'all', -} - -export interface WorkspaceHealthOptions { - mode: WorkspaceHealthMode; -} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/interfaces/workspace-table-definition.interface.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/interfaces/workspace-table-definition.interface.ts deleted file mode 100644 index 543db747ff4..00000000000 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/interfaces/workspace-table-definition.interface.ts +++ /dev/null @@ -1,18 +0,0 @@ -export interface WorkspaceTableStructure { - tableSchema: string; - tableName: string; - columnName: string; - dataType: string; - columnDefault: string; - isNullable: boolean; - isUnique: boolean; - isPrimaryKey: boolean; - isForeignKey: boolean; - isArray: boolean; - onUpdateAction: string; - onDeleteAction: string; -} - -export type WorkspaceTableStructureResult = { - [P in keyof WorkspaceTableStructure]: string; -}; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/database-structure.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/database-structure.service.ts deleted file mode 100644 index 26e330b68cb..00000000000 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/database-structure.service.ts +++ /dev/null @@ -1,328 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { InjectDataSource } from '@nestjs/typeorm'; - -import { - FieldMetadataType, - compositeTypeDefinitions, -} from 'twenty-shared/types'; -import { DataSource, type ColumnType } from 'typeorm'; -import { type ColumnMetadata } from 'typeorm/metadata/ColumnMetadata'; - -import { - type FieldMetadataDefaultValue, - type FieldMetadataFunctionDefaultValue, -} from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface'; -import { - type WorkspaceTableStructure, - type WorkspaceTableStructureResult, -} from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-table-definition.interface'; - -import { type FieldMetadataDefaultValueFunctionNames } from 'src/engine/metadata-modules/field-metadata/dtos/default-value.input'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { isFunctionDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/is-function-default-value.util'; -import { serializeFunctionDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-function-default-value.util'; -import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util'; -import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; -import { isMorphOrRelationFieldMetadataType } from 'src/engine/utils/is-morph-or-relation-field-metadata-type.util'; - -@Injectable() -export class DatabaseStructureService { - constructor( - @InjectDataSource() - private readonly coreDataSource: DataSource, - ) {} - - async getWorkspaceTableColumns( - schemaName: string, - tableName: string, - ): Promise { - const results = await this.coreDataSource.query< - WorkspaceTableStructureResult[] - >(` - WITH foreign_keys AS ( - SELECT - kcu.table_schema AS schema_name, - kcu.table_name AS table_name, - kcu.column_name AS column_name, - tc.constraint_name AS constraint_name - FROM - information_schema.key_column_usage AS kcu - JOIN - information_schema.table_constraints AS tc - ON tc.constraint_name = kcu.constraint_name - AND tc.table_schema = kcu.table_schema - WHERE - tc.constraint_type = 'FOREIGN KEY' - AND tc.table_schema = '${schemaName}' - AND tc.table_name = '${tableName}' - ), - unique_constraints AS ( - SELECT - tc.table_schema AS schema_name, - tc.table_name AS table_name, - kcu.column_name AS column_name - FROM - information_schema.key_column_usage AS kcu - JOIN - information_schema.table_constraints AS tc - ON tc.constraint_name = kcu.constraint_name - AND tc.table_schema = kcu.table_schema - WHERE - tc.constraint_type = 'UNIQUE' - AND tc.table_schema = '${schemaName}' - AND tc.table_name = '${tableName}' - ) - SELECT - c.table_schema AS "tableSchema", - c.table_name AS "tableName", - c.column_name AS "columnName", - CASE - WHEN c.data_type = 'ARRAY' THEN - (SELECT typname FROM pg_type WHERE oid = t.typelem) - WHEN c.data_type = 'USER-DEFINED' THEN - c.udt_name - ELSE - c.data_type - END AS "dataType", - c.is_nullable AS "isNullable", - c.column_default AS "columnDefault", - CASE - WHEN pk.constraint_type = 'PRIMARY KEY' THEN 'TRUE' - ELSE 'FALSE' - END AS "isPrimaryKey", - CASE - WHEN fk.constraint_name IS NOT NULL THEN 'TRUE' - ELSE 'FALSE' - END AS "isForeignKey", - CASE - WHEN uc.column_name IS NOT NULL THEN 'TRUE' - ELSE 'FALSE' - END AS "isUnique", - CASE - WHEN c.data_type = 'ARRAY' THEN 'TRUE' - ELSE 'FALSE' - END AS "isArray", - rc.update_rule AS "onUpdateAction", - rc.delete_rule AS "onDeleteAction" - FROM - information_schema.columns AS c - LEFT JOIN pg_type t ON t.typname = c.udt_name - LEFT JOIN - information_schema.constraint_column_usage AS ccu - ON c.column_name = ccu.column_name - AND c.table_name = ccu.table_name - AND c.table_schema = ccu.table_schema - LEFT JOIN - information_schema.table_constraints AS pk - ON pk.constraint_name = ccu.constraint_name - AND pk.constraint_type = 'PRIMARY KEY' - AND pk.table_name = c.table_name - AND pk.table_schema = c.table_schema - LEFT JOIN - foreign_keys AS fk - ON c.table_schema = fk.schema_name - AND c.table_name = fk.table_name - AND c.column_name = fk.column_name - LEFT JOIN - unique_constraints AS uc - ON c.table_schema = uc.schema_name - AND c.table_name = uc.table_name - AND c.column_name = uc.column_name - LEFT JOIN - information_schema.referential_constraints AS rc - ON rc.constraint_name = fk.constraint_name - AND rc.constraint_schema = '${schemaName}' - WHERE - c.table_schema = '${schemaName}' - AND c.table_name = '${tableName}'; - `); - - if (!results || results.length === 0) { - return []; - } - - return results.map((item) => ({ - ...item, - dataType: item.isArray === 'TRUE' ? `${item.dataType}[]` : item.dataType, - isNullable: item.isNullable === 'YES', - isPrimaryKey: item.isPrimaryKey === 'TRUE', - isForeignKey: item.isForeignKey === 'TRUE', - isUnique: item.isUnique === 'TRUE', - isArray: item.isArray === 'TRUE', - })); - } - - getPostgresDataTypes(fieldMetadata: FieldMetadataEntity): string[] { - const normalizer = ( - type: FieldMetadataType, - isArray: boolean | undefined, - columnName: string, - ) => { - const typeORMType = fieldMetadataTypeToColumnType(type); - - // Compute enum name to compare data type properly - if (typeORMType === 'enum') { - const objectName = computeObjectTargetTable(fieldMetadata.object); - - return `${objectName}_${columnName}_enum${isArray ? '[]' : ''}`; - } - - return this.coreDataSource.driver.normalizeType({ - type: typeORMType, - }); - }; - - if (isCompositeFieldMetadataType(fieldMetadata.type)) { - const compositeType = compositeTypeDefinitions.get(fieldMetadata.type); - - if (!compositeType) { - throw new Error( - `Composite type definition not found for ${fieldMetadata.type}`, - ); - } - - return compositeType.properties.map((compositeProperty) => - normalizer( - compositeProperty.type, - compositeProperty.isArray, - compositeProperty.name, - ), - ); - } - - return [ - normalizer( - fieldMetadata.type, - fieldMetadata.type === FieldMetadataType.MULTI_SELECT, - fieldMetadata.name, - ), - ]; - } - - getFieldMetadataTypeFromPostgresDataType( - postgresDataType: string, - ): FieldMetadataType | null { - const types = Object.values(FieldMetadataType).filter((type) => { - // We're skipping composite and relation types, as they're not directly mapped to a column type - if (isCompositeFieldMetadataType(type)) { - return false; - } - - if (isMorphOrRelationFieldMetadataType(type)) { - return false; - } - - return true; - }); - - for (const type of types) { - const typeORMType = fieldMetadataTypeToColumnType( - FieldMetadataType[type], - ) as ColumnType; - const dataType = this.coreDataSource.driver.normalizeType({ - type: typeORMType, - }); - - if (postgresDataType === dataType) { - return FieldMetadataType[type]; - } - } - - return null; - } - - getPostgresDefaults( - fieldMetadataType: FieldMetadataType, - initialDefaultValue: - | FieldMetadataDefaultValue - // Old format for default values - // TODO: Should be removed once all default values are migrated - | { type: FieldMetadataDefaultValueFunctionNames } - | null, - ): (string | null | undefined)[] { - const normalizer = ( - type: FieldMetadataType, - defaultValue: - | FieldMetadataDefaultValue - | { type: FieldMetadataDefaultValueFunctionNames } - | null, - ) => { - const typeORMType = fieldMetadataTypeToColumnType(type) as ColumnType; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let value: any = - // Old formart default values - defaultValue && - typeof defaultValue === 'object' && - 'value' in defaultValue - ? defaultValue.value - : defaultValue; - - // Old format for default values - // TODO: Should be removed once all default values are migrated - if ( - defaultValue && - typeof defaultValue === 'object' && - 'type' in defaultValue - ) { - return this.computeFunctionDefaultValue(defaultValue.type); - } - - if (isFunctionDefaultValue(value)) { - return this.computeFunctionDefaultValue(value); - } - - if (typeof value === 'number') { - return value.toString(); - } - - // Remove leading and trailing single quotes for string default values as it's already handled by TypeORM - if (typeof value === 'string' && value.match(/^'.*'$/)) { - value = value.replace(/^'/, '').replace(/'$/, ''); - } - - return this.coreDataSource.driver.normalizeDefault({ - type: typeORMType, - default: value, - isArray: false, - // Workaround to use normalizeDefault without a complete ColumnMetadata object - } as ColumnMetadata); - }; - - if (isCompositeFieldMetadataType(fieldMetadataType)) { - const compositeType = compositeTypeDefinitions.get(fieldMetadataType); - - if (!compositeType) { - throw new Error( - `Composite type definition not found for ${fieldMetadataType}`, - ); - } - - return compositeType.properties.map((compositeProperty) => - normalizer( - compositeProperty.type, - typeof initialDefaultValue === 'object' - ? // @ts-expect-error legacy noImplicitAny - initialDefaultValue?.[compositeProperty.name] - : null, - ), - ); - } - - return [normalizer(fieldMetadataType, initialDefaultValue)]; - } - - private computeFunctionDefaultValue( - value: FieldMetadataFunctionDefaultValue, - ) { - const serializedDefaultValue = serializeFunctionDefaultValue(value); - - // Special case for uuid_generate_v4() default value - if (serializedDefaultValue === 'public.uuid_generate_v4()') { - return 'uuid_generate_v4()'; - } - - return serializedDefaultValue; - } -} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/field-metadata-health.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/field-metadata-health.service.ts deleted file mode 100644 index 816d99bae38..00000000000 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/field-metadata-health.service.ts +++ /dev/null @@ -1,347 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { - FieldMetadataType, - compositeTypeDefinitions, -} from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; - -import { - type WorkspaceHealthIssue, - WorkspaceHealthIssueType, -} from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-health-issue.interface'; -import { type WorkspaceHealthOptions } from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-health-options.interface'; -import { type WorkspaceTableStructure } from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-table-definition.interface'; - -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { - computeColumnName, - computeCompositeColumnName, -} from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; -import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util'; -import { serializeDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-default-value'; -import { validateDefaultValueForType } from 'src/engine/metadata-modules/field-metadata/utils/validate-default-value-for-type.util'; -import { validateOptionsForType } from 'src/engine/metadata-modules/field-metadata/utils/validate-options-for-type.util'; -import { customNamePrefix } from 'src/engine/utils/compute-table-name.util'; -import { isMorphOrRelationFieldMetadataType } from 'src/engine/utils/is-morph-or-relation-field-metadata-type.util'; -import { DatabaseStructureService } from 'src/engine/workspace-manager/workspace-health/services/database-structure.service'; -import { validName } from 'src/engine/workspace-manager/workspace-health/utils/valid-name.util'; - -@Injectable() -export class FieldMetadataHealthService { - constructor( - private readonly databaseStructureService: DatabaseStructureService, - ) {} - - async healthCheck( - tableName: string, - workspaceTableColumns: WorkspaceTableStructure[], - fieldMetadataCollection: FieldMetadataEntity[], - options: WorkspaceHealthOptions, - ): Promise { - const issues: WorkspaceHealthIssue[] = []; - - for (const fieldMetadata of fieldMetadataCollection) { - // Relation metadata are checked in another service - if (isMorphOrRelationFieldMetadataType(fieldMetadata.type)) { - continue; - } - - const fieldIssues = await this.healthCheckField( - tableName, - workspaceTableColumns, - fieldMetadata, - options, - ); - - issues.push(...fieldIssues); - } - - return issues; - } - - private async healthCheckField( - tableName: string, - workspaceTableColumns: WorkspaceTableStructure[], - fieldMetadata: FieldMetadataEntity, - options: WorkspaceHealthOptions, - ): Promise { - const issues: WorkspaceHealthIssue[] = []; - - if (options.mode === 'structure' || options.mode === 'all') { - const structureIssues = this.structureFieldCheck( - tableName, - workspaceTableColumns, - fieldMetadata, - ); - - issues.push(...structureIssues); - } - - if (options.mode === 'metadata' || options.mode === 'all') { - const metadataIssues = this.metadataFieldCheck(tableName, fieldMetadata); - - issues.push(...metadataIssues); - } - - return issues; - } - - private structureFieldCheck( - tableName: string, - workspaceTableColumns: WorkspaceTableStructure[], - fieldMetadata: FieldMetadataEntity, - ): WorkspaceHealthIssue[] { - const dataTypes = - this.databaseStructureService.getPostgresDataTypes(fieldMetadata); - const issues: WorkspaceHealthIssue[] = []; - let columnNames: string[] = []; - - if (isCompositeFieldMetadataType(fieldMetadata.type)) { - const compositeType = compositeTypeDefinitions.get(fieldMetadata.type); - - if (!compositeType) { - throw new Error(`Composite type ${fieldMetadata.type} is not defined`); - } - - columnNames = compositeType.properties.map((compositeProperty) => - computeCompositeColumnName(fieldMetadata, compositeProperty), - ); - } else { - columnNames = [computeColumnName(fieldMetadata)]; - } - - const defaultValues = this.databaseStructureService.getPostgresDefaults( - fieldMetadata.type, - fieldMetadata.defaultValue, - ); - - // Check if column exist in database - const columnStructureMap = workspaceTableColumns.reduce( - (acc, workspaceTableColumn) => { - const columnName = workspaceTableColumn.columnName; - - if (columnNames.includes(columnName)) { - acc[columnName] = workspaceTableColumn; - } - - return acc; - }, - {} as { [key: string]: WorkspaceTableStructure }, - ); - - for (const [index, columnName] of columnNames.entries()) { - const columnStructure = columnStructureMap[columnName]; - - if (!columnStructure) { - issues.push({ - type: WorkspaceHealthIssueType.MISSING_COLUMN, - fieldMetadata, - columnStructures: workspaceTableColumns, - message: `Column ${columnName} not found in table ${tableName}`, - }); - - continue; - } - - const columnDefaultValue = - columnStructure.columnDefault?.split('::')?.[0]; - - // Check if column data type is the same - if (!dataTypes[index] || columnStructure.dataType !== dataTypes[index]) { - issues.push({ - type: WorkspaceHealthIssueType.COLUMN_DATA_TYPE_CONFLICT, - fieldMetadata, - columnStructure, - message: `Column ${columnName} type is not the same as the field metadata type "${columnStructure.dataType}" !== "${dataTypes[index]}"`, - }); - } - - if (columnStructure.isNullable !== fieldMetadata.isNullable) { - issues.push({ - type: WorkspaceHealthIssueType.COLUMN_NULLABILITY_CONFLICT, - fieldMetadata, - columnStructure, - message: `Column ${columnName} is expected to be ${ - fieldMetadata.isNullable ? 'nullable' : 'not nullable' - } but is ${columnStructure.isNullable ? 'nullable' : 'not nullable'}`, - }); - } - - if (columnDefaultValue && isEnumFieldMetadataType(fieldMetadata.type)) { - const enumValues = fieldMetadata.options?.map((option) => - serializeDefaultValue(`'${option.value}'`), - ); - - if (!isDefined(enumValues)) { - issues.push({ - type: WorkspaceHealthIssueType.COLUMN_OPTIONS_NOT_VALID, - fieldMetadata, - columnStructure, - message: `Column options of ${fieldMetadata.name} are not defined`, - }); - } - - if (isDefined(enumValues) && !enumValues.includes(columnDefaultValue)) { - issues.push({ - type: WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_NOT_VALID, - fieldMetadata, - columnStructure, - message: `Column ${columnName} default value is not in the enum values "${columnDefaultValue}" NOT IN "${enumValues}"`, - }); - } - } - - if (columnDefaultValue !== defaultValues[index]) { - issues.push({ - type: WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT, - fieldMetadata, - columnStructure, - message: `Column ${columnName} default value is not the same as the field metadata default value "${columnStructure.columnDefault}" !== "${defaultValues[index]}"`, - }); - } - } - - return issues; - } - - private metadataFieldCheck( - tableName: string, - fieldMetadata: FieldMetadataEntity, - ): WorkspaceHealthIssue[] { - const issues: WorkspaceHealthIssue[] = []; - const columnName = fieldMetadata.name; - const defaultValueIssues = this.defaultValueHealthCheck(fieldMetadata); - - if (fieldMetadata.name.startsWith(customNamePrefix)) { - issues.push({ - type: WorkspaceHealthIssueType.COLUMN_NAME_SHOULD_NOT_BE_PREFIXED, - fieldMetadata, - message: `Column ${columnName} should not be prefixed with "${customNamePrefix}"`, - }); - } - - if (fieldMetadata.isCustom && columnName?.startsWith(customNamePrefix)) { - issues.push({ - type: WorkspaceHealthIssueType.COLUMN_NAME_SHOULD_NOT_BE_CUSTOM, - fieldMetadata, - message: `Column ${columnName} is marked as custom in table ${tableName} but and start with "_", this behavior has been removed. Please remove the prefix.`, - }); - } - - if (!fieldMetadata.objectMetadataId) { - issues.push({ - type: WorkspaceHealthIssueType.COLUMN_OBJECT_REFERENCE_INVALID, - fieldMetadata, - message: `Column ${columnName} doesn't have a valid object metadata id`, - }); - } - - if (!Object.values(FieldMetadataType).includes(fieldMetadata.type)) { - issues.push({ - type: WorkspaceHealthIssueType.COLUMN_TYPE_NOT_VALID, - fieldMetadata, - message: `Column ${columnName} doesn't have a valid field metadata type`, - }); - } - - if ( - !fieldMetadata.name || - !validName(fieldMetadata.name) || - !fieldMetadata.label - ) { - issues.push({ - type: WorkspaceHealthIssueType.COLUMN_NAME_NOT_VALID, - fieldMetadata, - message: `Column ${columnName} doesn't have a valid name or label`, - }); - } - - if (!isDefined(fieldMetadata.options)) { - issues.push({ - type: WorkspaceHealthIssueType.COLUMN_OPTIONS_NOT_VALID, - fieldMetadata, - message: `Column options of ${fieldMetadata.name} are not defined`, - }); - } - - if ( - isEnumFieldMetadataType(fieldMetadata.type) && - isDefined(fieldMetadata.options) && - !validateOptionsForType(fieldMetadata.type, fieldMetadata.options) - ) { - issues.push({ - type: WorkspaceHealthIssueType.COLUMN_OPTIONS_NOT_VALID, - fieldMetadata, - message: `Column options of ${fieldMetadata.name} is not valid`, - }); - } - - issues.push(...defaultValueIssues); - - return issues; - } - - private defaultValueHealthCheck( - fieldMetadata: FieldMetadataEntity, - ): WorkspaceHealthIssue[] { - const issues: WorkspaceHealthIssue[] = []; - - if ( - !validateDefaultValueForType( - fieldMetadata.type, - fieldMetadata.defaultValue, - ).isValid - ) { - issues.push({ - type: WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_NOT_VALID, - fieldMetadata, - message: `Column default value for composite type ${fieldMetadata.type} is not well structured`, - }); - } - - if ( - isEnumFieldMetadataType(fieldMetadata.type) && - fieldMetadata.defaultValue - ) { - const enumValues = fieldMetadata.options?.map((option) => - serializeDefaultValue(`'${option.value}'`), - ); - const metadataDefaultValue = fieldMetadata.defaultValue; - - if (isDefined(metadataDefaultValue)) { - if (fieldMetadata.type === FieldMetadataType.MULTI_SELECT) { - if (!Array.isArray(metadataDefaultValue)) { - issues.push({ - type: WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_NOT_VALID, - fieldMetadata, - message: `Column default value for multi-select must be an array, got "${metadataDefaultValue}"`, - }); - } else { - metadataDefaultValue.forEach((value) => { - if (isDefined(enumValues) && !enumValues.includes(value)) { - issues.push({ - type: WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_NOT_VALID, - fieldMetadata, - message: `Column default value "${value}" is not in the enum values "${enumValues}"`, - }); - } - }); - } - } else if ( - isDefined(enumValues) && - !enumValues.includes(metadataDefaultValue as string) - ) { - issues.push({ - type: WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_NOT_VALID, - fieldMetadata, - message: `Column default value is not in the enum values "${metadataDefaultValue}" NOT IN "${enumValues}"`, - }); - } - } - } - - return issues; - } -} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/object-metadata-health.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/object-metadata-health.service.ts deleted file mode 100644 index db52bba3d1f..00000000000 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/object-metadata-health.service.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { InjectDataSource } from '@nestjs/typeorm'; - -import { DataSource } from 'typeorm'; - -import { - type WorkspaceHealthIssue, - WorkspaceHealthIssueType, -} from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-health-issue.interface'; -import { type WorkspaceHealthOptions } from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-health-options.interface'; - -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; -import { validName } from 'src/engine/workspace-manager/workspace-health/utils/valid-name.util'; - -@Injectable() -export class ObjectMetadataHealthService { - constructor( - @InjectDataSource() - private readonly coreDataSource: DataSource, - ) {} - - async healthCheck( - schemaName: string, - objectMetadata: ObjectMetadataEntity, - options: WorkspaceHealthOptions, - ): Promise { - const issues: WorkspaceHealthIssue[] = []; - - if (options.mode === 'structure' || options.mode === 'all') { - const structureIssues = await this.structureObjectCheck( - schemaName, - objectMetadata, - ); - - issues.push(...structureIssues); - } - - if (options.mode === 'metadata' || options.mode === 'all') { - const metadataIssues = this.metadataObjectCheck(objectMetadata); - - issues.push(...metadataIssues); - } - - return issues; - } - - /** - * Check the structure health of the table based on metadata - * @param schemaName - * @param objectMetadata - * @returns WorkspaceHealthIssue[] - */ - private async structureObjectCheck( - schemaName: string, - objectMetadata: ObjectMetadataEntity, - ): Promise { - const issues: WorkspaceHealthIssue[] = []; - - // Check if the table exist in database - const tableExist = await this.coreDataSource.query( - `SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_schema = '${schemaName}' - AND table_name = '${computeObjectTargetTable(objectMetadata)}')`, - ); - - if (!tableExist) { - issues.push({ - type: WorkspaceHealthIssueType.MISSING_TABLE, - objectMetadata, - message: `Table ${computeObjectTargetTable( - objectMetadata, - )} not found in schema ${schemaName}`, - }); - - return issues; - } - - return issues; - } - - /** - * Check ObjectMetadata health - * @param objectMetadata - * @returns WorkspaceHealthIssue[] - */ - private metadataObjectCheck( - objectMetadata: ObjectMetadataEntity, - ): WorkspaceHealthIssue[] { - const issues: WorkspaceHealthIssue[] = []; - - if (!objectMetadata.dataSourceId) { - issues.push({ - type: WorkspaceHealthIssueType.TABLE_DATA_SOURCE_ID_NOT_VALID, - objectMetadata, - message: `Table ${computeObjectTargetTable( - objectMetadata, - )} doesn't have a data source`, - }); - } - - if ( - !objectMetadata.nameSingular || - !objectMetadata.namePlural || - !validName(objectMetadata.nameSingular) || - !validName(objectMetadata.namePlural) || - !objectMetadata.labelSingular || - !objectMetadata.labelPlural - ) { - issues.push({ - type: WorkspaceHealthIssueType.TABLE_NAME_NOT_VALID, - objectMetadata, - message: `Table ${computeObjectTargetTable( - objectMetadata, - )} doesn't have a valid name or label`, - }); - } - - return issues; - } -} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/workspace-fix.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/workspace-fix.service.ts deleted file mode 100644 index c0a5f21b5be..00000000000 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/workspace-fix.service.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { type EntityManager } from 'typeorm'; - -import { WorkspaceHealthFixKind } from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-health-fix-kind.interface'; -import { type WorkspaceHealthIssue } from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-health-issue.interface'; - -import { type WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { WorkspaceNullableFixer } from 'src/engine/workspace-manager/workspace-health/fixer/workspace-nullable.fixer'; -import { WorkspaceDefaultValueFixer } from 'src/engine/workspace-manager/workspace-health/fixer/workspace-default-value.fixer'; -import { WorkspaceTypeFixer } from 'src/engine/workspace-manager/workspace-health/fixer/workspace-type.fixer'; -import { type CompareEntity } from 'src/engine/workspace-manager/workspace-health/fixer/abstract-workspace.fixer'; -import { WorkspaceMissingColumnFixer } from 'src/engine/workspace-manager/workspace-health/fixer/workspace-missing-column.fixer'; - -@Injectable() -export class WorkspaceFixService { - constructor( - private readonly workspaceNullableFixer: WorkspaceNullableFixer, - private readonly workspaceDefaultValueFixer: WorkspaceDefaultValueFixer, - private readonly workspaceTypeFixer: WorkspaceTypeFixer, - private readonly workspaceMissingColumnFixer: WorkspaceMissingColumnFixer, - ) {} - - async createWorkspaceMigrations( - manager: EntityManager, - objectMetadataCollection: ObjectMetadataEntity[], - type: WorkspaceHealthFixKind, - issues: WorkspaceHealthIssue[], - ): Promise[]> { - switch (type) { - case WorkspaceHealthFixKind.Nullable: { - const filteredIssues = this.workspaceNullableFixer.filterIssues(issues); - - return this.workspaceNullableFixer.createWorkspaceMigrations( - manager, - objectMetadataCollection, - filteredIssues, - ); - } - case WorkspaceHealthFixKind.DefaultValue: { - const filteredIssues = - this.workspaceDefaultValueFixer.filterIssues(issues); - - return this.workspaceDefaultValueFixer.createWorkspaceMigrations( - manager, - objectMetadataCollection, - filteredIssues, - ); - } - case WorkspaceHealthFixKind.Type: { - const filteredIssues = this.workspaceTypeFixer.filterIssues(issues); - - return this.workspaceTypeFixer.createWorkspaceMigrations( - manager, - objectMetadataCollection, - filteredIssues, - ); - } - case WorkspaceHealthFixKind.MissingColumn: { - const filteredIssues = - this.workspaceMissingColumnFixer.filterIssues(issues); - - return this.workspaceMissingColumnFixer.createWorkspaceMigrations( - manager, - objectMetadataCollection, - filteredIssues, - ); - } - default: { - return []; - } - } - } - - async createMetadataUpdates( - manager: EntityManager, - objectMetadataCollection: ObjectMetadataEntity[], - type: WorkspaceHealthFixKind, - - issues: WorkspaceHealthIssue[], - ): Promise[]> { - switch (type) { - case WorkspaceHealthFixKind.DefaultValue: { - const filteredIssues = - this.workspaceDefaultValueFixer.filterIssues(issues); - - return this.workspaceDefaultValueFixer.createMetadataUpdates( - manager, - objectMetadataCollection, - filteredIssues, - ); - } - default: { - return []; - } - } - } -} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/utils/valid-name.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/utils/valid-name.util.ts deleted file mode 100644 index e53fce5d4ea..00000000000 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/utils/valid-name.util.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const validName = (name: string): boolean => { - return /^[a-zA-Z0-9_]+$/.test(name); -}; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/workspace-health.module.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/workspace-health.module.ts deleted file mode 100644 index 4cb4db7d2c6..00000000000 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/workspace-health.module.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Module } from '@nestjs/common'; - -import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; -import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; -import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module'; -import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; -import { DatabaseStructureService } from 'src/engine/workspace-manager/workspace-health/services/database-structure.service'; -import { FieldMetadataHealthService } from 'src/engine/workspace-manager/workspace-health/services/field-metadata-health.service'; -import { ObjectMetadataHealthService } from 'src/engine/workspace-manager/workspace-health/services/object-metadata-health.service'; -import { WorkspaceHealthService } from 'src/engine/workspace-manager/workspace-health/workspace-health.service'; -import { WorkspaceMigrationBuilderModule } from 'src/engine/workspace-manager/workspace-migration-builder/workspace-migration-builder.module'; -import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module'; -import { WorkspaceFixService } from 'src/engine/workspace-manager/workspace-health/services/workspace-fix.service'; - -import { workspaceFixers } from './fixer'; - -@Module({ - imports: [ - DataSourceModule, - TypeORMModule, - ObjectMetadataModule, - WorkspaceDataSourceModule, - WorkspaceMigrationRunnerModule, - WorkspaceMigrationBuilderModule, - ], - providers: [ - ...workspaceFixers, - WorkspaceHealthService, - DatabaseStructureService, - ObjectMetadataHealthService, - FieldMetadataHealthService, - WorkspaceFixService, - ], - exports: [WorkspaceHealthService, DatabaseStructureService], -}) -export class WorkspaceHealthModule {} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/workspace-health.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/workspace-health.service.ts deleted file mode 100644 index b84eeecce17..00000000000 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/workspace-health.service.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { Injectable, Logger, NotFoundException } from '@nestjs/common'; -import { InjectDataSource } from '@nestjs/typeorm'; - -import { DataSource } from 'typeorm'; - -import { type WorkspaceHealthFixKind } from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-health-fix-kind.interface'; -import { type WorkspaceHealthIssue } from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-health-issue.interface'; -import { - WorkspaceHealthMode, - type WorkspaceHealthOptions, -} from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-health-options.interface'; - -import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service'; -import { WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; -import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; -import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util'; -import { DatabaseStructureService } from 'src/engine/workspace-manager/workspace-health/services/database-structure.service'; -import { FieldMetadataHealthService } from 'src/engine/workspace-manager/workspace-health/services/field-metadata-health.service'; -import { ObjectMetadataHealthService } from 'src/engine/workspace-manager/workspace-health/services/object-metadata-health.service'; -import { WorkspaceFixService } from 'src/engine/workspace-manager/workspace-health/services/workspace-fix.service'; -import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service'; - -@Injectable() -export class WorkspaceHealthService { - private readonly logger = new Logger(WorkspaceHealthService.name); - - constructor( - @InjectDataSource() - private readonly coreDataSource: DataSource, - private readonly dataSourceService: DataSourceService, - private readonly objectMetadataService: ObjectMetadataServiceV2, - private readonly databaseStructureService: DatabaseStructureService, - private readonly objectMetadataHealthService: ObjectMetadataHealthService, - private readonly fieldMetadataHealthService: FieldMetadataHealthService, - private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService, - private readonly workspaceFixService: WorkspaceFixService, - ) {} - - async healthCheck( - workspaceId: string, - options: WorkspaceHealthOptions = { mode: WorkspaceHealthMode.All }, - ): Promise { - const schemaName = getWorkspaceSchemaName(workspaceId); - const issues: WorkspaceHealthIssue[] = []; - - const dataSourceMetadata = - await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail( - workspaceId, - ); - - // Check if a data source exists for this workspace - if (!dataSourceMetadata) { - throw new NotFoundException( - `DataSource for workspace id ${workspaceId} not found`, - ); - } - - const objectMetadataCollection = - await this.objectMetadataService.findManyWithinWorkspace(workspaceId); - - // Check if object metadata exists for this workspace - if (!objectMetadataCollection || objectMetadataCollection.length === 0) { - throw new NotFoundException(`Workspace with id ${workspaceId} not found`); - } - - for (const objectMetadata of objectMetadataCollection) { - const tableName = computeObjectTargetTable(objectMetadata); - const workspaceTableColumns = - await this.databaseStructureService.getWorkspaceTableColumns( - schemaName, - tableName, - ); - - if (!workspaceTableColumns || workspaceTableColumns.length === 0) { - throw new NotFoundException( - `Table ${tableName} not found in schema ${schemaName}`, - ); - } - - // Check object metadata health - const objectIssues = await this.objectMetadataHealthService.healthCheck( - schemaName, - objectMetadata, - options, - ); - - issues.push(...objectIssues); - - // Check fields metadata health - const fieldIssues = await this.fieldMetadataHealthService.healthCheck( - computeObjectTargetTable(objectMetadata), - workspaceTableColumns, - objectMetadata.fields, - options, - ); - - issues.push(...fieldIssues); - } - - return issues; - } - - async fixIssues( - workspaceId: string, - issues: WorkspaceHealthIssue[], - options: { - type: WorkspaceHealthFixKind; - applyChanges?: boolean; - }, - ): Promise<{ - workspaceMigrations: Partial[]; - metadataEntities: unknown[]; - }> { - let workspaceMigrations: Partial[] = []; - let metadataEntities: unknown[] = []; - - // Set default options - options.applyChanges ??= true; - - const queryRunner = this.coreDataSource.createQueryRunner(); - - await queryRunner.connect(); - await queryRunner.startTransaction(); - - const manager = queryRunner.manager; - - try { - const workspaceMigrationRepository = manager.getRepository( - WorkspaceMigrationEntity, - ); - const objectMetadataCollection = - await this.objectMetadataService.findManyWithinWorkspace(workspaceId); - - workspaceMigrations = - await this.workspaceFixService.createWorkspaceMigrations( - manager, - objectMetadataCollection, - options.type, - issues, - ); - - metadataEntities = await this.workspaceFixService.createMetadataUpdates( - manager, - objectMetadataCollection, - options.type, - issues, - ); - - // Save workspace migrations into the database - await workspaceMigrationRepository.save(workspaceMigrations); - - if (!options.applyChanges) { - // Rollback transactions - await queryRunner.rollbackTransaction(); - - await queryRunner.release(); - - return { - workspaceMigrations, - metadataEntities, - }; - } - - // Commit the transaction - await queryRunner.commitTransaction(); - - // Apply pending migrations - await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations( - workspaceId, - ); - } catch (error) { - await queryRunner.rollbackTransaction(); - this.logger.error('Fix of issues failed with:', error); - } finally { - await queryRunner.release(); - } - - return { - workspaceMigrations, - metadataEntities, - }; - } -} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-manager.module.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-manager.module.ts index 247c2b85c38..a07f39e1af1 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-manager.module.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-manager.module.ts @@ -8,6 +8,7 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent import { AiAgentModule } from 'src/engine/metadata-modules/ai/ai-agent/ai-agent.module'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module'; import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module'; import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity'; @@ -17,7 +18,6 @@ import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role. import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; import { DevSeederModule } from 'src/engine/workspace-manager/dev-seeder/dev-seeder.module'; -import { WorkspaceHealthModule } from 'src/engine/workspace-manager/workspace-health/workspace-health.module'; import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-v2.module'; import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.module'; @@ -32,10 +32,10 @@ import { WorkspaceManagerService } from './workspace-manager.service'; DevSeederModule, DataSourceModule, WorkspaceSyncMetadataModule, - WorkspaceHealthModule, FeatureFlagModule, PermissionsModule, AiAgentModule, + WorkspaceManyOrAllFlatEntityMapsCacheModule, TypeOrmModule.forFeature([UserWorkspaceEntity, WorkspaceEntity]), RoleModule, UserRoleModule, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts index 38fda2b9610..aedf8f86bdf 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts @@ -10,6 +10,7 @@ import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { type DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; +import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service'; import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; @@ -48,6 +49,7 @@ export class WorkspaceManagerService { private readonly roleTargetsRepository: Repository, protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, private readonly applicationService: ApplicationService, + private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, ) {} public async init({ @@ -133,20 +135,27 @@ export class WorkspaceManagerService { featureFlags: Record, twentyStandardFlatApplication: FlatApplication, ) { - const createdObjectMetadata = - await this.objectMetadataServiceV2.findManyWithinWorkspace(workspaceId); + const { flatObjectMetadataMaps, flatFieldMetadataMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatObjectMetadataMaps', 'flatFieldMetadataMaps'], + }, + ); await standardObjectsPrefillData( this.coreDataSource, dataSourceMetadata.schema, - createdObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, ); await prefillCoreViews({ twentyStandardFlatApplication, coreDataSource: this.coreDataSource, workspaceId, - objectMetadataItems: createdObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, workspaceSchemaName: dataSourceMetadata.schema, featureFlags, }); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/commands/workspace-sync-metadata-commands.module.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/commands/workspace-sync-metadata-commands.module.ts index f6efdfd4acc..8af5486778f 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/commands/workspace-sync-metadata-commands.module.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/commands/workspace-sync-metadata-commands.module.ts @@ -1,22 +1,20 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +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'; import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; -import { WorkspaceHealthModule } from 'src/engine/workspace-manager/workspace-health/workspace-health.module'; import { SyncWorkspaceLoggerModule } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/services/sync-workspace-logger.module'; import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.module'; -import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; import { SyncWorkspaceMetadataCommand } from './sync-workspace-metadata.command'; @Module({ imports: [ WorkspaceSyncMetadataModule, - WorkspaceHealthModule, WorkspaceModule, DataSourceModule, WorkspaceDataSourceModule, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-metadata-updater.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-metadata-updater.service.ts index e9c4485f3a1..2f730732f94 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-metadata-updater.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-metadata-updater.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; +import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata'; import { FieldMetadataType, compositeTypeDefinitions, @@ -15,7 +16,6 @@ import { } from 'typeorm'; import { type DeepPartial } from 'typeorm/common/DeepPartial'; import { v4 as uuidV4 } from 'uuid'; -import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata'; import { type PartialFieldMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-field-metadata.interface'; import { type PartialIndexMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-index-metadata.interface'; diff --git a/packages/twenty-server/src/modules/connected-account/query-hooks/connected-account-delete-one.pre-query.hook.ts b/packages/twenty-server/src/modules/connected-account/query-hooks/connected-account-delete-one.pre-query.hook.ts index e56b567246f..10aa462fb49 100644 --- a/packages/twenty-server/src/modules/connected-account/query-hooks/connected-account-delete-one.pre-query.hook.ts +++ b/packages/twenty-server/src/modules/connected-account/query-hooks/connected-account-delete-one.pre-query.hook.ts @@ -1,7 +1,7 @@ import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; import { assertIsDefinedOrThrow } from 'twenty-shared/utils'; +import { Repository } from 'typeorm'; import { type WorkspacePreQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface'; import { type DeleteOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; @@ -9,11 +9,12 @@ import { type DeleteOneResolverArgs } from 'src/engine/api/graphql/workspace-res import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator'; import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; +import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception'; +import { fromObjectMetadataEntityToFlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-object-metadata-entity-to-flat-object-metadata.util'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; -import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception'; @WorkspaceQueryHook(`connectedAccount.destroyOne`) export class ConnectedAccountDeleteOnePreQueryHook @@ -46,18 +47,23 @@ export class ConnectedAccountDeleteOnePreQueryHook connectedAccountId, }); - const objectMetadata = await this.objectMetadataRepository.findOneOrFail({ - where: { - nameSingular: 'messageChannel', - workspaceId: workspace.id, - }, - }); + const objectMetadataEntity = + await this.objectMetadataRepository.findOneOrFail({ + where: { + nameSingular: 'messageChannel', + workspaceId: workspace.id, + }, + relations: ['fields', 'indexMetadatas', 'views'], + }); + + const flatObjectMetadata = + fromObjectMetadataEntityToFlatObjectMetadata(objectMetadataEntity); // TODO: handle cascade events for delete this.workspaceEventEmitter.emitDatabaseBatchEvent({ objectMetadataNameSingular: 'messageChannel', action: DatabaseEventAction.DESTROYED, - objectMetadata, + objectMetadata: flatObjectMetadata, events: messageChannels.map((messageChannel) => ({ recordId: messageChannel.id, properties: { diff --git a/packages/twenty-server/src/modules/timeline/services/timeline-activity.service.ts b/packages/twenty-server/src/modules/timeline/services/timeline-activity.service.ts index bd7e64b3c81..6f684eed443 100644 --- a/packages/twenty-server/src/modules/timeline/services/timeline-activity.service.ts +++ b/packages/twenty-server/src/modules/timeline/services/timeline-activity.service.ts @@ -4,16 +4,18 @@ import { type ObjectRecord } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { In } from 'typeorm'; +import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util'; import { type ObjectRecordBaseEvent } from 'src/engine/core-modules/event-emitter/types/object-record.base.event'; +import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type'; import { parseEventNameOrThrow } from 'src/engine/workspace-event-emitter/utils/parse-event-name'; +import { NoteWorkspaceEntity } from 'src/modules/note/standard-objects/note.workspace-entity'; +import { TaskWorkspaceEntity } from 'src/modules/task/standard-objects/task.workspace-entity'; import { TimelineActivityRepository } from 'src/modules/timeline/repositories/timeline-activity.repository'; import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity'; import { type TimelineActivityPayload } from 'src/modules/timeline/types/timeline-activity-payload'; -import { NoteWorkspaceEntity } from 'src/modules/note/standard-objects/note.workspace-entity'; -import { TaskWorkspaceEntity } from 'src/modules/task/standard-objects/task.workspace-entity'; type ActivityType = 'note' | 'task'; @@ -23,6 +25,7 @@ export class TimelineActivityService { @InjectObjectMetadataRepository(TimelineActivityWorkspaceEntity) private readonly timelineActivityRepository: TimelineActivityRepository, private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, ) {} private targetObjects: Record = { @@ -279,6 +282,23 @@ export class TimelineActivityService { return []; } + const { flatFieldMetadataMaps } = + await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatFieldMetadataMaps'], + }, + ); + + const fields = getFlatFieldsFromFlatObjectMetadata( + objectMetadata, + flatFieldMetadataMaps, + ); + + const activityObjectMetadataId = fields.find( + (field) => field.name === activityType, + )?.relationTargetObjectMetadataId; + return events .map((event) => { const activity = activities.find( @@ -291,10 +311,6 @@ export class TimelineActivityService { return; } - const activityObjectMetadataId = objectMetadata.fields.find( - (field) => field.name === activityType, - )?.relationTargetObjectMetadataId; - if (!isDefined(activityObjectMetadataId)) { return; } diff --git a/packages/twenty-server/src/modules/timeline/timeline-activity.module.ts b/packages/twenty-server/src/modules/timeline/timeline-activity.module.ts index f31a420fdb3..03c37b2cf3e 100644 --- a/packages/twenty-server/src/modules/timeline/timeline-activity.module.ts +++ b/packages/twenty-server/src/modules/timeline/timeline-activity.module.ts @@ -1,5 +1,6 @@ import { Module } from '@nestjs/common'; +import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module'; import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module'; import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; import { TimelineActivityService } from 'src/modules/timeline/services/timeline-activity.service'; @@ -11,6 +12,7 @@ import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-o TimelineActivityWorkspaceEntity, ]), TwentyORMModule, + WorkspaceManyOrAllFlatEntityMapsCacheModule, ], providers: [TimelineActivityService], exports: [TimelineActivityService], diff --git a/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-common.workspace-service.ts b/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-common.workspace-service.ts index 822a6ca4aed..dc10507df3c 100644 --- a/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-common.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-common.workspace-service.ts @@ -2,13 +2,12 @@ import { Injectable } from '@nestjs/common'; import { isDefined } from 'twenty-shared/utils'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util'; -import { buildObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-metadata-item-with-field-maps.util'; import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; -import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util'; import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { @@ -32,8 +31,9 @@ import { } from 'src/modules/workflow/workflow-trigger/exceptions/workflow-trigger.exception'; export type ObjectMetadataInfo = { - objectMetadataItemWithFieldsMaps: ObjectMetadataItemWithFieldMaps; - objectMetadataMaps: ObjectMetadataMaps; + flatObjectMetadata: FlatObjectMetadata; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; }; @Injectable() @@ -95,55 +95,52 @@ export class WorkflowCommonWorkspaceService { return { ...workflowVersion, trigger: workflowVersion.trigger }; } - async getObjectMetadataMaps( - workspaceId: string, - ): Promise { - const { flatObjectMetadataMaps, flatFieldMetadataMaps, flatIndexMaps } = + async getFlatEntityMaps(workspaceId: string): Promise<{ + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; + objectIdByNameSingular: Record; + }> { + const { flatObjectMetadataMaps, flatFieldMetadataMaps } = await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { workspaceId, - flatMapsKeys: [ - 'flatObjectMetadataMaps', - 'flatFieldMetadataMaps', - 'flatIndexMaps', - ], + flatMapsKeys: ['flatObjectMetadataMaps', 'flatFieldMetadataMaps'], }, ); const { idByNameSingular } = buildObjectIdByNameMaps( flatObjectMetadataMaps, ); - const objectMetadataMaps: ObjectMetadataMaps = { - byId: {}, - idByNameSingular, + + return { + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular: idByNameSingular, }; - - for (const [id, flatObj] of Object.entries(flatObjectMetadataMaps.byId)) { - if (isDefined(flatObj)) { - objectMetadataMaps.byId[id] = buildObjectMetadataItemWithFieldMaps( - flatObj, - flatFieldMetadataMaps, - flatIndexMaps, - ); - } - } - - return objectMetadataMaps; } - async getObjectMetadataItemWithFieldsMaps( + async getObjectMetadataInfo( objectNameSingular: string, workspaceId: string, ): Promise { - const objectMetadataMaps = await this.getObjectMetadataMaps(workspaceId); + const { + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, + } = await this.getFlatEntityMaps(workspaceId); - const objectMetadataItemWithFieldsMaps = - getObjectMetadataMapItemByNameSingular( - objectMetadataMaps, - objectNameSingular, + const objectId = objectIdByNameSingular[objectNameSingular]; + + if (!isDefined(objectId)) { + throw new WorkflowCommonException( + `Failed to read: Object ${objectNameSingular} not found`, + WorkflowCommonExceptionCode.OBJECT_METADATA_NOT_FOUND, ); + } - if (!objectMetadataItemWithFieldsMaps) { + const flatObjectMetadata = flatObjectMetadataMaps.byId[objectId]; + + if (!isDefined(flatObjectMetadata)) { throw new WorkflowCommonException( `Failed to read: Object ${objectNameSingular} not found`, WorkflowCommonExceptionCode.OBJECT_METADATA_NOT_FOUND, @@ -151,8 +148,9 @@ export class WorkflowCommonWorkspaceService { } return { - objectMetadataItemWithFieldsMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }; } diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-form-response.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-form-response.spec.ts index 80466fd0c8e..320ca4e2ddf 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-form-response.spec.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-form-response.spec.ts @@ -1,13 +1,13 @@ import { FieldMetadataType } from 'twenty-shared/types'; -import { mockObjectMetadataItemsWithFieldMaps } from 'src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps'; +import { + mockFlatFieldMetadataMaps, + mockFlatObjectMetadataMaps, + mockObjectIdByNameSingular, +} from 'src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps'; import { generateFakeFormResponse } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-form-response'; import { type FormFieldMetadata } from 'src/modules/workflow/workflow-executor/workflow-actions/form/types/workflow-form-action-settings.type'; -const companyMockObjectMetadataItem = mockObjectMetadataItemsWithFieldMaps.find( - (item) => item.nameSingular === 'company', -)!; - describe('generateFakeFormResponse', () => { it('should generate fake responses for a form schema', async () => { const formFieldMetadataItems: FormFieldMetadata[] = [ @@ -41,19 +41,11 @@ describe('generateFakeFormResponse', () => { }, ]; - const mockObjectMetadataMaps = { - byId: { - [companyMockObjectMetadataItem.id]: companyMockObjectMetadataItem, - }, - idByNameSingular: { - [companyMockObjectMetadataItem.nameSingular]: - companyMockObjectMetadataItem.id, - }, - }; - const result = generateFakeFormResponse({ formFieldMetadataItems, - objectMetadataMaps: mockObjectMetadataMaps, + flatObjectMetadataMaps: mockFlatObjectMetadataMaps, + flatFieldMetadataMaps: mockFlatFieldMetadataMaps, + objectIdByNameSingular: mockObjectIdByNameSingular, }); expect(result).toEqual({ @@ -103,7 +95,7 @@ describe('generateFakeFormResponse', () => { }, }, name: { - fieldMetadataId: 'nameFieldMetadataId', + fieldMetadataId: 'nameFieldMetadataId-company', icon: 'test-field-icon', isLeaf: true, label: 'Name', diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-object-record-event.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-object-record-event.spec.ts index e28882fe592..1fd3bc6404c 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-object-record-event.spec.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-object-record-event.spec.ts @@ -1,5 +1,5 @@ import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; -import { mockObjectMetadataItemsWithFieldMaps } from 'src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps'; +import { mockCompanyObjectMetadataInfo } from 'src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps'; import { generateFakeObjectRecordEvent } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record-event'; import { generateObjectRecordFields } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields'; @@ -25,33 +25,13 @@ describe('generateFakeObjectRecordEvent', () => { }, }; - const companyMockObjectMetadataItem = - mockObjectMetadataItemsWithFieldMaps.find( - (item) => item.nameSingular === 'company', - )!; - - const mockObjectMetadataMaps = { - byId: { - [companyMockObjectMetadataItem.id]: companyMockObjectMetadataItem, - }, - idByNameSingular: { - [companyMockObjectMetadataItem.nameSingular]: - companyMockObjectMetadataItem.id, - }, - }; - - const objectMetadataInfo = { - objectMetadataMaps: mockObjectMetadataMaps, - objectMetadataItemWithFieldsMaps: companyMockObjectMetadataItem, - }; - beforeEach(() => { (generateObjectRecordFields as jest.Mock).mockReturnValue(mockFields); }); it('should generate record with "after" prefix for CREATED action', () => { const result = generateFakeObjectRecordEvent( - objectMetadataInfo, + mockCompanyObjectMetadataInfo, DatabaseEventAction.CREATED, ); @@ -82,7 +62,7 @@ describe('generateFakeObjectRecordEvent', () => { it('should generate record with "after" prefix for UPDATED action', () => { const result = generateFakeObjectRecordEvent( - objectMetadataInfo, + mockCompanyObjectMetadataInfo, DatabaseEventAction.UPDATED, ); @@ -113,7 +93,7 @@ describe('generateFakeObjectRecordEvent', () => { it('should generate record with "before" prefix for DELETED action', () => { const result = generateFakeObjectRecordEvent( - objectMetadataInfo, + mockCompanyObjectMetadataInfo, DatabaseEventAction.DELETED, ); @@ -144,7 +124,7 @@ describe('generateFakeObjectRecordEvent', () => { it('should generate record with "before" prefix for DESTROYED action', () => { const result = generateFakeObjectRecordEvent( - objectMetadataInfo, + mockCompanyObjectMetadataInfo, DatabaseEventAction.DESTROYED, ); @@ -175,7 +155,7 @@ describe('generateFakeObjectRecordEvent', () => { it('should generate record with "after" prefix for UPSERTED action', () => { const result = generateFakeObjectRecordEvent( - objectMetadataInfo, + mockCompanyObjectMetadataInfo, DatabaseEventAction.UPSERTED, ); @@ -207,7 +187,7 @@ describe('generateFakeObjectRecordEvent', () => { it('should throw error for unknown action', () => { expect(() => { generateFakeObjectRecordEvent( - objectMetadataInfo, + mockCompanyObjectMetadataInfo, 'UNKNOWN' as DatabaseEventAction, ); }).toThrow("Unknown action 'UNKNOWN'"); diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-object-record.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-object-record.spec.ts index be6955fa161..144546ab46c 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-object-record.spec.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-object-record.spec.ts @@ -1,4 +1,4 @@ -import { mockObjectMetadataItemsWithFieldMaps } from 'src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps'; +import { mockCompanyObjectMetadataInfo } from 'src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps'; import { generateFakeObjectRecord } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record'; import { generateObjectRecordFields } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields'; @@ -12,28 +12,11 @@ jest.mock( }), ); -const companyMockObjectMetadataItem = mockObjectMetadataItemsWithFieldMaps.find( - (item) => item.nameSingular === 'company', -)!; - -const mockObjectMetadataMaps = { - byId: { - [companyMockObjectMetadataItem.id]: companyMockObjectMetadataItem, - }, - idByNameSingular: { - [companyMockObjectMetadataItem.nameSingular]: - companyMockObjectMetadataItem.id, - }, -}; - -const objectMetadataInfo = { - objectMetadataMaps: mockObjectMetadataMaps, - objectMetadataItemWithFieldsMaps: companyMockObjectMetadataItem, -}; - describe('generateFakeObjectRecord', () => { it('should generate a record with correct object metadata', () => { - const result = generateFakeObjectRecord({ objectMetadataInfo }); + const result = generateFakeObjectRecord({ + objectMetadataInfo: mockCompanyObjectMetadataInfo, + }); expect(result).toEqual({ object: { @@ -53,10 +36,12 @@ describe('generateFakeObjectRecord', () => { }); it('should call generateObjectRecordFields with the object metadata', () => { - generateFakeObjectRecord({ objectMetadataInfo }); + generateFakeObjectRecord({ + objectMetadataInfo: mockCompanyObjectMetadataInfo, + }); expect(generateObjectRecordFields).toHaveBeenCalledWith({ - objectMetadataInfo, + objectMetadataInfo: mockCompanyObjectMetadataInfo, depth: 0, maxDepth: 1, }); diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-object-record-fields.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-object-record-fields.spec.ts index 7c4261bfbaa..483badde691 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-object-record-fields.spec.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-object-record-fields.spec.ts @@ -1,6 +1,6 @@ import { FieldMetadataType } from 'twenty-shared/types'; -import { mockObjectMetadataItemsWithFieldMaps } from 'src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps'; +import { mockCompanyObjectMetadataInfo } from 'src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps'; import { generateFakeRecordField } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-record-field'; import { generateObjectRecordFields } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields'; import { shouldGenerateFieldFakeValue } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/should-generate-field-fake-value'; @@ -17,26 +17,6 @@ describe('generateObjectRecordFields', () => { jest.clearAllMocks(); }); - const companyMockObjectMetadataItem = - mockObjectMetadataItemsWithFieldMaps.find( - (item) => item.nameSingular === 'company', - )!; - - const mockObjectMetadataMaps = { - byId: { - [companyMockObjectMetadataItem.id]: companyMockObjectMetadataItem, - }, - idByNameSingular: { - [companyMockObjectMetadataItem.nameSingular]: - companyMockObjectMetadataItem.id, - }, - }; - - const objectMetadataInfo = { - objectMetadataMaps: mockObjectMetadataMaps, - objectMetadataItemWithFieldsMaps: companyMockObjectMetadataItem, - }; - it('should generate fields for valid fields only', () => { (shouldGenerateFieldFakeValue as jest.Mock).mockImplementation( (field) => field.type !== FieldMetadataType.RELATION, @@ -51,7 +31,9 @@ describe('generateObjectRecordFields', () => { }), ); - const result = generateObjectRecordFields({ objectMetadataInfo }); + const result = generateObjectRecordFields({ + objectMetadataInfo: mockCompanyObjectMetadataInfo, + }); expect(result).toEqual({ domainName: { @@ -75,7 +57,9 @@ describe('generateObjectRecordFields', () => { it('should return empty object when no valid fields', () => { (shouldGenerateFieldFakeValue as jest.Mock).mockReturnValue(false); - const result = generateObjectRecordFields({ objectMetadataInfo }); + const result = generateObjectRecordFields({ + objectMetadataInfo: mockCompanyObjectMetadataInfo, + }); expect(result).toEqual({}); expect(shouldGenerateFieldFakeValue).toHaveBeenCalledTimes(2); diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/should-generate-field-fake-value.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/should-generate-field-fake-value.spec.ts index 8d7bf2de09a..e99bed9a9af 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/should-generate-field-fake-value.spec.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/should-generate-field-fake-value.spec.ts @@ -1,99 +1,72 @@ -import { FieldMetadataType } from 'twenty-shared/types'; +import { FieldMetadataType, RelationType } from 'twenty-shared/types'; +import { getFlatFieldMetadataMock } from 'src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock'; import { shouldGenerateFieldFakeValue } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/should-generate-field-fake-value'; -import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock'; describe('shouldGenerateFieldFakeValue', () => { it('should return true for active non-system fields', () => { - const field = getMockFieldMetadataEntity({ - workspaceId: '20202020-0000-0000-0000-000000000000', + const field = getFlatFieldMetadataMock({ objectMetadataId: '20202020-0000-0000-0000-000000000001', - id: '20202020-0000-0000-0000-000000000002', isSystem: false, isActive: true, type: FieldMetadataType.TEXT, name: 'testField', - label: 'Test Field', - isNullable: true, - isLabelSyncedWithName: true, - createdAt: new Date(), - updatedAt: new Date(), + universalIdentifier: 'test-field-universal-id', }); expect(shouldGenerateFieldFakeValue(field)).toBe(true); }); it('should return true for system id field', () => { - const field = getMockFieldMetadataEntity({ - workspaceId: '20202020-0000-0000-0000-000000000000', + const field = getFlatFieldMetadataMock({ objectMetadataId: '20202020-0000-0000-0000-000000000001', - id: '20202020-0000-0000-0000-000000000003', isSystem: true, isActive: true, type: FieldMetadataType.UUID, name: 'id', - label: 'ID', - isNullable: false, - isLabelSyncedWithName: true, - createdAt: new Date(), - updatedAt: new Date(), + universalIdentifier: 'id-field-universal-id', }); expect(shouldGenerateFieldFakeValue(field)).toBe(true); }); it('should return false for inactive fields', () => { - const field = getMockFieldMetadataEntity({ - workspaceId: '20202020-0000-0000-0000-000000000000', + const field = getFlatFieldMetadataMock({ objectMetadataId: '20202020-0000-0000-0000-000000000001', - id: '20202020-0000-0000-0000-000000000004', isSystem: false, isActive: false, type: FieldMetadataType.TEXT, name: 'testField', - label: 'Test Field', - isNullable: true, - isLabelSyncedWithName: true, - createdAt: new Date(), - updatedAt: new Date(), + universalIdentifier: 'inactive-field-universal-id', }); expect(shouldGenerateFieldFakeValue(field)).toBe(false); }); it('should return true for system fields', () => { - const field = getMockFieldMetadataEntity({ - workspaceId: '20202020-0000-0000-0000-000000000000', + const field = getFlatFieldMetadataMock({ objectMetadataId: '20202020-0000-0000-0000-000000000001', - id: '20202020-0000-0000-0000-000000000005', isSystem: true, isActive: true, type: FieldMetadataType.TEXT, name: 'testField', - label: 'Test Field', - isNullable: true, - isLabelSyncedWithName: true, - createdAt: new Date(), - updatedAt: new Date(), + universalIdentifier: 'system-field-universal-id', }); expect(shouldGenerateFieldFakeValue(field)).toBe(true); }); - it('should return false for relation fields', () => { - const field = getMockFieldMetadataEntity({ - workspaceId: '20202020-0000-0000-0000-000000000000', + it('should return false for many-to-one relation fields', () => { + const field = getFlatFieldMetadataMock({ objectMetadataId: '20202020-0000-0000-0000-000000000001', - id: '20202020-0000-0000-0000-000000000006', isSystem: false, isActive: true, type: FieldMetadataType.RELATION, name: 'testField', - label: 'Test Field', - isNullable: true, - isLabelSyncedWithName: true, - createdAt: new Date(), - updatedAt: new Date(), + universalIdentifier: 'relation-field-universal-id', + settings: { + relationType: RelationType.MANY_TO_ONE, + }, }); expect(shouldGenerateFieldFakeValue(field)).toBe(false); diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-form-response.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-form-response.ts index c5657ebd9cf..b7c11d0063a 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-form-response.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-form-response.ts @@ -1,7 +1,8 @@ import { isDefined } from 'twenty-shared/utils'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; -import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { type Leaf, type Node, @@ -12,12 +13,16 @@ import { type FormFieldMetadata } from 'src/modules/workflow/workflow-executor/w type GenerateFakeFormResponseArgs = { formFieldMetadataItems: FormFieldMetadata[]; - objectMetadataMaps: ObjectMetadataMaps; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; + objectIdByNameSingular: Record; }; export const generateFakeFormResponse = ({ formFieldMetadataItems, - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, }: GenerateFakeFormResponseArgs): Record => { const result = formFieldMetadataItems.map((formFieldMetadata) => { if (formFieldMetadata.type === 'RECORD') { @@ -25,13 +30,18 @@ export const generateFakeFormResponse = ({ return undefined; } - const objectMetadataItemWithFieldsMaps = - getObjectMetadataMapItemByNameSingular( - objectMetadataMaps, - formFieldMetadata?.settings?.objectName, - ); + const objectId = + objectIdByNameSingular[formFieldMetadata?.settings?.objectName]; - if (!isDefined(objectMetadataItemWithFieldsMaps)) { + if (!isDefined(objectId)) { + throw new Error( + `Object metadata not found for object name ${formFieldMetadata?.settings?.objectName}`, + ); + } + + const flatObjectMetadata = flatObjectMetadataMaps.byId[objectId]; + + if (!isDefined(flatObjectMetadata)) { throw new Error( `Object metadata not found for object name ${formFieldMetadata?.settings?.objectName}`, ); @@ -43,8 +53,9 @@ export const generateFakeFormResponse = ({ label: formFieldMetadata.label, value: generateFakeObjectRecord({ objectMetadataInfo: { - objectMetadataItemWithFieldsMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }, }), }, diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record-event.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record-event.ts index 8b59e3bf46b..e7684f41330 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record-event.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record-event.ts @@ -13,6 +13,7 @@ const generateFakeObjectRecordEventWithPrefix = ({ objectMetadataInfo: ObjectMetadataInfo; prefix: string; }): RecordOutputSchema => { + const { flatObjectMetadata } = objectMetadataInfo; const recordFields = generateObjectRecordFields({ objectMetadataInfo }); const prefixedRecordFields = Object.entries(recordFields).reduce( (acc, [key, value]) => { @@ -26,12 +27,11 @@ const generateFakeObjectRecordEventWithPrefix = ({ return { object: { isLeaf: true, - icon: - objectMetadataInfo.objectMetadataItemWithFieldsMaps.icon ?? undefined, - label: objectMetadataInfo.objectMetadataItemWithFieldsMaps.labelSingular, - value: objectMetadataInfo.objectMetadataItemWithFieldsMaps.description, + icon: flatObjectMetadata.icon ?? undefined, + label: flatObjectMetadata.labelSingular, + value: flatObjectMetadata.description, fieldIdName: `${prefix}.id`, - objectMetadataId: objectMetadataInfo.objectMetadataItemWithFieldsMaps.id, + objectMetadataId: flatObjectMetadata.id, }, fields: prefixedRecordFields, _outputSchemaType: 'RECORD', diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record.ts index 9ae8b623339..0fe4757e5f3 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record.ts @@ -11,15 +11,16 @@ export const generateFakeObjectRecord = ({ depth?: number; maxDepth?: number; }): RecordOutputSchema => { + const { flatObjectMetadata } = objectMetadataInfo; + return { object: { isLeaf: true, - icon: - objectMetadataInfo.objectMetadataItemWithFieldsMaps.icon ?? undefined, - label: objectMetadataInfo.objectMetadataItemWithFieldsMaps.labelSingular, - value: objectMetadataInfo.objectMetadataItemWithFieldsMaps.description, + icon: flatObjectMetadata.icon ?? undefined, + label: flatObjectMetadata.labelSingular, + value: flatObjectMetadata.description, fieldIdName: 'id', - objectMetadataId: objectMetadataInfo.objectMetadataItemWithFieldsMaps.id, + objectMetadataId: flatObjectMetadata.id, }, fields: generateObjectRecordFields({ objectMetadataInfo, diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields.ts index cfea6703624..44ac25b7267 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields.ts @@ -1,6 +1,7 @@ import { FieldMetadataType } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; +import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; import { type ObjectMetadataInfo } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; import { type FieldOutputSchema } from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type'; import { generateFakeObjectRecord } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record'; @@ -17,67 +18,71 @@ export const generateObjectRecordFields = ({ depth?: number; maxDepth?: number; }): Record => { - const objectMetadata = objectMetadataInfo.objectMetadataItemWithFieldsMaps; + const { flatObjectMetadata, flatObjectMetadataMaps, flatFieldMetadataMaps } = + objectMetadataInfo; - return Object.values(objectMetadata.fieldsById).reduce( - (acc: Record, field) => { - if (!shouldGenerateFieldFakeValue(field)) { - return acc; + const result: Record = {}; + + for (const fieldId of flatObjectMetadata.fieldMetadataIds) { + const field = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityMaps: flatFieldMetadataMaps, + flatEntityId: fieldId, + }); + + if (!shouldGenerateFieldFakeValue(field)) { + continue; + } + + if (field.type !== FieldMetadataType.RELATION) { + result[field.name] = generateFakeRecordField({ + type: field.type, + label: field.label, + icon: field.icon ?? undefined, + fieldMetadataId: field.id, + }); + + continue; + } + + if (!isDefined(field.relationTargetObjectMetadataId)) { + continue; + } + + if (depth < maxDepth) { + const relationTargetObjectMetadata = + flatObjectMetadataMaps.byId[field.relationTargetObjectMetadataId]; + + if (!isDefined(relationTargetObjectMetadata)) { + continue; } - if (field.type !== FieldMetadataType.RELATION) { - acc[field.name] = generateFakeRecordField({ - type: field.type, - label: field.label, - icon: field.icon ?? undefined, - fieldMetadataId: field.id, - }); + result[field.name] = { + isLeaf: false, + icon: field.icon ?? undefined, + label: field.label, + type: field.type, + fieldMetadataId: field.id, + value: generateFakeObjectRecord({ + objectMetadataInfo: { + flatObjectMetadata: relationTargetObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + }, + depth: depth + 1, + }), + }; + } else if (depth === maxDepth) { + const relationIdFieldName = `${field.name}Id`; + const relationIdFieldLabel = camelToTitleCase(relationIdFieldName); - return acc; - } + result[relationIdFieldName] = generateFakeRecordField({ + type: FieldMetadataType.UUID, + label: relationIdFieldLabel, + icon: field.icon ?? undefined, + fieldMetadataId: field.id, + }); + } + } - if (!isDefined(field.relationTargetObjectMetadataId)) { - return acc; - } - - if (depth < maxDepth) { - const relationTargetObjectMetadata = - objectMetadataInfo.objectMetadataMaps.byId[ - field.relationTargetObjectMetadataId - ]; - - if (!isDefined(relationTargetObjectMetadata)) { - return acc; - } - - acc[field.name] = { - isLeaf: false, - icon: field.icon ?? undefined, - label: field.label, - type: field.type, - fieldMetadataId: field.id, - value: generateFakeObjectRecord({ - objectMetadataInfo: { - objectMetadataItemWithFieldsMaps: relationTargetObjectMetadata, - objectMetadataMaps: objectMetadataInfo.objectMetadataMaps, - }, - depth: depth + 1, - }), - }; - } else if (depth === maxDepth) { - const relationIdFieldName = `${field.name}Id`; - const relationIdFieldLabel = camelToTitleCase(relationIdFieldName); - - acc[relationIdFieldName] = generateFakeRecordField({ - type: FieldMetadataType.UUID, - label: relationIdFieldLabel, - icon: field.icon ?? undefined, - fieldMetadataId: field.id, - }); - } - - return acc; - }, - {} as Record, - ); + return result; }; diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/should-generate-field-fake-value.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/should-generate-field-fake-value.ts index e297f093b68..c589cef30fb 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/should-generate-field-fake-value.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/should-generate-field-fake-value.ts @@ -1,21 +1,22 @@ -import { FieldMetadataType } from 'twenty-shared/types'; +import { FieldMetadataType, RelationType } from 'twenty-shared/types'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util'; -const isManyToOneRelationField = (field: FieldMetadataEntity) => - (field as FieldMetadataEntity).settings - ?.relationType === 'MANY_TO_ONE'; +const isManyToOneRelationField = (field: FlatFieldMetadata) => { + if (isFlatFieldMetadataOfType(field, FieldMetadataType.RELATION)) { + return field.settings?.relationType === RelationType.MANY_TO_ONE; + } + + return false; +}; const EXCLUDED_SYSTEM_FIELDS = ['searchVector', 'position']; -// TODO refactor -export const shouldGenerateFieldFakeValue = ( - field: FieldMetadataEntity, -) => { +export const shouldGenerateFieldFakeValue = (field: FlatFieldMetadata) => { return ( field.isActive && !(EXCLUDED_SYSTEM_FIELDS.includes(field.name) && field.isSystem) && - (field.type !== FieldMetadataType.RELATION || - isManyToOneRelationField(field as unknown as FieldMetadataEntity)) + !isManyToOneRelationField(field) ); }; diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service.ts index 9d8eb939310..03e9d9f9648 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service.ts @@ -216,7 +216,7 @@ export class WorkflowSchemaWorkspaceService { } const objectMetadataInfo = - await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps( + await this.workflowCommonWorkspaceService.getObjectMetadataInfo( nameSingular, workspaceId, ); @@ -241,14 +241,14 @@ export class WorkflowSchemaWorkspaceService { }); const objectMetadataInfo = - await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps( + await this.workflowCommonWorkspaceService.getObjectMetadataInfo( objectType, workspaceId, ); const first: Node = { isLeaf: false, - label: `First ${objectMetadataInfo.objectMetadataItemWithFieldsMaps.labelSingular ?? 'Record'}`, + label: `First ${objectMetadataInfo.flatObjectMetadata.labelSingular ?? 'Record'}`, icon: 'IconAlpha', type: 'object', value: recordOutputSchema, @@ -256,7 +256,7 @@ export class WorkflowSchemaWorkspaceService { const all: Leaf = { isLeaf: true, - label: `All ${objectMetadataInfo.objectMetadataItemWithFieldsMaps.labelPlural ?? 'Records'}`, + label: `All ${objectMetadataInfo.flatObjectMetadata.labelPlural ?? 'Records'}`, type: 'array', icon: 'IconListDetails', value: 'Returns an array of records', @@ -283,7 +283,7 @@ export class WorkflowSchemaWorkspaceService { maxDepth?: number; }): Promise { const objectMetadataInfo = - await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps( + await this.workflowCommonWorkspaceService.getObjectMetadataInfo( objectType, workspaceId, ); @@ -302,14 +302,18 @@ export class WorkflowSchemaWorkspaceService { formFieldMetadataItems: FormFieldMetadata[]; workspaceId: string; }): Promise { - const objectMetadataMaps = - await this.workflowCommonWorkspaceService.getObjectMetadataMaps( - workspaceId, - ); + const { + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, + } = + await this.workflowCommonWorkspaceService.getFlatEntityMaps(workspaceId); return generateFakeFormResponse({ formFieldMetadataItems, - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, }); } @@ -337,20 +341,18 @@ export class WorkflowSchemaWorkspaceService { if (availability.type === 'BULK_RECORDS') { const objectMetadataInfo = - await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps( + await this.workflowCommonWorkspaceService.getObjectMetadataInfo( availability.objectNameSingular, workspaceId, ); return { - [objectMetadataInfo.objectMetadataItemWithFieldsMaps.namePlural]: { - label: - objectMetadataInfo.objectMetadataItemWithFieldsMaps.labelPlural, + [objectMetadataInfo.flatObjectMetadata.namePlural]: { + label: objectMetadataInfo.flatObjectMetadata.labelPlural, isLeaf: true, type: 'array', value: - 'Array of ' + - objectMetadataInfo.objectMetadataItemWithFieldsMaps.labelPlural, + 'Array of ' + objectMetadataInfo.flatObjectMetadata.labelPlural, }, }; } @@ -417,7 +419,7 @@ export class WorkflowSchemaWorkspaceService { case WorkflowTriggerType.MANUAL: { if (trigger.settings.availability?.type === 'BULK_RECORDS') { const objectMetadataInfo = - await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps( + await this.workflowCommonWorkspaceService.getObjectMetadataInfo( trigger.settings.availability.objectNameSingular, workspaceId, ); @@ -425,8 +427,7 @@ export class WorkflowSchemaWorkspaceService { return { label: 'Current Item (' + - objectMetadataInfo.objectMetadataItemWithFieldsMaps - .labelSingular + + objectMetadataInfo.flatObjectMetadata.labelSingular + ')', isLeaf: false, type: 'object', @@ -468,7 +469,7 @@ export class WorkflowSchemaWorkspaceService { switch (step.type) { case WorkflowActionType.FIND_RECORDS: { const objectMetadataInfo = - await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps( + await this.workflowCommonWorkspaceService.getObjectMetadataInfo( step.settings.input.objectName, workspaceId, ); @@ -476,7 +477,7 @@ export class WorkflowSchemaWorkspaceService { return { label: 'Current Item (' + - objectMetadataInfo.objectMetadataItemWithFieldsMaps.labelSingular + + objectMetadataInfo.flatObjectMetadata.labelSingular + ')', isLeaf: false, type: 'object', diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-operations.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-operations.workspace-service.ts index de49125db6b..9c459fd52b0 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-operations.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-operations.workspace-service.ts @@ -6,6 +6,7 @@ import { isDefined, isValidUuid } from 'twenty-shared/utils'; import { Repository } from 'typeorm'; import { v4 } from 'uuid'; +import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util'; import { BASE_TYPESCRIPT_PROJECT_INPUT_SCHEMA } from 'src/engine/core-modules/serverless/drivers/constants/base-typescript-project-input-schema'; import { type WorkflowStepPositionInput } from 'src/engine/core-modules/workflow/dtos/update-workflow-step-position-input.dto'; import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity'; @@ -26,7 +27,6 @@ import { type WorkflowEmptyAction, type WorkflowFormAction, } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type'; - const BASE_STEP_DEFINITION: BaseWorkflowActionSettings = { outputSchema: {}, errorHandlingOptions: { @@ -460,14 +460,15 @@ export class WorkflowVersionStepOperationsWorkspaceService { // @ts-expect-error legacy noImplicitAny isValidUuid(response[key].id) ) { - const objectMetadataInfo = - await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps( + const { flatObjectMetadata, flatFieldMetadataMaps } = + await this.workflowCommonWorkspaceService.getObjectMetadataInfo( field.settings.objectName, workspaceId, ); - const relationFieldsNames = Object.values( - objectMetadataInfo.objectMetadataItemWithFieldsMaps.fieldsById, + const relationFieldsNames = getFlatFieldsFromFlatObjectMetadata( + flatObjectMetadata, + flatFieldMetadataMaps, ) .filter((field) => field.type === FieldMetadataType.RELATION) .map((field) => field.name); diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/listeners/__tests__/workflow-database-event-trigger.listener.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/listeners/__tests__/workflow-database-event-trigger.listener.spec.ts index 03084f9dc3a..2012a17ae96 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/listeners/__tests__/workflow-database-event-trigger.listener.spec.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/listeners/__tests__/workflow-database-event-trigger.listener.spec.ts @@ -1,13 +1,13 @@ import { Test, type TestingModule } from '@nestjs/testing'; import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { type WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type'; import { AutomatedTriggerType } from 'src/modules/workflow/common/standard-objects/workflow-automated-trigger.workspace-entity'; import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; import { WorkflowDatabaseEventTriggerListener } from 'src/modules/workflow/workflow-trigger/automated-trigger/listeners/workflow-database-event-trigger.listener'; import { WorkflowTriggerJob } from 'src/modules/workflow/workflow-trigger/jobs/workflow-trigger.job'; -import { getMockObjectMetadataEntity } from 'src/utils/__test__/get-object-metadata-entity.mock'; -import { getMockObjectMetadataItemWithFieldsMaps } from 'src/utils/__test__/get-object-metadata-item-with-fields-maps.mock'; describe('WorkflowDatabaseEventTriggerListener', () => { let listener: WorkflowDatabaseEventTriggerListener; @@ -18,6 +18,35 @@ describe('WorkflowDatabaseEventTriggerListener', () => { find: jest.fn(), }; + const createMockFlatObjectMetadata = ( + overrides: Partial, + ): FlatObjectMetadata => + ({ + id: 'test-object-metadata', + workspaceId: 'test-workspace', + nameSingular: 'testObject', + namePlural: 'testObjects', + labelSingular: 'Test Object', + labelPlural: 'Test Objects', + description: 'Test object for testing', + targetTableName: 'test_objects', + isSystem: false, + isCustom: false, + isActive: true, + isRemote: false, + isAuditLogged: true, + isSearchable: true, + createdAt: new Date(), + updatedAt: new Date(), + icon: 'Icon123', + universalIdentifier: 'test-object-metadata', + fieldMetadataIds: [], + indexMetadataIds: [], + viewIds: [], + applicationId: null, + ...overrides, + }) as FlatObjectMetadata; + beforeEach(async () => { twentyORMGlobalManager = { getRepositoryForWorkspace: jest.fn().mockResolvedValue(mockRepository), @@ -46,37 +75,10 @@ describe('WorkflowDatabaseEventTriggerListener', () => { provide: WorkflowCommonWorkspaceService, useValue: { getWorkflowById: jest.fn(), - getObjectMetadataItemWithFieldsMaps: jest.fn().mockResolvedValue({ - objectMetadataMaps: { - byId: { - 'test-object-metadata': { - nameSingular: 'testObject', - namePlural: 'testObjects', - }, - }, - }, - objectMetadataItemWithFieldsMaps: - getMockObjectMetadataItemWithFieldsMaps({ - id: 'test-object-metadata', - workspaceId: 'test-workspace', - nameSingular: 'testObject', - namePlural: 'testObjects', - labelSingular: 'Test Object', - labelPlural: 'Test Objects', - description: 'Test object for testing', - indexMetadatas: [], - targetTableName: 'test_objects', - isSystem: false, - isCustom: false, - isActive: true, - isRemote: false, - isAuditLogged: true, - isSearchable: true, - icon: 'Icon123', - fieldIdByJoinColumnName: {}, - fieldsById: {}, - fieldIdByName: {}, - }), + getObjectMetadataInfo: jest.fn().mockResolvedValue({ + flatObjectMetadata: createMockFlatObjectMetadata({}), + flatObjectMetadataMaps: { byId: {}, byName: {} }, + flatFieldMetadataMaps: { byId: {}, byName: {} }, }), }, }, @@ -93,30 +95,10 @@ describe('WorkflowDatabaseEventTriggerListener', () => { const databaseEventName = 'testEvent'; const workflowId = 'test-workflow'; - const mockPayload = { + const mockPayload: WorkspaceEventBatch = { workspaceId, name: databaseEventName, - objectMetadata: getMockObjectMetadataEntity({ - id: 'test-object-metadata', - workspaceId, - nameSingular: 'testObject', - namePlural: 'testObjects', - labelSingular: 'Test Object', - labelPlural: 'Test Objects', - description: 'Test object for testing', - targetTableName: 'test_objects', - isSystem: false, - isCustom: false, - isActive: true, - isRemote: false, - isAuditLogged: true, - isSearchable: true, - createdAt: new Date(), - updatedAt: new Date(), - fields: [], - indexMetadatas: [], - icon: 'Icon123', - }), + objectMetadata: createMockFlatObjectMetadata({}), events: [ { recordId: 'test-record', @@ -205,7 +187,7 @@ describe('WorkflowDatabaseEventTriggerListener', () => { }); it('should handle create events correctly', async () => { - const createPayload = { + const createPayload: WorkspaceEventBatch = { ...mockPayload, name: 'createEvent', events: [ @@ -242,7 +224,7 @@ describe('WorkflowDatabaseEventTriggerListener', () => { }); it('should handle delete events correctly', async () => { - const deletePayload = { + const deletePayload: WorkspaceEventBatch = { ...mockPayload, name: 'deleteEvent', events: [ @@ -279,7 +261,7 @@ describe('WorkflowDatabaseEventTriggerListener', () => { }); it('should handle destroy events correctly', async () => { - const destroyPayload = { + const destroyPayload: WorkspaceEventBatch = { ...mockPayload, name: 'destroyEvent', events: [ @@ -316,7 +298,7 @@ describe('WorkflowDatabaseEventTriggerListener', () => { }); it('should handle multiple events in a batch', async () => { - const batchPayload = { + const batchPayload: WorkspaceEventBatch = { ...mockPayload, events: [ mockPayload.events[0], diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/listeners/workflow-database-event-trigger.listener.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/listeners/workflow-database-event-trigger.listener.ts index ce16475a135..92ebc4b9129 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/listeners/workflow-database-event-trigger.listener.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/listeners/workflow-database-event-trigger.listener.ts @@ -15,8 +15,11 @@ import { type ObjectRecordUpsertEvent } from 'src/engine/core-modules/event-emit import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; -import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type'; import { @@ -135,17 +138,21 @@ export class WorkflowDatabaseEventTriggerListener { payload: WorkspaceEventBatch, ) { const workspaceId = payload.workspaceId; - const { objectMetadataMaps, objectMetadataItemWithFieldsMaps } = - await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps( - payload.objectMetadata.nameSingular, - workspaceId, - ); + const { + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + } = await this.workflowCommonWorkspaceService.getObjectMetadataInfo( + payload.objectMetadata.nameSingular, + workspaceId, + ); await this.enrichRecordsWithRelations({ records: payload.events.map((event) => event.properties.after), workspaceId, - objectMetadataMaps, - objectMetadataItemWithFieldsMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); } @@ -153,23 +160,28 @@ export class WorkflowDatabaseEventTriggerListener { payload: WorkspaceEventBatch, ) { const workspaceId = payload.workspaceId; - const { objectMetadataMaps, objectMetadataItemWithFieldsMaps } = - await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps( - payload.objectMetadata.nameSingular, - workspaceId, - ); + const { + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + } = await this.workflowCommonWorkspaceService.getObjectMetadataInfo( + payload.objectMetadata.nameSingular, + workspaceId, + ); await this.enrichRecordsWithRelations({ records: payload.events.map((event) => event.properties.before), workspaceId, - objectMetadataMaps, - objectMetadataItemWithFieldsMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); await this.enrichRecordsWithRelations({ records: payload.events.map((event) => event.properties.after), workspaceId, - objectMetadataMaps, - objectMetadataItemWithFieldsMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); } @@ -177,17 +189,21 @@ export class WorkflowDatabaseEventTriggerListener { payload: WorkspaceEventBatch, ) { const workspaceId = payload.workspaceId; - const { objectMetadataMaps, objectMetadataItemWithFieldsMaps } = - await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps( - payload.objectMetadata.nameSingular, - workspaceId, - ); + const { + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + } = await this.workflowCommonWorkspaceService.getObjectMetadataInfo( + payload.objectMetadata.nameSingular, + workspaceId, + ); await this.enrichRecordsWithRelations({ records: payload.events.map((event) => event.properties.before), workspaceId, - objectMetadataMaps, - objectMetadataItemWithFieldsMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); } @@ -195,36 +211,50 @@ export class WorkflowDatabaseEventTriggerListener { payload: WorkspaceEventBatch, ) { const workspaceId = payload.workspaceId; - const { objectMetadataMaps, objectMetadataItemWithFieldsMaps } = - await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps( - payload.objectMetadata.nameSingular, - workspaceId, - ); + const { + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + } = await this.workflowCommonWorkspaceService.getObjectMetadataInfo( + payload.objectMetadata.nameSingular, + workspaceId, + ); await this.enrichRecordsWithRelations({ records: payload.events.map((event) => event.properties.before), - objectMetadataMaps, workspaceId, - objectMetadataItemWithFieldsMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); } private async enrichRecordsWithRelations({ records, workspaceId, - objectMetadataMaps, - objectMetadataItemWithFieldsMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }: { records: Partial[]; workspaceId: string; - objectMetadataMaps: ObjectMetadataMaps; - objectMetadataItemWithFieldsMaps: ObjectMetadataItemWithFieldMaps; + flatObjectMetadata: FlatObjectMetadata; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; }) { + const { fieldIdByJoinColumnName } = buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + flatObjectMetadata, + ); + for (const [joinColumnName, joinFieldId] of Object.entries( - objectMetadataItemWithFieldsMaps.fieldIdByJoinColumnName, + fieldIdByJoinColumnName, )) { - const joinField = - objectMetadataItemWithFieldsMaps.fieldsById[joinFieldId]; + const joinField = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityMaps: flatFieldMetadataMaps, + flatEntityId: joinFieldId, + }); + const joinRecordIds = records .map((record) => record[joinColumnName]) .filter(isDefined); @@ -240,7 +270,7 @@ export class WorkflowDatabaseEventTriggerListener { } const relatedObjectMetadataNameSingular = - objectMetadataMaps.byId[relatedObjectMetadataId]?.nameSingular; + flatObjectMetadataMaps.byId[relatedObjectMetadataId]?.nameSingular; if (!isDefined(relatedObjectMetadataNameSingular)) { continue; diff --git a/packages/twenty-server/src/utils/__test__/get-object-metadata-item-with-fields-maps.mock.ts b/packages/twenty-server/src/utils/__test__/get-object-metadata-item-with-fields-maps.mock.ts deleted file mode 100644 index ab310cc46da..00000000000 --- a/packages/twenty-server/src/utils/__test__/get-object-metadata-item-with-fields-maps.mock.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { - type GetMockObjectMetadataEntityOverride, - getMockObjectMetadataEntity, -} from 'src/utils/__test__/get-object-metadata-entity.mock'; - -type GetMockObjectMetadataItemWithFielsMapsOverride = - GetMockObjectMetadataEntityOverride & - Required< - Pick< - ObjectMetadataItemWithFieldMaps, - | 'fieldsById' - | 'fieldIdByJoinColumnName' - | 'fieldIdByName' - | 'indexMetadatas' - > - >; - -export const getMockObjectMetadataItemWithFieldsMaps = ({ - fieldsById, - fieldIdByJoinColumnName, - fieldIdByName, - indexMetadatas, - ...objectMetadataOverrides -}: GetMockObjectMetadataItemWithFielsMapsOverride): ObjectMetadataItemWithFieldMaps => { - const { fields: _fields, ...objectMetadata } = getMockObjectMetadataEntity( - objectMetadataOverrides, - ); - - return { - ...objectMetadata, - fieldsById, - fieldIdByJoinColumnName, - fieldIdByName, - indexMetadatas, - }; -}; diff --git a/packages/twenty-server/test/integration/graphql/suites/group-by-resolver.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/group-by-resolver.integration-spec.ts index 691b54b08be..0fdf5e46299 100644 --- a/packages/twenty-server/test/integration/graphql/suites/group-by-resolver.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/group-by-resolver.integration-spec.ts @@ -320,20 +320,19 @@ describe('group-by resolver (integration)', () => { expect(groups).toBeDefined(); expect(Array.isArray(groups)).toBe(true); + expect(groups.length).toBe(2); - // Expect two groups: January 2025 and March 2025 - const janGroup = groups.find((g: any) => - g.groupByDimensionValues?.[0]?.startsWith?.('2025-01-01'), - ); - const marGroup = groups.find((g: any) => - g.groupByDimensionValues?.[0]?.startsWith?.('2025-03-01'), - ); + // Expect two groups: one with 2 records (January) and one with 1 record (March) + // Note: DATE_TRUNC returns dates in server timezone, which when converted to UTC + // may show as the previous day at 23:00 (e.g., 2024-12-31T23:00 for Jan 1 local) + const groupWith2Records = groups.find((g: any) => g.totalCount === 2); + const groupWith1Record = groups.find((g: any) => g.totalCount === 1); - expect(janGroup).toBeDefined(); - expect(marGroup).toBeDefined(); + expect(groupWith2Records).toBeDefined(); + expect(groupWith1Record).toBeDefined(); - expect(janGroup.totalCount).toBe(2); - expect(marGroup.totalCount).toBe(1); + expect(groupWith2Records.totalCount).toBe(2); + expect(groupWith1Record.totalCount).toBe(1); }); describe('group by week', () => { diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/nested-relation-queries.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/nested-relation-queries.integration-spec.ts index b94d10c06f5..fe2ff14f8c5 100644 --- a/packages/twenty-server/test/integration/graphql/suites/object-generated/nested-relation-queries.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/nested-relation-queries.integration-spec.ts @@ -602,7 +602,21 @@ describe('relation connect in workspace createOne/createMany resolvers (e2e)', it('should connect a morph relation ownerSurveyResult on pet via the connect feature', async () => { const PET_OBJECT_NAME = 'pet'; + const SURVEY_RESULT_OBJECT_NAME = 'surveyResult'; const TEST_PET_ID = TEST_PET_ID_1; + const TEST_SURVEY_RESULT_ID = TEST_SURVEY_RESULT_1_ID; + + // Create the survey result record first + await makeGraphqlAPIRequest( + createOneOperationFactory({ + objectMetadataSingularName: SURVEY_RESULT_OBJECT_NAME, + gqlFields: 'id', + data: { + id: TEST_SURVEY_RESULT_ID, + name: 'Test Survey Result', + }, + }), + ); await makeGraphqlAPIRequest( createOneOperationFactory({ @@ -615,8 +629,6 @@ describe('relation connect in workspace createOne/createMany resolvers (e2e)', }), ); - const TEST_SURVEY_RESULT_ID = TEST_SURVEY_RESULT_1_ID; - const updatePetOwnerSurveyResultOp = updateOneOperationFactory({ objectMetadataSingularName: PET_OBJECT_NAME, recordId: TEST_PET_ID, @@ -630,7 +642,7 @@ describe('relation connect in workspace createOne/createMany resolvers (e2e)', }, }); - let response = await makeGraphqlAPIRequest(updatePetOwnerSurveyResultOp); + const response = await makeGraphqlAPIRequest(updatePetOwnerSurveyResultOp); expect(response.body.data.updatePet).toBeDefined(); expect(response.body.data.updatePet.ownerSurveyResult).toBeDefined(); @@ -642,7 +654,21 @@ describe('relation connect in workspace createOne/createMany resolvers (e2e)', it('should disconnect a morph relation successfully', async () => { const PET_OBJECT_NAME = 'pet'; + const SURVEY_RESULT_OBJECT_NAME = 'surveyResult'; const TEST_PET_ID = TEST_PET_ID_2; + const TEST_SURVEY_RESULT_ID = TEST_SURVEY_RESULT_1_ID; + + // Create the survey result record first (if not already created by previous test) + await makeGraphqlAPIRequest( + createOneOperationFactory({ + objectMetadataSingularName: SURVEY_RESULT_OBJECT_NAME, + gqlFields: 'id', + data: { + id: TEST_SURVEY_RESULT_ID, + name: 'Test Survey Result', + }, + }), + ); await makeGraphqlAPIRequest( createOneOperationFactory({ @@ -655,8 +681,6 @@ describe('relation connect in workspace createOne/createMany resolvers (e2e)', }), ); - const TEST_SURVEY_RESULT_ID = TEST_SURVEY_RESULT_1_ID; - const updatePetOwnerSurveyResultOp = updateOneOperationFactory({ objectMetadataSingularName: PET_OBJECT_NAME, recordId: TEST_PET_ID, diff --git a/packages/twenty-server/test/integration/graphql/suites/object-records-permissions/fields-permissions/update-permissions.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-records-permissions/fields-permissions/update-permissions.integration-spec.ts index 65828a1a507..917d3af2239 100644 --- a/packages/twenty-server/test/integration/graphql/suites/object-records-permissions/fields-permissions/update-permissions.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-records-permissions/fields-permissions/update-permissions.integration-spec.ts @@ -386,7 +386,7 @@ describe('Field update permissions restrictions', () => { objectMetadataPluralName: 'companies', gqlFields: COMPANY_GQL_FIELDS_WITHOUT_EMPLOYEES, data: { name: 'UpdatedCompany' }, - filter: { name: { ilike: '%aa%' } }, + filter: { id: { eq: companyId } }, }); const response = diff --git a/packages/twenty-server/test/integration/graphql/suites/workspace/successful-user-and-workspace-creation.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/workspace/successful-user-and-workspace-creation.integration-spec.ts index d298b6e8caa..8d61662c1a9 100644 --- a/packages/twenty-server/test/integration/graphql/suites/workspace/successful-user-and-workspace-creation.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/workspace/successful-user-and-workspace-creation.integration-spec.ts @@ -1,3 +1,5 @@ +import { randomUUID } from 'crypto'; + import { activateWorkspace } from 'test/integration/graphql/utils/activate-workspace.util'; import { deleteUser } from 'test/integration/graphql/utils/delete-user.util'; import { findManyApplications } from 'test/integration/graphql/utils/find-many-applications.util'; @@ -27,9 +29,11 @@ describe('Successful user and workspace creation', () => { }); it('should sign up a new user and create a new workspace successfully', async () => { + const uniqueEmail = `test-${randomUUID()}@example.com`; + const { data } = await signUp({ input: { - email: `test-1234@example.com`, + email: uniqueEmail, password: 'Test123!@#', }, @@ -39,6 +43,12 @@ describe('Successful user and workspace creation', () => { createdUserAccessToken = data.signUp.tokens.accessOrWorkspaceAgnosticToken.token; + // Mark email as verified to bypass email verification requirement + await testDataSource.query( + 'UPDATE core."user" SET "isEmailVerified" = true WHERE email = $1', + [uniqueEmail], + ); + const { data: { signUpInNewWorkspace: signUpInNewWorkspaceData }, } = await signUpInNewWorkspace({ @@ -121,9 +131,11 @@ describe('Successful user and workspace creation', () => { }); it('should delete workspace and related metadata entities when last user is deleted', async () => { + const uniqueEmail = `test-delete-${randomUUID()}@example.com`; + const { data } = await signUp({ input: { - email: `test-delete-5678@example.com`, + email: uniqueEmail, password: 'Test123!@#', }, expectToFail: false, @@ -132,6 +144,12 @@ describe('Successful user and workspace creation', () => { createdUserAccessToken = data.signUp.tokens.accessOrWorkspaceAgnosticToken.token; + // Mark email as verified to bypass email verification requirement + await testDataSource.query( + 'UPDATE core."user" SET "isEmailVerified" = true WHERE email = $1', + [uniqueEmail], + ); + const { data: { signUpInNewWorkspace: signUpInNewWorkspaceData }, } = await signUpInNewWorkspace({ diff --git a/packages/twenty-server/test/integration/metadata/suites/agent/agent-tool.service.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/agent/agent-tool.service.integration-spec.ts index 305cb4cfe5f..608d742e70a 100644 --- a/packages/twenty-server/test/integration/metadata/suites/agent/agent-tool.service.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/agent/agent-tool.service.integration-spec.ts @@ -1,3 +1,5 @@ +import { fromObjectMetadataEntityToFlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-object-metadata-entity-to-flat-object-metadata.util'; + import { type AgentToolTestContext, createAgentToolTestModule, @@ -45,7 +47,11 @@ describe('AgentToolGeneratorService Integration', () => { }); jest .spyOn(context.objectMetadataService, 'findManyWithinWorkspace') - .mockResolvedValue([context.testObjectMetadata]); + .mockResolvedValue([ + fromObjectMetadataEntityToFlatObjectMetadata( + context.testObjectMetadata, + ), + ]); const tools = await context.agentToolService.generateToolsForAgent( context.testAgentId, @@ -89,7 +95,11 @@ describe('AgentToolGeneratorService Integration', () => { }); jest .spyOn(context.objectMetadataService, 'findManyWithinWorkspace') - .mockResolvedValue([context.testObjectMetadata]); + .mockResolvedValue([ + fromObjectMetadataEntityToFlatObjectMetadata( + context.testObjectMetadata, + ), + ]); const tools = await context.agentToolService.generateToolsForAgent( context.testAgentId, @@ -146,7 +156,9 @@ describe('AgentToolGeneratorService Integration', () => { }); jest .spyOn(context.objectMetadataService, 'findManyWithinWorkspace') - .mockResolvedValue([workflowObject]); + .mockResolvedValue([ + fromObjectMetadataEntityToFlatObjectMetadata(workflowObject), + ]); const tools = await context.agentToolService.generateToolsForAgent( context.testAgentId, diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/create-delete-and-create-object-metadata-v2.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/object-metadata/create-delete-and-create-object-metadata-v2.integration-spec.ts index a7059640db5..ec297ecaebd 100644 --- a/packages/twenty-server/test/integration/metadata/suites/object-metadata/create-delete-and-create-object-metadata-v2.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/create-delete-and-create-object-metadata-v2.integration-spec.ts @@ -2,7 +2,6 @@ import { type CreateOneFieldFactoryInput } from 'test/integration/metadata/suite import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util'; import { deleteOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/delete-one-field-metadata.util'; import { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata.util'; -import { CUSTOM_OBJECT_DISHES } from 'test/integration/metadata/suites/object-metadata/constants/custom-object-dishes.constants'; import { type CreateOneObjectFactoryInput } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata-query-factory.util'; import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; @@ -12,6 +11,7 @@ import { isDefined } from 'twenty-shared/utils'; describe('Handle same object sequential operations test suite', () => { let createdObjectMetadataId: string | undefined = undefined; + const uniqueSuffix = Date.now().toString().slice(-8); afterEach(async () => { if (!isDefined(createdObjectMetadataId)) { @@ -35,19 +35,12 @@ describe('Handle same object sequential operations test suite', () => { createdObjectMetadataId = undefined; }); it('It should handle creation deletion and creation of the same metadata entity gracefully', async () => { - const { - labelPlural, - description, - labelSingular, - namePlural, - nameSingular, - } = CUSTOM_OBJECT_DISHES; const createObjectInput: CreateOneObjectFactoryInput = { - labelPlural, - description, - labelSingular, - namePlural, - nameSingular, + namePlural: `dishes${uniqueSuffix}`, + nameSingular: `dish${uniqueSuffix}`, + labelPlural: `Dishes ${uniqueSuffix}`, + labelSingular: `Dish ${uniqueSuffix}`, + description: 'My favorite dishes', icon: 'IconBuildingSkyscraper', isLabelSyncedWithName: false, }; diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/rename-custom-object.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/object-metadata/rename-custom-object.integration-spec.ts index ebbbfbe6959..88580853a69 100644 --- a/packages/twenty-server/test/integration/metadata/suites/object-metadata/rename-custom-object.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/rename-custom-object.integration-spec.ts @@ -8,6 +8,7 @@ import { FieldMetadataType } from 'twenty-shared/types'; describe('Custom object renaming', () => { let listingObjectId = ''; + const uniqueSuffix = Date.now().toString().slice(-8); const STANDARD_OBJECT_RELATIONS = [ 'noteTarget', @@ -79,10 +80,10 @@ describe('Custom object renaming', () => { fillStandardObjectRelationsMapObjectMetadataId(standardObjects); const CUSTOM_OBJECT = { - namePlural: 'customObjectNamePlural', - nameSingular: 'customObjectNameSingular', - labelPlural: 'customObjectLabelPlural', - labelSingular: 'customObjectLabelSingular', + namePlural: `customObjects${uniqueSuffix}`, + nameSingular: `customObject${uniqueSuffix}`, + labelPlural: `Custom Objects ${uniqueSuffix}`, + labelSingular: `Custom Object ${uniqueSuffix}`, description: 'Custom object description', icon: 'IconListNumbers', isLabelSyncedWithName: false, @@ -135,10 +136,10 @@ describe('Custom object renaming', () => { it('2. should rename custom object', async () => { // Arrange - const HOUSE_NAME_SINGULAR = 'house'; - const HOUSE_NAME_PLURAL = 'houses'; - const HOUSE_LABEL_SINGULAR = 'House'; - const HOUSE_LABEL_PLURAL = 'Houses'; + const HOUSE_NAME_SINGULAR = `house${uniqueSuffix}`; + const HOUSE_NAME_PLURAL = `houses${uniqueSuffix}`; + const HOUSE_LABEL_SINGULAR = `House ${uniqueSuffix}`; + const HOUSE_LABEL_PLURAL = `Houses ${uniqueSuffix}`; // Act const { data } = await updateOneObjectMetadata({