From d5c974054d6edb5b33c87580d97563af030841f7 Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Mon, 23 Jun 2025 21:06:17 +0200 Subject: [PATCH] Improve performance on metadata computation (#12785) In this PR: ## Improve recompute metadata cache performance. We are aiming for ~100ms Deleting relationMetadata table and FKs pointing on it Fetching indexMetadata and indexFieldMetadata in a separate query as typeorm is suboptimizing ## Remove caching lock As recomputing the metadata cache is lighter, we try to stop preventing multiple concurrent computations. This also simplifies interfaces ## Introduce self recovery mecanisms to recompute cache automatically if corrupted Aka getFreshObjectMetadataMaps ## custom object resolver performance improvement: 1sec to 200ms Double check queries and indexes used while creating a custom object Remove the queries to db to use the cached objectMetadataMap ## reduce objectMetadataMaps to 500kb image We used to stored 3 fieldMetadataMaps (byId, byName, byJoinColumnName). While this is great for devXP, this is not great for performances. Using the same mecanisme as for objectMetadataMap: we only keep byIdMap and introduce two otherMaps to idByName, idByJoinColumnName to make the bridge ## Add dataloader on IndexMetadata (aka indexMetadataList in the API) ## Improve field resolver performances too ## Deprecate ClientConfig --- .../src/generated-metadata/gql.ts | 4 +- .../src/generated-metadata/graphql.ts | 34 +- .../twenty-front/src/generated/graphql.tsx | 135 +---- .../UserOrMetadataLoader.stories.tsx | 8 - .../graphql/queries/getClientConfig.ts | 75 --- .../client-config/hooks/useClientConfig.ts | 2 +- .../states/clientConfigApiStatusState.ts | 2 +- .../client-config/types/ClientConfig.ts | 37 ++ .../client-config/utils/getClientConfig.ts | 2 +- .../states/domainConfigurationState.ts | 2 +- .../object-metadata/graphql/queries.ts | 31 +- .../types/ObjectMetadataItem.ts | 1 + ...bjectMetadataItemsToObjectMetadataItems.ts | 19 +- .../twenty-front/src/testing/graphqlMocks.ts | 10 +- .../src/testing/mock-data/config.ts | 7 +- .../generated/mock-metadata-query-result.ts | 563 ++---------------- .../generatedMockObjectMetadataItems.ts | 11 +- ...ded-workspaces-migration.command-runner.ts | 1 - .../1750673748111-remove-relation-metadata.ts | 30 + .../__mocks__/object-metadata-item.mock.ts | 18 +- .../__mocks__/mockPersonObjectMetadata.ts | 36 +- .../errors/graphql-query-runner.exception.ts | 1 + .../graphql-query-filter-condition.parser.ts | 16 +- .../graphql-query-filter-field.parser.ts | 20 +- .../graphql-query-order.parser.ts | 12 +- ...raphql-selected-fields-aggregate.parser.ts | 7 +- ...graphql-selected-fields-relation.parser.ts | 6 +- .../graphql-selected-fields.parser.ts | 22 +- .../graphql-query.parser.ts | 27 +- ...ct-records-to-graphql-connection.helper.ts | 3 +- .../process-nested-relations-v2.helper.ts | 9 +- .../interfaces/base-resolver-service.ts | 4 +- ...phql-query-create-many-resolver.service.ts | 34 +- ...aphql-query-create-one-resolver.service.ts | 5 +- ...phql-query-delete-many-resolver.service.ts | 5 +- ...aphql-query-delete-one-resolver.service.ts | 5 +- ...hql-query-destroy-many-resolver.service.ts | 5 +- ...phql-query-destroy-one-resolver.service.ts | 5 +- ...-query-find-duplicates-resolver.service.ts | 5 +- ...raphql-query-find-many-resolver.service.ts | 2 +- ...hql-query-restore-many-resolver.service.ts | 5 +- ...phql-query-restore-one-resolver.service.ts | 5 +- ...phql-query-update-many-resolver.service.ts | 5 +- ...aphql-query-update-one-resolver.service.ts | 5 +- .../query-runner-args.factory.spec.ts | 31 +- .../query-result-getters.factory.ts | 8 +- .../factories/query-runner-args.factory.ts | 67 +-- ...hql-query-runner-exception-handler.util.ts | 1 + .../utils/handle-duplicate-key-error.util.ts | 8 +- .../workspace-resolver-builder.service.ts | 2 +- .../api/graphql/workspace-schema.factory.ts | 63 +- .../handlers/rest-api-create-many.handler.ts | 6 +- .../handlers/rest-api-create-one.handler.ts | 6 +- .../handlers/rest-api-delete-one.handler.ts | 5 +- .../rest-api-find-duplicates.handler.ts | 4 +- .../handlers/rest-api-update-one.handler.ts | 5 +- .../core/interfaces/rest-api-base.handler.ts | 65 +- .../factories/create-many-query.factory.ts | 2 +- .../find-duplicates-query.factory.ts | 2 +- .../__tests__/check-fields.utils.spec.ts | 14 +- .../__tests__/get-field-type.utils.spec.ts | 14 +- ...ld-metadata-to-graphql-query.utils.spec.ts | 28 +- .../query-builder/utils/check-fields.utils.ts | 2 +- .../utils/check-order-by.utils.ts | 2 +- .../check-filter-enum-values.spec.ts | 14 +- .../__tests__/parse-filter.utils.spec.ts | 22 +- .../filter-utils/check-filter-enum-values.ts | 3 +- .../utils/get-field-type.utils.ts | 5 +- .../__tests__/filter-input.factory.spec.ts | 25 +- .../build-duplicate-conditions.utils.spec.ts | 18 +- .../compute-cursor-arg-filter.utils.spec.ts | 111 +++- .../build-cursor-where-condition.utils.ts | 13 +- .../utils/compute-cursor-arg-filter.utils.ts | 8 +- .../mockObjectMetadataItemsWithFieldMaps.ts | 187 +----- .../token/services/access-token.service.ts | 5 +- .../types/object-record.base.event.ts | 2 +- .../object-record-changed-values.spec.ts | 1 + .../services/feature-flag.service.ts | 10 +- .../record-input-transformer.service.ts | 14 +- .../search/services/search.service.ts | 10 +- .../dataloaders/dataloader.interface.ts | 10 +- .../engine/dataloaders/dataloader.module.ts | 3 +- .../engine/dataloaders/dataloader.service.ts | 76 ++- .../data-source/data-source.entity.ts | 6 +- .../field-metadata/field-metadata.entity.ts | 4 +- .../field-metadata/field-metadata.module.ts | 2 + .../field-metadata/field-metadata.service.ts | 167 +++--- .../interfaces/field-metadata.interface.ts | 5 +- .../interfaces/object-metadata.interface.ts | 2 +- .../field-metadata-relation.service.ts | 16 +- .../field-metadata-enum-validation.service.ts | 7 +- ...ect-or-multi-select-field-metadata.util.ts | 8 +- .../index-metadata/index-metadata.entity.ts | 2 +- .../index-metadata/index-metadata.service.ts | 10 +- .../index-field-metadata.interface.ts | 2 + .../interfaces/index-metadata.interface.ts | 7 + .../object-metadata/object-metadata.module.ts | 2 + .../object-metadata.resolver.ts | 23 + .../object-metadata.service.ts | 184 ++---- .../object-metadata-field-relation.service.ts | 99 ++- .../object-metadata-migration.service.ts | 54 +- ...object-metadata-related-records.service.ts | 5 +- ...d-default-fields-for-custom-object.util.ts | 8 + ...ations-for-custom-object-relations.util.ts | 12 +- .../object-permission.service.spec.ts | 1 - .../object-permission.service.ts | 1 - .../metadata-modules/role/role.service.ts | 3 - .../setting-permission.service.ts | 1 - .../object-metadata-item-with-field-maps.ts | 11 +- .../user-role/user-role.service.ts | 1 - .../validate-field-name-availability.spec.ts | 14 +- .../generate-object-metadata-maps.util.ts | 26 +- ...om-object-metadata-Item-with-field-maps.ts | 18 + ...ve-field-maps-from-object-metadata.util.ts | 14 - .../validate-field-name-availability.utils.ts | 14 +- ...ect-with-same-name-exists-or-throw.util.ts | 35 ++ ...rkspace-feature-flags-map-cache.service.ts | 29 - .../workspace-metadata-cache.service.ts | 150 +++-- .../workspace-metadata-cache.module.ts | 7 +- ...space-permissions-cache-storage.service.ts | 44 -- .../workspace-permissions-cache.service.ts | 114 +--- .../exceptions/twenty-orm.exception.ts | 1 + .../factories/entity-schema-column.factory.ts | 10 +- .../entity-schema-relation.factory.ts | 8 +- .../factories/entity-schema.factory.ts | 6 +- .../scoped-workspace-context.factory.ts | 5 - .../factories/workspace-datasource.factory.ts | 104 ++-- .../twenty-orm/twenty-orm-global.manager.ts | 25 +- .../engine/twenty-orm/twenty-orm.manager.ts | 22 +- .../engine/twenty-orm/twenty-orm.module.ts | 3 +- .../twenty-orm/utils/format-data.util.ts | 25 +- .../twenty-orm/utils/format-result.util.ts | 24 +- ...get-data-from-cache-with-recompute.util.ts | 7 +- .../workspace-cache-storage.service.ts | 95 +-- .../factories/standard-field.factory.ts | 2 + .../factories/standard-index.factory.ts | 9 +- .../factories/standard-object.factory.ts | 4 +- .../partial-field-metadata.interface.ts | 7 +- .../utils/compute-standard-fields.util.ts | 9 +- .../should-generate-field-fake-value.spec.ts | 23 +- .../utils/generate-object-record-fields.ts | 77 +-- .../create-record.workflow-action.ts | 5 +- .../find-records.workflow-action.ts | 3 +- .../database-event-trigger.listener.spec.ts | 24 +- .../database-event-trigger.listener.ts | 11 +- 145 files changed, 1485 insertions(+), 2245 deletions(-) delete mode 100644 packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts create mode 100644 packages/twenty-front/src/modules/client-config/types/ClientConfig.ts create mode 100644 packages/twenty-server/src/database/typeorm/core/migrations/common/1750673748111-remove-relation-metadata.ts create 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/remove-field-maps-from-object-metadata.util.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/utils/validate-no-other-object-with-same-name-exists-or-throw.util.ts diff --git a/packages/twenty-front/src/generated-metadata/gql.ts b/packages/twenty-front/src/generated-metadata/gql.ts index 876f75f74..f1896f17c 100644 --- a/packages/twenty-front/src/generated-metadata/gql.ts +++ b/packages/twenty-front/src/generated-metadata/gql.ts @@ -30,7 +30,7 @@ const documents = { "\n mutation UpdateOneObjectMetadataItem(\n $idToUpdate: UUID!\n $updatePayload: UpdateObjectPayload!\n ) {\n updateOneObject(input: { id: $idToUpdate, update: $updatePayload }) {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isActive\n isSearchable\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n isLabelSyncedWithName\n }\n }\n": types.UpdateOneObjectMetadataItemDocument, "\n mutation DeleteOneObjectMetadataItem($idToDelete: UUID!) {\n deleteOneObject(input: { id: $idToDelete }) {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isActive\n isSearchable\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n isLabelSyncedWithName\n }\n }\n": types.DeleteOneObjectMetadataItemDocument, "\n mutation DeleteOneFieldMetadataItem($idToDelete: UUID!) {\n deleteOneField(input: { id: $idToDelete }) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n settings\n }\n }\n": types.DeleteOneFieldMetadataItemDocument, - "\n query ObjectMetadataItems {\n objects(paging: { first: 1000 }) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n shortcut\n isLabelSyncedWithName\n isSearchable\n duplicateCriteria\n indexMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n indexFieldMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n order\n fieldMetadataId\n }\n }\n }\n }\n }\n }\n fieldsList {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n isLabelSyncedWithName\n relation {\n type\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n": types.ObjectMetadataItemsDocument, + "\n query ObjectMetadataItems {\n objects(paging: { first: 1000 }) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n shortcut\n isLabelSyncedWithName\n isSearchable\n duplicateCriteria\n indexMetadataList {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n }\n fieldsList {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n isLabelSyncedWithName\n relation {\n type\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n": types.ObjectMetadataItemsDocument, "\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n timeoutSeconds\n latestVersion\n latestVersionInputSchema\n publishedVersions\n createdAt\n updatedAt\n }\n": types.ServerlessFunctionFieldsFragmentDoc, "\n \n mutation CreateOneServerlessFunctionItem(\n $input: CreateServerlessFunctionInput!\n ) {\n createOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.CreateOneServerlessFunctionItemDocument, "\n \n mutation DeleteOneServerlessFunction($input: ServerlessFunctionIdInput!) {\n deleteOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.DeleteOneServerlessFunctionDocument, @@ -128,7 +128,7 @@ export function graphql(source: "\n mutation DeleteOneFieldMetadataItem($idToDe /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "\n query ObjectMetadataItems {\n objects(paging: { first: 1000 }) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n shortcut\n isLabelSyncedWithName\n isSearchable\n duplicateCriteria\n indexMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n indexFieldMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n order\n fieldMetadataId\n }\n }\n }\n }\n }\n }\n fieldsList {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n isLabelSyncedWithName\n relation {\n type\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n"): (typeof documents)["\n query ObjectMetadataItems {\n objects(paging: { first: 1000 }) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n shortcut\n isLabelSyncedWithName\n isSearchable\n duplicateCriteria\n indexMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n indexFieldMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n order\n fieldMetadataId\n }\n }\n }\n }\n }\n }\n fieldsList {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n isLabelSyncedWithName\n relation {\n type\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n"]; +export function graphql(source: "\n query ObjectMetadataItems {\n objects(paging: { first: 1000 }) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n shortcut\n isLabelSyncedWithName\n isSearchable\n duplicateCriteria\n indexMetadataList {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n }\n fieldsList {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n isLabelSyncedWithName\n relation {\n type\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n"): (typeof documents)["\n query ObjectMetadataItems {\n objects(paging: { first: 1000 }) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n shortcut\n isLabelSyncedWithName\n isSearchable\n duplicateCriteria\n indexMetadataList {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n }\n fieldsList {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n isLabelSyncedWithName\n relation {\n type\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index a9f69b48e..9c105ebab 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -346,34 +346,6 @@ export type ClientAiModelConfig = { provider: ModelProvider; }; -export type ClientConfig = { - __typename?: 'ClientConfig'; - aiModels: Array; - analyticsEnabled: Scalars['Boolean']['output']; - api: ApiConfig; - authProviders: AuthProviders; - billing: Billing; - calendarBookingPageId?: Maybe; - canManageFeatureFlags: Scalars['Boolean']['output']; - captcha: Captcha; - chromeExtensionId?: Maybe; - debugMode: Scalars['Boolean']['output']; - defaultSubdomain?: Maybe; - frontDomain: Scalars['String']['output']; - isAttachmentPreviewEnabled: Scalars['Boolean']['output']; - isConfigVariablesInDbEnabled: Scalars['Boolean']['output']; - isEmailVerificationRequired: Scalars['Boolean']['output']; - isGoogleCalendarEnabled: Scalars['Boolean']['output']; - isGoogleMessagingEnabled: Scalars['Boolean']['output']; - isMicrosoftCalendarEnabled: Scalars['Boolean']['output']; - isMicrosoftMessagingEnabled: Scalars['Boolean']['output']; - isMultiWorkspaceEnabled: Scalars['Boolean']['output']; - publicFeatureFlags: Array; - sentry: Sentry; - signInPrefilled: Scalars['Boolean']['output']; - support: Support; -}; - export type ComputeStepOutputSchemaInput = { /** Step JSON format */ step: Scalars['JSON']['input']; @@ -1507,6 +1479,7 @@ export type Object = { icon?: Maybe; id: Scalars['UUID']['output']; imageIdentifierFieldMetadataId?: Maybe; + indexMetadataList: Array; indexMetadatas: ObjectIndexMetadatasConnection; isActive: Scalars['Boolean']['output']; isCustom: Scalars['Boolean']['output']; @@ -1706,7 +1679,6 @@ export type Query = { billingPortalSession: BillingSessionOutput; checkUserExists: CheckUserExistOutput; checkWorkspaceInviteHashIsValid: WorkspaceInviteHashValid; - clientConfig: ClientConfig; currentUser: User; currentWorkspace: Workspace; field: Field; @@ -2834,7 +2806,7 @@ export type DeleteOneFieldMetadataItemMutation = { __typename?: 'Mutation', dele export type ObjectMetadataItemsQueryVariables = Exact<{ [key: string]: never; }>; -export type ObjectMetadataItemsQuery = { __typename?: 'Query', objects: { __typename?: 'ObjectConnection', edges: Array<{ __typename?: 'ObjectEdge', node: { __typename?: 'Object', id: any, dataSourceId: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, isCustom: boolean, isRemote: boolean, isActive: boolean, isSystem: boolean, createdAt: any, updatedAt: any, labelIdentifierFieldMetadataId?: string | null, imageIdentifierFieldMetadataId?: string | null, shortcut?: string | null, isLabelSyncedWithName: boolean, isSearchable: boolean, duplicateCriteria?: Array> | null, indexMetadatas: { __typename?: 'ObjectIndexMetadatasConnection', edges: Array<{ __typename?: 'IndexEdge', node: { __typename?: 'Index', id: any, createdAt: any, updatedAt: any, name: string, indexWhereClause?: string | null, indexType: IndexType, isUnique: boolean, indexFieldMetadatas: { __typename?: 'IndexIndexFieldMetadatasConnection', edges: Array<{ __typename?: 'IndexFieldEdge', node: { __typename?: 'IndexField', id: any, createdAt: any, updatedAt: any, order: number, fieldMetadataId: any } }> } } }> }, fieldsList: Array<{ __typename?: 'Field', id: any, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isSystem?: boolean | null, isNullable?: boolean | null, isUnique?: boolean | null, createdAt: any, updatedAt: any, defaultValue?: any | null, options?: any | null, settings?: any | null, isLabelSyncedWithName?: boolean | null, relation?: { __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: any, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: any, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: any, name: string }, targetFieldMetadata: { __typename?: 'Field', id: any, name: string } } | null }> } }>, pageInfo: { __typename?: 'PageInfo', hasNextPage?: boolean | null, hasPreviousPage?: boolean | null, startCursor?: any | null, endCursor?: any | null } } }; +export type ObjectMetadataItemsQuery = { __typename?: 'Query', objects: { __typename?: 'ObjectConnection', edges: Array<{ __typename?: 'ObjectEdge', node: { __typename?: 'Object', id: any, dataSourceId: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, isCustom: boolean, isRemote: boolean, isActive: boolean, isSystem: boolean, createdAt: any, updatedAt: any, labelIdentifierFieldMetadataId?: string | null, imageIdentifierFieldMetadataId?: string | null, shortcut?: string | null, isLabelSyncedWithName: boolean, isSearchable: boolean, duplicateCriteria?: Array> | null, indexMetadataList: Array<{ __typename?: 'Index', id: any, createdAt: any, updatedAt: any, name: string, indexWhereClause?: string | null, indexType: IndexType, isUnique: boolean }>, fieldsList: Array<{ __typename?: 'Field', id: any, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isSystem?: boolean | null, isNullable?: boolean | null, isUnique?: boolean | null, createdAt: any, updatedAt: any, defaultValue?: any | null, options?: any | null, settings?: any | null, isLabelSyncedWithName?: boolean | null, relation?: { __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: any, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: any, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: any, name: string }, targetFieldMetadata: { __typename?: 'Field', id: any, name: string } } | null }> } }>, pageInfo: { __typename?: 'PageInfo', hasNextPage?: boolean | null, hasPreviousPage?: boolean | null, startCursor?: any | null, endCursor?: any | null } } }; export type ServerlessFunctionFieldsFragment = { __typename?: 'ServerlessFunction', id: any, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, latestVersionInputSchema?: any | null, publishedVersions: Array, createdAt: any, updatedAt: any }; @@ -2917,7 +2889,7 @@ export const UpdateOneFieldMetadataItemDocument = {"kind":"Document","definition export const UpdateOneObjectMetadataItemDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateOneObjectMetadataItem"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"idToUpdate"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"updatePayload"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateObjectPayload"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateOneObject"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"idToUpdate"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"update"},"value":{"kind":"Variable","name":{"kind":"Name","value":"updatePayload"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"dataSourceId"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}},{"kind":"Field","name":{"kind":"Name","value":"labelSingular"}},{"kind":"Field","name":{"kind":"Name","value":"labelPlural"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"isSearchable"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"labelIdentifierFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"imageIdentifierFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"isLabelSyncedWithName"}}]}}]}}]} as unknown as DocumentNode; export const DeleteOneObjectMetadataItemDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteOneObjectMetadataItem"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"idToDelete"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteOneObject"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"idToDelete"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"dataSourceId"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}},{"kind":"Field","name":{"kind":"Name","value":"labelSingular"}},{"kind":"Field","name":{"kind":"Name","value":"labelPlural"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"isSearchable"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"labelIdentifierFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"imageIdentifierFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"isLabelSyncedWithName"}}]}}]}}]} as unknown as DocumentNode; export const DeleteOneFieldMetadataItemDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteOneFieldMetadataItem"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"idToDelete"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteOneField"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"idToDelete"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"isNullable"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"settings"}}]}}]}}]} as unknown as DocumentNode; -export const ObjectMetadataItemsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ObjectMetadataItems"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"objects"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"paging"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"1000"}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"dataSourceId"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}},{"kind":"Field","name":{"kind":"Name","value":"labelSingular"}},{"kind":"Field","name":{"kind":"Name","value":"labelPlural"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"isRemote"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"isSystem"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"labelIdentifierFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"imageIdentifierFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"shortcut"}},{"kind":"Field","name":{"kind":"Name","value":"isLabelSyncedWithName"}},{"kind":"Field","name":{"kind":"Name","value":"isSearchable"}},{"kind":"Field","name":{"kind":"Name","value":"duplicateCriteria"}},{"kind":"Field","name":{"kind":"Name","value":"indexMetadatas"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"paging"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"100"}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"indexWhereClause"}},{"kind":"Field","name":{"kind":"Name","value":"indexType"}},{"kind":"Field","name":{"kind":"Name","value":"isUnique"}},{"kind":"Field","name":{"kind":"Name","value":"indexFieldMetadatas"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"paging"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"100"}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"order"}},{"kind":"Field","name":{"kind":"Name","value":"fieldMetadataId"}}]}}]}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"fieldsList"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"isSystem"}},{"kind":"Field","name":{"kind":"Name","value":"isNullable"}},{"kind":"Field","name":{"kind":"Name","value":"isUnique"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"defaultValue"}},{"kind":"Field","name":{"kind":"Name","value":"options"}},{"kind":"Field","name":{"kind":"Name","value":"settings"}},{"kind":"Field","name":{"kind":"Name","value":"isLabelSyncedWithName"}},{"kind":"Field","name":{"kind":"Name","value":"relation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"sourceObjectMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}}]}},{"kind":"Field","name":{"kind":"Name","value":"targetObjectMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}}]}},{"kind":"Field","name":{"kind":"Name","value":"sourceFieldMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"targetFieldMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"hasPreviousPage"}},{"kind":"Field","name":{"kind":"Name","value":"startCursor"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]}}]}}]} as unknown as DocumentNode; +export const ObjectMetadataItemsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ObjectMetadataItems"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"objects"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"paging"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"1000"}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"dataSourceId"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}},{"kind":"Field","name":{"kind":"Name","value":"labelSingular"}},{"kind":"Field","name":{"kind":"Name","value":"labelPlural"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"isRemote"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"isSystem"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"labelIdentifierFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"imageIdentifierFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"shortcut"}},{"kind":"Field","name":{"kind":"Name","value":"isLabelSyncedWithName"}},{"kind":"Field","name":{"kind":"Name","value":"isSearchable"}},{"kind":"Field","name":{"kind":"Name","value":"duplicateCriteria"}},{"kind":"Field","name":{"kind":"Name","value":"indexMetadataList"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"indexWhereClause"}},{"kind":"Field","name":{"kind":"Name","value":"indexType"}},{"kind":"Field","name":{"kind":"Name","value":"isUnique"}}]}},{"kind":"Field","name":{"kind":"Name","value":"fieldsList"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"isSystem"}},{"kind":"Field","name":{"kind":"Name","value":"isNullable"}},{"kind":"Field","name":{"kind":"Name","value":"isUnique"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"defaultValue"}},{"kind":"Field","name":{"kind":"Name","value":"options"}},{"kind":"Field","name":{"kind":"Name","value":"settings"}},{"kind":"Field","name":{"kind":"Name","value":"isLabelSyncedWithName"}},{"kind":"Field","name":{"kind":"Name","value":"relation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"sourceObjectMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}}]}},{"kind":"Field","name":{"kind":"Name","value":"targetObjectMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}}]}},{"kind":"Field","name":{"kind":"Name","value":"sourceFieldMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"targetFieldMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"hasPreviousPage"}},{"kind":"Field","name":{"kind":"Name","value":"startCursor"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]}}]}}]} as unknown as DocumentNode; export const CreateOneServerlessFunctionItemDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateOneServerlessFunctionItem"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateServerlessFunctionInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createOneServerlessFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServerlessFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerlessFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"timeoutSeconds"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersion"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersionInputSchema"}},{"kind":"Field","name":{"kind":"Name","value":"publishedVersions"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode; export const DeleteOneServerlessFunctionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteOneServerlessFunction"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunctionIdInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteOneServerlessFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServerlessFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerlessFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"timeoutSeconds"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersion"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersionInputSchema"}},{"kind":"Field","name":{"kind":"Name","value":"publishedVersions"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode; export const ExecuteOneServerlessFunctionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ExecuteOneServerlessFunction"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ExecuteServerlessFunctionInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"executeOneServerlessFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"data"}},{"kind":"Field","name":{"kind":"Name","value":"logs"}},{"kind":"Field","name":{"kind":"Name","value":"duration"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}}]}}]} as unknown as DocumentNode; diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 734ae4066..378f6a7e0 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -338,34 +338,6 @@ export type ClientAiModelConfig = { provider: ModelProvider; }; -export type ClientConfig = { - __typename?: 'ClientConfig'; - aiModels: Array; - analyticsEnabled: Scalars['Boolean']; - api: ApiConfig; - authProviders: AuthProviders; - billing: Billing; - calendarBookingPageId?: Maybe; - canManageFeatureFlags: Scalars['Boolean']; - captcha: Captcha; - chromeExtensionId?: Maybe; - debugMode: Scalars['Boolean']; - defaultSubdomain?: Maybe; - frontDomain: Scalars['String']; - isAttachmentPreviewEnabled: Scalars['Boolean']; - isConfigVariablesInDbEnabled: Scalars['Boolean']; - isEmailVerificationRequired: Scalars['Boolean']; - isGoogleCalendarEnabled: Scalars['Boolean']; - isGoogleMessagingEnabled: Scalars['Boolean']; - isMicrosoftCalendarEnabled: Scalars['Boolean']; - isMicrosoftMessagingEnabled: Scalars['Boolean']; - isMultiWorkspaceEnabled: Scalars['Boolean']; - publicFeatureFlags: Array; - sentry: Sentry; - signInPrefilled: Scalars['Boolean']; - support: Support; -}; - export type ComputeStepOutputSchemaInput = { /** Step JSON format */ step: Scalars['JSON']; @@ -1410,6 +1382,7 @@ export type Object = { icon?: Maybe; id: Scalars['UUID']; imageIdentifierFieldMetadataId?: Maybe; + indexMetadataList: Array; indexMetadatas: ObjectIndexMetadatasConnection; isActive: Scalars['Boolean']; isCustom: Scalars['Boolean']; @@ -1609,7 +1582,6 @@ export type Query = { billingPortalSession: BillingSessionOutput; checkUserExists: CheckUserExistOutput; checkWorkspaceInviteHashIsValid: WorkspaceInviteHashValid; - clientConfig: ClientConfig; currentUser: User; currentWorkspace: Workspace; field: Field; @@ -2821,11 +2793,6 @@ export type GetMeteredProductsUsageQueryVariables = Exact<{ [key: string]: never export type GetMeteredProductsUsageQuery = { __typename?: 'Query', getMeteredProductsUsage: Array<{ __typename?: 'BillingMeteredProductUsageOutput', productKey: BillingProductKey, usageQuantity: number, freeTierQuantity: number, freeTrialQuantity: number, unitPriceCents: number, totalCostCents: number }> }; -export type GetClientConfigQueryVariables = Exact<{ [key: string]: never; }>; - - -export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, isMultiWorkspaceEnabled: boolean, isEmailVerificationRequired: boolean, defaultSubdomain?: string | null, frontDomain: string, debugMode: boolean, analyticsEnabled: boolean, isAttachmentPreviewEnabled: boolean, chromeExtensionId?: string | null, canManageFeatureFlags: boolean, isMicrosoftMessagingEnabled: boolean, isMicrosoftCalendarEnabled: boolean, isGoogleMessagingEnabled: boolean, isGoogleCalendarEnabled: boolean, isConfigVariablesInDbEnabled: boolean, calendarBookingPageId?: string | null, aiModels: Array<{ __typename?: 'ClientAIModelConfig', modelId: string, label: string, provider: ModelProvider, inputCostPer1kTokensInCredits: number, outputCostPer1kTokensInCredits: number }>, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl?: string | null, trialPeriods: Array<{ __typename?: 'BillingTrialPeriodDTO', duration: number, isCreditCardRequired: boolean }> }, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean, microsoft: boolean, sso: Array<{ __typename?: 'SSOIdentityProvider', id: string, name: string, type: IdentityProviderType, status: SsoIdentityProviderStatus, issuer: string }> }, support: { __typename?: 'Support', supportDriver: SupportDriver, supportFrontChatId?: string | null }, sentry: { __typename?: 'Sentry', dsn?: string | null, environment?: string | null, release?: string | null }, captcha: { __typename?: 'Captcha', provider?: CaptchaDriverType | null, siteKey?: string | null }, api: { __typename?: 'ApiConfig', mutationMaximumAffectedRecords: number }, publicFeatureFlags: Array<{ __typename?: 'PublicFeatureFlag', key: FeatureFlagKey, metadata: { __typename?: 'PublicFeatureFlagMetadata', label: string, description: string, imagePath: string } }> } }; - export type SearchQueryVariables = Exact<{ searchInput: Scalars['String']; limit: Scalars['Int']; @@ -4792,106 +4759,6 @@ export function useGetMeteredProductsUsageLazyQuery(baseOptions?: Apollo.LazyQue export type GetMeteredProductsUsageQueryHookResult = ReturnType; export type GetMeteredProductsUsageLazyQueryHookResult = ReturnType; export type GetMeteredProductsUsageQueryResult = Apollo.QueryResult; -export const GetClientConfigDocument = gql` - query GetClientConfig { - clientConfig { - aiModels { - modelId - label - provider - inputCostPer1kTokensInCredits - outputCostPer1kTokensInCredits - } - billing { - isBillingEnabled - billingUrl - trialPeriods { - duration - isCreditCardRequired - } - } - authProviders { - google - password - microsoft - sso { - id - name - type - status - issuer - } - } - signInPrefilled - isMultiWorkspaceEnabled - isEmailVerificationRequired - defaultSubdomain - frontDomain - debugMode - analyticsEnabled - isAttachmentPreviewEnabled - support { - supportDriver - supportFrontChatId - } - sentry { - dsn - environment - release - } - captcha { - provider - siteKey - } - api { - mutationMaximumAffectedRecords - } - chromeExtensionId - canManageFeatureFlags - publicFeatureFlags { - key - metadata { - label - description - imagePath - } - } - isMicrosoftMessagingEnabled - isMicrosoftCalendarEnabled - isGoogleMessagingEnabled - isGoogleCalendarEnabled - isConfigVariablesInDbEnabled - calendarBookingPageId - } -} - `; - -/** - * __useGetClientConfigQuery__ - * - * To run a query within a React component, call `useGetClientConfigQuery` and pass it any options that fit your needs. - * When your component renders, `useGetClientConfigQuery` returns an object from Apollo Client that contains loading, error, and data properties - * you can use to render your UI. - * - * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; - * - * @example - * const { data, loading, error } = useGetClientConfigQuery({ - * variables: { - * }, - * }); - */ -export function useGetClientConfigQuery(baseOptions?: Apollo.QueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useQuery(GetClientConfigDocument, options); - } -export function useGetClientConfigLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useLazyQuery(GetClientConfigDocument, options); - } -export type GetClientConfigQueryHookResult = ReturnType; -export type GetClientConfigLazyQueryHookResult = ReturnType; -export type GetClientConfigQueryResult = Apollo.QueryResult; export const SearchDocument = gql` query Search($searchInput: String!, $limit: Int!, $after: String, $excludedObjectNameSingulars: [String!], $includedObjectNameSingulars: [String!], $filter: ObjectRecordFilterInput) { search( diff --git a/packages/twenty-front/src/loading/components/__stories__/UserOrMetadataLoader.stories.tsx b/packages/twenty-front/src/loading/components/__stories__/UserOrMetadataLoader.stories.tsx index 23af939ad..29cbbdca7 100644 --- a/packages/twenty-front/src/loading/components/__stories__/UserOrMetadataLoader.stories.tsx +++ b/packages/twenty-front/src/loading/components/__stories__/UserOrMetadataLoader.stories.tsx @@ -5,7 +5,6 @@ import { within } from '@storybook/test'; import { HttpResponse, graphql, http } from 'msw'; import { GET_PUBLIC_WORKSPACE_DATA_BY_DOMAIN } from '@/auth/graphql/queries/getPublicWorkspaceDataByDomain'; -import { GET_CLIENT_CONFIG } from '@/client-config/graphql/queries/getClientConfig'; import { FIND_MANY_OBJECT_METADATA_ITEMS } from '@/object-metadata/graphql/queries'; import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser'; import { REACT_APP_SERVER_BASE_URL } from '~/config'; @@ -32,13 +31,6 @@ const userMetadataLoaderMocks = { }, }); }), - graphql.query(getOperationName(GET_CLIENT_CONFIG) ?? '', () => { - return HttpResponse.json({ - data: { - clientConfig: mockedClientConfig, - }, - }); - }), graphql.query( getOperationName(GET_PUBLIC_WORKSPACE_DATA_BY_DOMAIN) ?? '', () => { diff --git a/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts b/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts deleted file mode 100644 index b57dd3822..000000000 --- a/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { gql } from '@apollo/client'; - -export const GET_CLIENT_CONFIG = gql` - query GetClientConfig { - clientConfig { - aiModels { - modelId - label - provider - inputCostPer1kTokensInCredits - outputCostPer1kTokensInCredits - } - billing { - isBillingEnabled - billingUrl - trialPeriods { - duration - isCreditCardRequired - } - } - authProviders { - google - password - microsoft - sso { - id - name - type - status - issuer - } - } - signInPrefilled - isMultiWorkspaceEnabled - isEmailVerificationRequired - defaultSubdomain - frontDomain - debugMode - analyticsEnabled - isAttachmentPreviewEnabled - support { - supportDriver - supportFrontChatId - } - sentry { - dsn - environment - release - } - captcha { - provider - siteKey - } - api { - mutationMaximumAffectedRecords - } - chromeExtensionId - canManageFeatureFlags - publicFeatureFlags { - key - metadata { - label - description - imagePath - } - } - isMicrosoftMessagingEnabled - isMicrosoftCalendarEnabled - isGoogleMessagingEnabled - isGoogleCalendarEnabled - isConfigVariablesInDbEnabled - calendarBookingPageId - } - } -`; diff --git a/packages/twenty-front/src/modules/client-config/hooks/useClientConfig.ts b/packages/twenty-front/src/modules/client-config/hooks/useClientConfig.ts index 8f1fff94d..d30b66107 100644 --- a/packages/twenty-front/src/modules/client-config/hooks/useClientConfig.ts +++ b/packages/twenty-front/src/modules/client-config/hooks/useClientConfig.ts @@ -1,6 +1,6 @@ +import { ClientConfig } from '@/client-config/types/ClientConfig'; import { useCallback } from 'react'; import { useRecoilValue, useSetRecoilState } from 'recoil'; -import { ClientConfig } from '~/generated/graphql'; import { clientConfigApiStatusState } from '../states/clientConfigApiStatusState'; import { getClientConfig } from '../utils/getClientConfig'; diff --git a/packages/twenty-front/src/modules/client-config/states/clientConfigApiStatusState.ts b/packages/twenty-front/src/modules/client-config/states/clientConfigApiStatusState.ts index de3f188e4..2fb98ee12 100644 --- a/packages/twenty-front/src/modules/client-config/states/clientConfigApiStatusState.ts +++ b/packages/twenty-front/src/modules/client-config/states/clientConfigApiStatusState.ts @@ -1,5 +1,5 @@ +import { ClientConfig } from '@/client-config/types/ClientConfig'; import { createState } from 'twenty-ui/utilities'; -import { ClientConfig } from '~/generated/graphql'; type ClientConfigApiStatus = { isLoadedOnce: boolean; diff --git a/packages/twenty-front/src/modules/client-config/types/ClientConfig.ts b/packages/twenty-front/src/modules/client-config/types/ClientConfig.ts new file mode 100644 index 000000000..2d674c29e --- /dev/null +++ b/packages/twenty-front/src/modules/client-config/types/ClientConfig.ts @@ -0,0 +1,37 @@ +import { + ApiConfig, + AuthProviders, + Billing, + Captcha, + ClientAiModelConfig, + PublicFeatureFlag, + Sentry, + Support, +} from '~/generated-metadata/graphql'; + +export type ClientConfig = { + aiModels: Array; + analyticsEnabled: boolean; + api: ApiConfig; + authProviders: AuthProviders; + billing: Billing; + calendarBookingPageId?: string; + canManageFeatureFlags: boolean; + captcha: Captcha; + chromeExtensionId?: string; + debugMode: boolean; + defaultSubdomain?: string; + frontDomain: string; + isAttachmentPreviewEnabled: boolean; + isConfigVariablesInDbEnabled: boolean; + isEmailVerificationRequired: boolean; + isGoogleCalendarEnabled: boolean; + isGoogleMessagingEnabled: boolean; + isMicrosoftCalendarEnabled: boolean; + isMicrosoftMessagingEnabled: boolean; + isMultiWorkspaceEnabled: boolean; + publicFeatureFlags: Array; + sentry: Sentry; + signInPrefilled: boolean; + support: Support; +}; diff --git a/packages/twenty-front/src/modules/client-config/utils/getClientConfig.ts b/packages/twenty-front/src/modules/client-config/utils/getClientConfig.ts index c061dee74..990620603 100644 --- a/packages/twenty-front/src/modules/client-config/utils/getClientConfig.ts +++ b/packages/twenty-front/src/modules/client-config/utils/getClientConfig.ts @@ -1,5 +1,5 @@ +import { ClientConfig } from '@/client-config/types/ClientConfig'; import { REACT_APP_SERVER_BASE_URL } from '~/config'; -import { ClientConfig } from '~/generated/graphql'; export const getClientConfig = async (): Promise => { const response = await fetch(`${REACT_APP_SERVER_BASE_URL}/client-config`, { diff --git a/packages/twenty-front/src/modules/domain-manager/states/domainConfigurationState.ts b/packages/twenty-front/src/modules/domain-manager/states/domainConfigurationState.ts index eaf7188d7..ccf117de3 100644 --- a/packages/twenty-front/src/modules/domain-manager/states/domainConfigurationState.ts +++ b/packages/twenty-front/src/modules/domain-manager/states/domainConfigurationState.ts @@ -1,4 +1,4 @@ -import { ClientConfig } from '~/generated/graphql'; +import { ClientConfig } from '@/client-config/types/ClientConfig'; import { createState } from 'twenty-ui/utilities'; export const domainConfigurationState = createState< diff --git a/packages/twenty-front/src/modules/object-metadata/graphql/queries.ts b/packages/twenty-front/src/modules/object-metadata/graphql/queries.ts index 422626e7a..2e1510171 100644 --- a/packages/twenty-front/src/modules/object-metadata/graphql/queries.ts +++ b/packages/twenty-front/src/modules/object-metadata/graphql/queries.ts @@ -25,29 +25,14 @@ export const FIND_MANY_OBJECT_METADATA_ITEMS = gql` isLabelSyncedWithName isSearchable duplicateCriteria - indexMetadatas(paging: { first: 100 }) { - edges { - node { - id - createdAt - updatedAt - name - indexWhereClause - indexType - isUnique - indexFieldMetadatas(paging: { first: 100 }) { - edges { - node { - id - createdAt - updatedAt - order - fieldMetadataId - } - } - } - } - } + indexMetadataList { + id + createdAt + updatedAt + name + indexWhereClause + indexType + isUnique } fieldsList { id diff --git a/packages/twenty-front/src/modules/object-metadata/types/ObjectMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/types/ObjectMetadataItem.ts index 638d18ed0..d519a69df 100644 --- a/packages/twenty-front/src/modules/object-metadata/types/ObjectMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/types/ObjectMetadataItem.ts @@ -11,6 +11,7 @@ export type ObjectMetadataItem = Omit< | 'indexMetadatas' | 'labelIdentifierFieldMetadataId' | 'fieldsList' + | 'indexMetadataList' > & { __typename?: string; fields: FieldMetadataItem[]; diff --git a/packages/twenty-front/src/modules/object-metadata/utils/mapPaginatedObjectMetadataItemsToObjectMetadataItems.ts b/packages/twenty-front/src/modules/object-metadata/utils/mapPaginatedObjectMetadataItemsToObjectMetadataItems.ts index 6dba97233..6f2b8d0c3 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/mapPaginatedObjectMetadataItemsToObjectMetadataItems.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/mapPaginatedObjectMetadataItemsToObjectMetadataItems.ts @@ -1,3 +1,4 @@ +import { IndexMetadataItem } from '@/object-metadata/types/IndexMetadataItem'; import { objectMetadataItemSchema } from '@/object-metadata/validation-schemas/objectMetadataItemSchema'; import { ObjectMetadataItemsQuery } from '~/generated-metadata/graphql'; import { ObjectMetadataItem } from '../types/ObjectMetadataItem'; @@ -14,19 +15,21 @@ export const mapPaginatedObjectMetadataItemsToObjectMetadataItems = ({ object.node.labelIdentifierFieldMetadataId, ); - const { fieldsList, ...objectWithoutFieldsList } = object.node; + const { fieldsList, indexMetadataList, ...objectWithoutFieldsList } = + object.node; return { ...objectWithoutFieldsList, fields: fieldsList, labelIdentifierFieldMetadataId, - indexMetadatas: object.node.indexMetadatas?.edges.map((index) => ({ - ...index.node, - indexFieldMetadatas: index.node.indexFieldMetadatas?.edges.map( - (indexField) => indexField.node, - ), - })), - }; + indexMetadatas: indexMetadataList.map( + (index) => + ({ + ...index, + indexFieldMetadatas: [], + }) satisfies IndexMetadataItem, + ), + } satisfies ObjectMetadataItem; }) ?? []; return formattedObjects; diff --git a/packages/twenty-front/src/testing/graphqlMocks.ts b/packages/twenty-front/src/testing/graphqlMocks.ts index 5174a1430..95cb1db86 100644 --- a/packages/twenty-front/src/testing/graphqlMocks.ts +++ b/packages/twenty-front/src/testing/graphqlMocks.ts @@ -2,7 +2,6 @@ import { getOperationName } from '@apollo/client/utilities'; import { graphql, GraphQLQuery, http, HttpResponse } from 'msw'; import { TRACK_ANALYTICS } from '@/analytics/graphql/queries/track'; -import { GET_CLIENT_CONFIG } from '@/client-config/graphql/queries/getClientConfig'; import { FIND_MANY_OBJECT_METADATA_ITEMS } from '@/object-metadata/graphql/queries'; import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser'; import { REACT_APP_SERVER_BASE_URL } from '~/config'; @@ -15,11 +14,11 @@ import { mockedFavoritesData } from '~/testing/mock-data/favorite'; import { mockedFavoriteFoldersData } from '~/testing/mock-data/favorite-folders'; import { mockedNotes } from '~/testing/mock-data/notes'; import { getPeopleRecordConnectionMock } from '~/testing/mock-data/people'; +import { mockedPublicWorkspaceDataBySubdomain } from '~/testing/mock-data/publicWorkspaceDataBySubdomain'; import { mockedRemoteTables } from '~/testing/mock-data/remote-tables'; import { mockedUserData } from '~/testing/mock-data/users'; import { mockedViewsData } from '~/testing/mock-data/views'; import { mockWorkspaceMembers } from '~/testing/mock-data/workspace-members'; -import { mockedPublicWorkspaceDataBySubdomain } from '~/testing/mock-data/publicWorkspaceDataBySubdomain'; import { GET_PUBLIC_WORKSPACE_DATA_BY_DOMAIN } from '@/auth/graphql/queries/getPublicWorkspaceDataByDomain'; import { GET_ROLES } from '@/settings/roles/graphql/queries/getRolesQuery'; @@ -104,13 +103,6 @@ export const graphqlMocks = { }, }); }), - graphql.query(getOperationName(GET_CLIENT_CONFIG) ?? '', () => { - return HttpResponse.json({ - data: { - clientConfig: mockedClientConfig, - }, - }); - }), http.get(`${REACT_APP_SERVER_BASE_URL}/client-config`, () => { return HttpResponse.json(mockedClientConfig); }), diff --git a/packages/twenty-front/src/testing/mock-data/config.ts b/packages/twenty-front/src/testing/mock-data/config.ts index 459b281fb..a12538b9c 100644 --- a/packages/twenty-front/src/testing/mock-data/config.ts +++ b/packages/twenty-front/src/testing/mock-data/config.ts @@ -1,8 +1,5 @@ -import { - CaptchaDriverType, - ClientConfig, - SupportDriver, -} from '~/generated/graphql'; +import { ClientConfig } from '@/client-config/types/ClientConfig'; +import { CaptchaDriverType, SupportDriver } from '~/generated/graphql'; export const mockedClientConfig: ClientConfig = { aiModels: [], diff --git a/packages/twenty-front/src/testing/mock-data/generated/mock-metadata-query-result.ts b/packages/twenty-front/src/testing/mock-data/generated/mock-metadata-query-result.ts index 16290a5c8..f2a20c5df 100644 --- a/packages/twenty-front/src/testing/mock-data/generated/mock-metadata-query-result.ts +++ b/packages/twenty-front/src/testing/mock-data/generated/mock-metadata-query-result.ts @@ -42,10 +42,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = "labelPlural": "Workflow Runs", "description": "A workflow run", "icon": "IconHistoryToggle", - "indexMetadatas": { - "__typename": "ObjectIndexMetadatasConnection", - "edges": [] - }, + "indexMetadataList": [], "fieldsList": [ { "__typename": "Field", @@ -561,10 +558,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = "labelPlural": "Note Targets", "description": "A note target", "icon": "IconCheckbox", - "indexMetadatas": { - "__typename": "ObjectIndexMetadatasConnection", - "edges": [] - }, + "indexMetadataList": [], "fieldsList": [ { "__typename": "Field", @@ -1033,40 +1027,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = "labelPlural": "Survey results", "description": null, "icon": "IconRulerMeasure", - "indexMetadatas": { - "__typename": "ObjectIndexMetadatasConnection", - "edges": [ - { - "__typename": "IndexEdge", - "node": { - "__typename": "Index", - "id": "e5cc0d0e-515d-4ae4-bbd4-a2c76b394ff7", - "createdAt": "2025-06-09T18:53:52.547Z", - "updatedAt": "2025-06-09T18:53:52.547Z", - "name": "IDX_e2a25535adda4544be555d3b6d8", - "indexWhereClause": null, - "indexType": "GIN", - "isUnique": false, - "indexFieldMetadatas": { - "__typename": "IndexIndexFieldMetadatasConnection", - "edges": [ - { - "__typename": "IndexFieldEdge", - "node": { - "__typename": "IndexField", - "id": "d1c159f6-148a-4670-87eb-05fc7c824704", - "createdAt": "2025-06-09T18:53:52.547Z", - "updatedAt": "2025-06-09T18:53:52.547Z", - "order": 0, - "fieldMetadataId": "a4c055c6-df30-4cdf-9622-3d3db93b4337" - } - } - ] - } - } - } - ] - }, + "indexMetadataList": [], "fieldsList": [ { "__typename": "Field", @@ -1650,10 +1611,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = "labelPlural": "API Keys", "description": "An API key", "icon": "IconRobot", - "indexMetadatas": { - "__typename": "ObjectIndexMetadatasConnection", - "edges": [] - }, + "indexMetadataList": [], "fieldsList": [ { "__typename": "Field", @@ -1835,10 +1793,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = "labelPlural": "View Filter Groups", "description": "(System) View Filter Groups", "icon": "IconFilterBolt", - "indexMetadatas": { - "__typename": "ObjectIndexMetadatasConnection", - "edges": [] - }, + "indexMetadataList": [], "fieldsList": [ { "__typename": "Field", @@ -2092,10 +2047,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = "labelPlural": "Timeline Activities", "description": "Aggregated / filtered event to be displayed on the timeline", "icon": "IconTimelineEvent", - "indexMetadatas": { - "__typename": "ObjectIndexMetadatasConnection", - "edges": [] - }, + "indexMetadataList": [], "fieldsList": [ { "__typename": "Field", @@ -2940,10 +2892,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = "labelPlural": "Message Threads", "description": "A group of related messages (e.g. email thread, chat thread)", "icon": "IconMessage", - "indexMetadatas": { - "__typename": "ObjectIndexMetadatasConnection", - "edges": [] - }, + "indexMetadataList": [], "fieldsList": [ { "__typename": "Field", @@ -3110,10 +3059,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = "labelPlural": "View Groups", "description": "(System) View Groups", "icon": "IconTag", - "indexMetadatas": { - "__typename": "ObjectIndexMetadatasConnection", - "edges": [] - }, + "indexMetadataList": [], "fieldsList": [ { "__typename": "Field", @@ -3366,40 +3312,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = "labelPlural": "Rockets", "description": "A rocket", "icon": "IconRocket", - "indexMetadatas": { - "__typename": "ObjectIndexMetadatasConnection", - "edges": [ - { - "__typename": "IndexEdge", - "node": { - "__typename": "Index", - "id": "960148fc-d043-415e-a71d-434cf3f614b9", - "createdAt": "2025-06-09T18:53:50.917Z", - "updatedAt": "2025-06-09T18:53:50.917Z", - "name": "IDX_530792e4278e7696c4e3e3e55f8", - "indexWhereClause": null, - "indexType": "GIN", - "isUnique": false, - "indexFieldMetadatas": { - "__typename": "IndexIndexFieldMetadatasConnection", - "edges": [ - { - "__typename": "IndexFieldEdge", - "node": { - "__typename": "IndexField", - "id": "54da1cce-c87c-4709-ab33-1b92e0b20e47", - "createdAt": "2025-06-09T18:53:50.917Z", - "updatedAt": "2025-06-09T18:53:50.917Z", - "order": 0, - "fieldMetadataId": "683ab4d4-70fb-4fc9-a765-e77755a8f30e" - } - } - ] - } - } - } - ] - }, + "indexMetadataList": [], "fieldsList": [ { "__typename": "Field", @@ -3839,10 +3752,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = "labelPlural": "Message Participants", "description": "Message Participants", "icon": "IconUserCircle", - "indexMetadatas": { - "__typename": "ObjectIndexMetadatasConnection", - "edges": [] - }, + "indexMetadataList": [], "fieldsList": [ { "__typename": "Field", @@ -4203,10 +4113,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = "labelPlural": "Views", "description": "(System) Views", "icon": "IconLayoutCollage", - "indexMetadatas": { - "__typename": "ObjectIndexMetadatasConnection", - "edges": [] - }, + "indexMetadataList": [], "fieldsList": [ { "__typename": "Field", @@ -4952,10 +4859,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = "labelPlural": "Favorites", "description": "A favorite that can be accessed from the left menu", "icon": "IconHeart", - "indexMetadatas": { - "__typename": "ObjectIndexMetadatasConnection", - "edges": [] - }, + "indexMetadataList": [], "fieldsList": [ { "__typename": "Field", @@ -5795,10 +5699,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = "labelPlural": "Favorite Folders", "description": "A Folder of favorites", "icon": "IconFolder", - "indexMetadatas": { - "__typename": "ObjectIndexMetadatasConnection", - "edges": [] - }, + "indexMetadataList": [], "fieldsList": [ { "__typename": "Field", @@ -6007,10 +5908,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = "labelPlural": "Attachments", "description": "An attachment", "icon": "IconFileImport", - "indexMetadatas": { - "__typename": "ObjectIndexMetadatasConnection", - "edges": [] - }, + "indexMetadataList": [], "fieldsList": [ { "__typename": "Field", @@ -6642,10 +6540,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = "labelPlural": "Message Channel Message Associations", "description": "Message Synced with a Message Channel", "icon": "IconMessage", - "indexMetadatas": { - "__typename": "ObjectIndexMetadatasConnection", - "edges": [] - }, + "indexMetadataList": [], "fieldsList": [ { "__typename": "Field", @@ -6942,10 +6837,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = "labelPlural": "View Filters", "description": "(System) View Filters", "icon": "IconFilterBolt", - "indexMetadatas": { - "__typename": "ObjectIndexMetadatasConnection", - "edges": [] - }, + "indexMetadataList": [], "fieldsList": [ { "__typename": "Field", @@ -7261,40 +7153,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = "labelPlural": "Tasks", "description": "A task", "icon": "IconCheckbox", - "indexMetadatas": { - "__typename": "ObjectIndexMetadatasConnection", - "edges": [ - { - "__typename": "IndexEdge", - "node": { - "__typename": "Index", - "id": "4e92884d-139a-451d-8a1f-380078369a88", - "createdAt": "2025-06-09T18:53:47.000Z", - "updatedAt": "2025-06-09T18:53:47.000Z", - "name": "IDX_d01a000cf26e1225d894dc3d364", - "indexWhereClause": null, - "indexType": "GIN", - "isUnique": false, - "indexFieldMetadatas": { - "__typename": "IndexIndexFieldMetadatasConnection", - "edges": [ - { - "__typename": "IndexFieldEdge", - "node": { - "__typename": "IndexField", - "id": "0338d40f-cbb0-4ee8-b024-fe66d623444e", - "createdAt": "2025-06-09T18:53:47.000Z", - "updatedAt": "2025-06-09T18:53:47.000Z", - "order": 0, - "fieldMetadataId": "27443165-8f25-4018-b1d6-f9f44e17efba" - } - } - ] - } - } - } - ] - }, + "indexMetadataList": [], "fieldsList": [ { "__typename": "Field", @@ -7852,10 +7711,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = "labelPlural": "Task Targets", "description": "A task target", "icon": "IconCheckbox", - "indexMetadatas": { - "__typename": "ObjectIndexMetadatasConnection", - "edges": [] - }, + "indexMetadataList": [], "fieldsList": [ { "__typename": "Field", @@ -8324,10 +8180,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = "labelPlural": "Calendar events", "description": "Calendar events", "icon": "IconCalendar", - "indexMetadatas": { - "__typename": "ObjectIndexMetadatasConnection", - "edges": [] - }, + "indexMetadataList": [], "fieldsList": [ { "__typename": "Field", @@ -8798,10 +8651,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = "labelPlural": "Blocklists", "description": "Blocklist", "icon": "IconForbid2", - "indexMetadatas": { - "__typename": "ObjectIndexMetadatasConnection", - "edges": [] - }, + "indexMetadataList": [], "fieldsList": [ { "__typename": "Field", @@ -9002,69 +8852,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = "labelPlural": "People", "description": "A person", "icon": "IconUser", - "indexMetadatas": { - "__typename": "ObjectIndexMetadatasConnection", - "edges": [ - { - "__typename": "IndexEdge", - "node": { - "__typename": "Index", - "id": "4d799743-3cb0-4112-a497-452d9c0e6cbf", - "createdAt": "2025-06-09T18:53:47.000Z", - "updatedAt": "2025-06-09T18:53:47.000Z", - "name": "IDX_bbd7aec1976fc684a0a5e4816c9", - "indexWhereClause": null, - "indexType": "GIN", - "isUnique": false, - "indexFieldMetadatas": { - "__typename": "IndexIndexFieldMetadatasConnection", - "edges": [ - { - "__typename": "IndexFieldEdge", - "node": { - "__typename": "IndexField", - "id": "c8156533-4e84-44ff-a2f5-838588041df9", - "createdAt": "2025-06-09T18:53:47.000Z", - "updatedAt": "2025-06-09T18:53:47.000Z", - "order": 0, - "fieldMetadataId": "28abc54f-5d86-4d54-8e65-5b44b9249153" - } - } - ] - } - } - }, - { - "__typename": "IndexEdge", - "node": { - "__typename": "Index", - "id": "6d3629ae-2d71-4349-b94b-c2baf736a7f7", - "createdAt": "2025-06-09T18:53:47.000Z", - "updatedAt": "2025-06-09T18:53:47.000Z", - "name": "IDX_UNIQUE_87914cd3ce963115f8cb943e2ac", - "indexWhereClause": null, - "indexType": "BTREE", - "isUnique": true, - "indexFieldMetadatas": { - "__typename": "IndexIndexFieldMetadatasConnection", - "edges": [ - { - "__typename": "IndexFieldEdge", - "node": { - "__typename": "IndexField", - "id": "a33a4d19-3a59-4d01-bd82-787172be4c4f", - "createdAt": "2025-06-09T18:53:47.000Z", - "updatedAt": "2025-06-09T18:53:47.000Z", - "order": 0, - "fieldMetadataId": "d49dcd4e-9565-4a11-99ac-c6e278fc028b" - } - } - ] - } - } - } - ] - }, + "indexMetadataList": [], "fieldsList": [ { "__typename": "Field", @@ -10013,80 +9801,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = "labelPlural": "Opportunities", "description": "An opportunity", "icon": "IconTargetArrow", - "indexMetadatas": { - "__typename": "ObjectIndexMetadatasConnection", - "edges": [ - { - "__typename": "IndexEdge", - "node": { - "__typename": "Index", - "id": "517dbbfa-650d-4d6b-b7a6-35ff3c10637d", - "createdAt": "2025-06-09T18:53:47.000Z", - "updatedAt": "2025-06-09T18:53:47.000Z", - "name": "IDX_9f96d65260c4676faac27cb6bf3", - "indexWhereClause": null, - "indexType": "GIN", - "isUnique": false, - "indexFieldMetadatas": { - "__typename": "IndexIndexFieldMetadatasConnection", - "edges": [ - { - "__typename": "IndexFieldEdge", - "node": { - "__typename": "IndexField", - "id": "815a1083-69db-486b-b175-22e80d0a99f2", - "createdAt": "2025-06-09T18:53:47.000Z", - "updatedAt": "2025-06-09T18:53:47.000Z", - "order": 0, - "fieldMetadataId": "e918212c-7546-4d09-a4f2-b1718976d451" - } - } - ] - } - } - }, - { - "__typename": "IndexEdge", - "node": { - "__typename": "Index", - "id": "5eaf9196-e14f-423c-97b6-5911a2ea6ef0", - "createdAt": "2025-06-09T18:53:47.000Z", - "updatedAt": "2025-06-09T18:53:47.000Z", - "name": "IDX_4f469d3a7ee08aefdc099836364", - "indexWhereClause": null, - "indexType": "BTREE", - "isUnique": false, - "indexFieldMetadatas": { - "__typename": "IndexIndexFieldMetadatasConnection", - "edges": [ - { - "__typename": "IndexFieldEdge", - "node": { - "__typename": "IndexField", - "id": "d1ceb012-d59c-4965-97d4-a8c8655e6cca", - "createdAt": "2025-06-09T18:53:47.000Z", - "updatedAt": "2025-06-09T18:53:47.000Z", - "order": 0, - "fieldMetadataId": "77fccf84-50d7-40d0-a097-564ceb3e8433" - } - }, - { - "__typename": "IndexFieldEdge", - "node": { - "__typename": "IndexField", - "id": "0ebf386b-65ca-48a9-bfef-afb2d9df8c75", - "createdAt": "2025-06-09T18:53:47.000Z", - "updatedAt": "2025-06-09T18:53:47.000Z", - "order": 1, - "fieldMetadataId": "5398e336-835f-467b-94da-4a8869456dfd" - } - } - ] - } - } - } - ] - }, + "indexMetadataList": [], "fieldsList": [ { "__typename": "Field", @@ -10735,40 +10450,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = "labelPlural": "Pets", "description": null, "icon": "IconCat", - "indexMetadatas": { - "__typename": "ObjectIndexMetadatasConnection", - "edges": [ - { - "__typename": "IndexEdge", - "node": { - "__typename": "Index", - "id": "42d09c88-b63a-4249-91bb-0db7fd653749", - "createdAt": "2025-06-09T18:53:51.353Z", - "updatedAt": "2025-06-09T18:53:51.353Z", - "name": "IDX_82c02a6c94da4f260020dfb54b9", - "indexWhereClause": null, - "indexType": "GIN", - "isUnique": false, - "indexFieldMetadatas": { - "__typename": "IndexIndexFieldMetadatasConnection", - "edges": [ - { - "__typename": "IndexFieldEdge", - "node": { - "__typename": "IndexField", - "id": "cd98958f-f90d-48b0-8697-73613103caa3", - "createdAt": "2025-06-09T18:53:51.353Z", - "updatedAt": "2025-06-09T18:53:51.353Z", - "order": 0, - "fieldMetadataId": "0306bb8e-b9c2-4ffe-9395-60a053110018" - } - } - ] - } - } - } - ] - }, + "indexMetadataList": [], "fieldsList": [ { "__typename": "Field", @@ -11688,10 +11370,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = "labelPlural": "WorkflowAutomatedTriggers", "description": "A workflow automated trigger", "icon": "IconSettingsAutomation", - "indexMetadatas": { - "__typename": "ObjectIndexMetadatasConnection", - "edges": [] - }, + "indexMetadataList": [], "fieldsList": [ { "__typename": "Field", @@ -11924,69 +11603,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = "labelPlural": "Companies", "description": "A company", "icon": "IconBuildingSkyscraper", - "indexMetadatas": { - "__typename": "ObjectIndexMetadatasConnection", - "edges": [ - { - "__typename": "IndexEdge", - "node": { - "__typename": "Index", - "id": "7471efe7-2fa4-4496-8403-cf3d6e1f4d76", - "createdAt": "2025-06-09T18:53:47.000Z", - "updatedAt": "2025-06-09T18:53:47.000Z", - "name": "IDX_UNIQUE_2a32339058d0b6910b0834ddf81", - "indexWhereClause": null, - "indexType": "BTREE", - "isUnique": true, - "indexFieldMetadatas": { - "__typename": "IndexIndexFieldMetadatasConnection", - "edges": [ - { - "__typename": "IndexFieldEdge", - "node": { - "__typename": "IndexField", - "id": "1173c55d-3d5e-4c7a-9ea1-e1c2aca9c39b", - "createdAt": "2025-06-09T18:53:47.000Z", - "updatedAt": "2025-06-09T18:53:47.000Z", - "order": 0, - "fieldMetadataId": "a710c97a-565d-4868-be27-fa846be32021" - } - } - ] - } - } - }, - { - "__typename": "IndexEdge", - "node": { - "__typename": "Index", - "id": "f42775a0-8f04-4123-81f4-f080ef14ceb9", - "createdAt": "2025-06-09T18:53:47.000Z", - "updatedAt": "2025-06-09T18:53:47.000Z", - "name": "IDX_fb1f4905546cfc6d70a971c76f7", - "indexWhereClause": null, - "indexType": "GIN", - "isUnique": false, - "indexFieldMetadatas": { - "__typename": "IndexIndexFieldMetadatasConnection", - "edges": [ - { - "__typename": "IndexFieldEdge", - "node": { - "__typename": "IndexField", - "id": "cbd8e127-c06a-4f2c-929f-bee3d2fe8b2c", - "createdAt": "2025-06-09T18:53:47.000Z", - "updatedAt": "2025-06-09T18:53:47.000Z", - "order": 0, - "fieldMetadataId": "d69d9ca9-df3f-400b-87c0-ef09fa250f0c" - } - } - ] - } - } - } - ] - }, + "indexMetadataList": [], "fieldsList": [ { "__typename": "Field", @@ -12860,10 +12477,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = "labelPlural": "Connected Accounts", "description": "A connected account", "icon": "IconAt", - "indexMetadatas": { - "__typename": "ObjectIndexMetadatasConnection", - "edges": [] - }, + "indexMetadataList": [], "fieldsList": [ { "__typename": "Field", @@ -13296,40 +12910,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = "labelPlural": "Notes", "description": "A note", "icon": "IconNotes", - "indexMetadatas": { - "__typename": "ObjectIndexMetadatasConnection", - "edges": [ - { - "__typename": "IndexEdge", - "node": { - "__typename": "Index", - "id": "d7aa0d37-d4e2-455f-bbb9-201c03e876dd", - "createdAt": "2025-06-09T18:53:47.000Z", - "updatedAt": "2025-06-09T18:53:47.000Z", - "name": "IDX_f20de8d7fc74a405e4083051275", - "indexWhereClause": null, - "indexType": "GIN", - "isUnique": false, - "indexFieldMetadatas": { - "__typename": "IndexIndexFieldMetadatasConnection", - "edges": [ - { - "__typename": "IndexFieldEdge", - "node": { - "__typename": "IndexField", - "id": "53699c8e-22dc-4ade-a144-189162ac63b7", - "createdAt": "2025-06-09T18:53:47.000Z", - "updatedAt": "2025-06-09T18:53:47.000Z", - "order": 0, - "fieldMetadataId": "ac77bf26-535c-4c88-8735-97702866de25" - } - } - ] - } - } - } - ] - }, + "indexMetadataList": [], "fieldsList": [ { "__typename": "Field", @@ -13773,10 +13354,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = "labelPlural": "View Fields", "description": "(System) View Fields", "icon": "IconTag", - "indexMetadatas": { - "__typename": "ObjectIndexMetadatasConnection", - "edges": [] - }, + "indexMetadataList": [], "fieldsList": [ { "__typename": "Field", @@ -14135,10 +13713,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = "labelPlural": "Workflow Versions", "description": "A workflow version", "icon": "IconVersions", - "indexMetadatas": { - "__typename": "ObjectIndexMetadatasConnection", - "edges": [] - }, + "indexMetadataList": [], "fieldsList": [ { "__typename": "Field", @@ -14585,10 +14160,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = "labelPlural": "Calendar event participants", "description": "Calendar event participants", "icon": "IconCalendar", - "indexMetadatas": { - "__typename": "ObjectIndexMetadatasConnection", - "edges": [] - }, + "indexMetadataList": [], "fieldsList": [ { "__typename": "Field", @@ -14970,10 +14542,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = "labelPlural": "Workflows", "description": "A workflow", "icon": "IconSettingsAutomation", - "indexMetadatas": { - "__typename": "ObjectIndexMetadatasConnection", - "edges": [] - }, + "indexMetadataList": [], "fieldsList": [ { "__typename": "Field", @@ -15460,40 +15029,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = "labelPlural": "Workspace Members", "description": "A workspace member", "icon": "IconUserCircle", - "indexMetadatas": { - "__typename": "ObjectIndexMetadatasConnection", - "edges": [ - { - "__typename": "IndexEdge", - "node": { - "__typename": "Index", - "id": "827348bd-da39-40d9-85c8-09370b9ec58e", - "createdAt": "2025-06-09T18:53:47.000Z", - "updatedAt": "2025-06-09T18:53:47.000Z", - "name": "IDX_e47451872f70c8f187a6b460ac7", - "indexWhereClause": null, - "indexType": "GIN", - "isUnique": false, - "indexFieldMetadatas": { - "__typename": "IndexIndexFieldMetadatasConnection", - "edges": [ - { - "__typename": "IndexFieldEdge", - "node": { - "__typename": "IndexField", - "id": "ebbf44cc-2880-4b99-b817-2851e131b13f", - "createdAt": "2025-06-09T18:53:47.000Z", - "updatedAt": "2025-06-09T18:53:47.000Z", - "order": 0, - "fieldMetadataId": "7e389f1a-c377-4846-92d9-3e38697ee198" - } - } - ] - } - } - } - ] - }, + "indexMetadataList": [], "fieldsList": [ { "__typename": "Field", @@ -16329,10 +15865,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = "labelPlural": "Webhooks", "description": "A webhook", "icon": "IconRobot", - "indexMetadatas": { - "__typename": "ObjectIndexMetadatasConnection", - "edges": [] - }, + "indexMetadataList": [], "fieldsList": [ { "__typename": "Field", @@ -16537,10 +16070,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = "labelPlural": "Message Folders", "description": "Folder for Message Channel", "icon": "IconFolder", - "indexMetadatas": { - "__typename": "ObjectIndexMetadatasConnection", - "edges": [] - }, + "indexMetadataList": [], "fieldsList": [ { "__typename": "Field", @@ -16751,10 +16281,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = "labelPlural": "View Sorts", "description": "(System) View Sorts", "icon": "IconArrowsSort", - "indexMetadatas": { - "__typename": "ObjectIndexMetadatasConnection", - "edges": [] - }, + "indexMetadataList": [], "fieldsList": [ { "__typename": "Field", @@ -16965,10 +16492,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = "labelPlural": "Calendar Channel Event Associations", "description": "Calendar Channel Event Associations", "icon": "IconCalendar", - "indexMetadatas": { - "__typename": "ObjectIndexMetadatasConnection", - "edges": [] - }, + "indexMetadataList": [], "fieldsList": [ { "__typename": "Field", @@ -17229,10 +16753,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = "labelPlural": "Calendar Channels", "description": "Calendar Channels", "icon": "IconCalendar", - "indexMetadatas": { - "__typename": "ObjectIndexMetadatasConnection", - "edges": [] - }, + "indexMetadataList": [], "fieldsList": [ { "__typename": "Field", @@ -17803,10 +17324,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = "labelPlural": "Message Channels", "description": "Message Channels", "icon": "IconMessage", - "indexMetadatas": { - "__typename": "ObjectIndexMetadatasConnection", - "edges": [] - }, + "indexMetadataList": [], "fieldsList": [ { "__typename": "Field", @@ -18503,10 +18021,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = "labelPlural": "Messages", "description": "A message sent or received through a messaging channel (email, chat, etc.)", "icon": "IconMessage", - "indexMetadatas": { - "__typename": "ObjectIndexMetadatasConnection", - "edges": [] - }, + "indexMetadataList": [], "fieldsList": [ { "__typename": "Field", diff --git a/packages/twenty-front/src/testing/mock-data/generatedMockObjectMetadataItems.ts b/packages/twenty-front/src/testing/mock-data/generatedMockObjectMetadataItems.ts index eb45cf686..618067c54 100644 --- a/packages/twenty-front/src/testing/mock-data/generatedMockObjectMetadataItems.ts +++ b/packages/twenty-front/src/testing/mock-data/generatedMockObjectMetadataItems.ts @@ -10,17 +10,16 @@ export const generatedMockObjectMetadataItems: ObjectMetadataItem[] = edge.node.labelIdentifierFieldMetadataId, ); - const { fieldsList, ...objectWithoutFieldsList } = edge.node; + const { fieldsList, indexMetadataList, ...objectWithoutFieldsList } = + edge.node; return { ...objectWithoutFieldsList, fields: fieldsList, labelIdentifierFieldMetadataId, - indexMetadatas: edge.node.indexMetadatas.edges.map((index) => ({ - ...index.node, - indexFieldMetadatas: index.node.indexFieldMetadatas?.edges.map( - (indexField) => indexField.node, - ), + indexMetadatas: indexMetadataList.map((index) => ({ + ...index, + indexFieldMetadatas: [], })), }; }); diff --git a/packages/twenty-server/src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner.ts b/packages/twenty-server/src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner.ts index 53c7dd75f..6258c8f34 100644 --- a/packages/twenty-server/src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner.ts +++ b/packages/twenty-server/src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner.ts @@ -140,7 +140,6 @@ export abstract class ActiveOrSuspendedWorkspacesMigrationCommandRunner< const dataSource = await this.twentyORMGlobalManager.getDataSourceForWorkspace({ workspaceId, - shouldFailIfMetadataNotFound: false, }); await this.runOnWorkspace({ diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/common/1750673748111-remove-relation-metadata.ts b/packages/twenty-server/src/database/typeorm/core/migrations/common/1750673748111-remove-relation-metadata.ts new file mode 100644 index 000000000..02eb9c95e --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/common/1750673748111-remove-relation-metadata.ts @@ -0,0 +1,30 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RemoveRelationMetadata1750673748111 implements MigrationInterface { + name = 'RemoveRelationMetadata1750673748111'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE INDEX "IDX_DATA_SOURCE_WORKSPACE_ID_CREATED_AT" ON "core"."dataSource" ("workspaceId", "createdAt") `, + ); + await queryRunner.query( + `ALTER TABLE "core"."relationMetadata" DROP CONSTRAINT IF EXISTS "FK_9dea8f90d04edbbf9c541a95c3b"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."relationMetadata" DROP CONSTRAINT IF EXISTS "FK_3deb257254145a3bdde9575e7d6"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."relationMetadata" DROP CONSTRAINT IF EXISTS "FK_0f781f589e5a527b8f3d3a4b824"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."relationMetadata" DROP CONSTRAINT IF EXISTS "FK_f2a0acd3a548ee446a1a35df44d"`, + ); + await queryRunner.query(`DROP TABLE IF EXISTS "core"."relationMetadata"`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `DROP INDEX "core"."IDX_DATA_SOURCE_WORKSPACE_ID_CREATED_AT"`, + ); + } +} 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 f154856dc..47229427c 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 @@ -307,22 +307,34 @@ export const objectMetadataItemMock = { export const objectMetadataMapItemMock = { id: 'mockObjectId', + icon: 'Icon123', nameSingular: 'objectName', namePlural: 'objectsName', - fields, fieldsById: fields.reduce((acc, field) => { // @ts-expect-error legacy noImplicitAny acc[field.id] = field; return acc; }, {}), - fieldsByName: fields.reduce((acc, field) => { + fieldIdByName: fields.reduce((acc, field) => { // @ts-expect-error legacy noImplicitAny acc[field.name] = field; return acc; }, {}), -} as ObjectMetadataItemWithFieldMaps; + fieldIdByJoinColumnName: {}, + labelSingular: 'Object', + labelPlural: 'Objects', + workspaceId: 'mockWorkspaceId', + isCustom: false, + isSystem: false, + targetTableName: '', + indexMetadatas: [], + isActive: true, + isRemote: false, + isAuditLogged: false, + isSearchable: false, +} satisfies ObjectMetadataItemWithFieldMaps; export const objectMetadataMapsMock = { byId: { 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 bc92a7584..8501a583f 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 @@ -3,10 +3,11 @@ import { FieldMetadataType } from 'twenty-shared/types'; import { WorkspaceEntityDuplicateCriteria } from 'src/engine/api/graphql/workspace-query-builder/types/workspace-entity-duplicate-criteria.type'; import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -export const mockPersonObjectMetadata = ( +export const mockPersonObjectMetadataWithFieldMaps = ( duplicateCriteria: WorkspaceEntityDuplicateCriteria[], ): ObjectMetadataItemWithFieldMaps => ({ id: '', + icon: 'Icon123', standardId: '', nameSingular: 'person', namePlural: 'people', @@ -24,12 +25,16 @@ export const mockPersonObjectMetadata = ( labelIdentifierFieldMetadataId: '', imageIdentifierFieldMetadataId: '', workspaceId: '', - fields: [], indexMetadatas: [], - fieldsById: {}, - fieldsByJoinColumnName: {}, - fieldsByName: { - name: { + fieldIdByName: { + name: 'name-id', + emails: 'emails-id', + linkedinLink: 'linkedinLink-id', + jobTitle: 'jobTitle-id', + }, + fieldIdByJoinColumnName: {}, + fieldsById: { + 'name-id': { id: '', objectMetadataId: '', type: FieldMetadataType.FULL_NAME, @@ -44,8 +49,11 @@ export const mockPersonObjectMetadata = ( isNullable: true, isUnique: false, workspaceId: '', + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), }, - emails: { + 'emails-id': { id: '', objectMetadataId: '', type: FieldMetadataType.EMAILS, @@ -57,9 +65,13 @@ export const mockPersonObjectMetadata = ( }, description: 'Contact’s Emails', isCustom: false, + isNullable: true, workspaceId: '', + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), }, - linkedinLink: { + 'linkedinLink-id': { id: '', objectMetadataId: '', type: FieldMetadataType.LINKS, @@ -75,8 +87,11 @@ export const mockPersonObjectMetadata = ( isNullable: true, isUnique: false, workspaceId: '', + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), }, - jobTitle: { + 'jobTitle-id': { id: '', objectMetadataId: '', type: FieldMetadataType.TEXT, @@ -88,6 +103,9 @@ export const mockPersonObjectMetadata = ( isNullable: false, isUnique: false, workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419', + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), }, }, }); diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception.ts index 7075173d9..7acac330f 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception.ts @@ -15,6 +15,7 @@ export enum GraphqlQueryRunnerExceptionCode { UNSUPPORTED_OPERATOR = 'UNSUPPORTED_OPERATOR', ARGS_CONFLICT = 'ARGS_CONFLICT', FIELD_NOT_FOUND = 'FIELD_NOT_FOUND', + MISSING_SYSTEM_FIELD = 'MISSING_SYSTEM_FIELD', OBJECT_METADATA_NOT_FOUND = 'OBJECT_METADATA_NOT_FOUND', RECORD_NOT_FOUND = 'RECORD_NOT_FOUND', INVALID_ARGS_FIRST = 'INVALID_ARGS_FIRST', 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 1a24eb902..5e5e1daa6 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,25 +2,19 @@ import { Brackets, NotBrackets, WhereExpressionBuilder } from 'typeorm'; import { ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; -import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; +import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder'; import { GraphqlQueryFilterFieldParser } from './graphql-query-filter-field.parser'; export class GraphqlQueryFilterConditionParser { - private fieldMetadataMapByName: FieldMetadataMap; - private fieldMetadataMapByJoinColumnName: FieldMetadataMap; + private objectMetadataMapItem: ObjectMetadataItemWithFieldMaps; private queryFilterFieldParser: GraphqlQueryFilterFieldParser; - constructor( - fieldMetadataMapByName: FieldMetadataMap, - fieldMetadataMapByJoinColumnName: FieldMetadataMap, - ) { - this.fieldMetadataMapByName = fieldMetadataMapByName; - this.fieldMetadataMapByJoinColumnName = fieldMetadataMapByJoinColumnName; + constructor(objectMetadataMapItem: ObjectMetadataItemWithFieldMaps) { + this.objectMetadataMapItem = objectMetadataMapItem; this.queryFilterFieldParser = new GraphqlQueryFilterFieldParser( - this.fieldMetadataMapByName, - this.fieldMetadataMapByJoinColumnName, + this.objectMetadataMapItem, ); } 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 1c963a818..209344380 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 @@ -10,21 +10,16 @@ import { import { computeWhereConditionParts } from 'src/engine/api/graphql/graphql-query-runner/utils/compute-where-condition-parts'; import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; +import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory'; const ARRAY_OPERATORS = ['in', 'contains', 'notContains']; export class GraphqlQueryFilterFieldParser { - private fieldMetadataMapByName: FieldMetadataMap; - private fieldMetadataMapByJoinColumnName: FieldMetadataMap; + private objectMetadataMapItem: ObjectMetadataItemWithFieldMaps; - constructor( - fieldMetadataMapByName: FieldMetadataMap, - fieldMetadataMapByJoinColumnName: FieldMetadataMap, - ) { - this.fieldMetadataMapByName = fieldMetadataMapByName; - this.fieldMetadataMapByJoinColumnName = fieldMetadataMapByJoinColumnName; + constructor(objectMetadataMapItem: ObjectMetadataItemWithFieldMaps) { + this.objectMetadataMapItem = objectMetadataMapItem; } public parse( @@ -35,9 +30,12 @@ export class GraphqlQueryFilterFieldParser { filterValue: any, isFirst = false, ): void { + const fieldMetadataId = + this.objectMetadataMapItem.fieldIdByName[`${key}`] || + this.objectMetadataMapItem.fieldIdByJoinColumnName[`${key}`]; + const fieldMetadata = - this.fieldMetadataMapByName[`${key}`] || - this.fieldMetadataMapByJoinColumnName[`${key}`]; + this.objectMetadataMapItem.fieldsById[fieldMetadataId]; if (!fieldMetadata) { throw new Error(`Field metadata not found for field: ${key}`); diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-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 3fe947109..ac432ef1c 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 @@ -12,14 +12,14 @@ import { } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; +import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory'; export class GraphqlQueryOrderFieldParser { - private fieldMetadataMapByName: FieldMetadataMap; + private objectMetadataMapItem: ObjectMetadataItemWithFieldMaps; - constructor(fieldMetadataMapByName: FieldMetadataMap) { - this.fieldMetadataMapByName = fieldMetadataMapByName; + constructor(objectMetadataMapItem: ObjectMetadataItemWithFieldMaps) { + this.objectMetadataMapItem = objectMetadataMapItem; } parse( @@ -30,7 +30,9 @@ export class GraphqlQueryOrderFieldParser { return orderBy.reduce( (acc, item) => { Object.entries(item).forEach(([key, value]) => { - const fieldMetadata = this.fieldMetadataMapByName[key]; + const fieldMetadataId = this.objectMetadataMapItem.fieldIdByName[key]; + const fieldMetadata = + this.objectMetadataMapItem.fieldsById[fieldMetadataId]; if (!fieldMetadata || value === undefined) { throw new GraphqlQueryRunnerException( 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 ad08692fb..8fde11659 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,20 @@ -import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; - import { GraphqlQuerySelectedFieldsResult } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser'; import { AggregationField, getAvailableAggregationsFromObjectFields, } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util'; +import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; export class GraphqlQuerySelectedFieldsAggregateParser { parse( // eslint-disable-next-line @typescript-eslint/no-explicit-any graphqlSelectedFields: Partial>, - fieldMetadataMapByName: Record, + objectMetadataMapItem: ObjectMetadataItemWithFieldMaps, accumulator: GraphqlQuerySelectedFieldsResult, ): void { const availableAggregations: Record = getAvailableAggregationsFromObjectFields( - Object.values(fieldMetadataMapByName), + Object.values(objectMetadataMapItem.fieldsById), ); for (const selectedField of Object.keys(graphqlSelectedFields)) { 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 f747e07ec..5cc057320 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 @@ -32,11 +32,13 @@ export class GraphqlQuerySelectedFieldsRelationParser { this.objectMetadataMaps, ); - const targetFields = targetObjectMetadata.fieldsByName; const fieldParser = new GraphqlQuerySelectedFieldsParser( this.objectMetadataMaps, ); - const relationAccumulator = fieldParser.parse(fieldValue, targetFields); + const relationAccumulator = fieldParser.parse( + fieldValue, + targetObjectMetadata, + ); accumulator.select[fieldKey] = { id: true, 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 ee6107dac..8b61a0d51 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,6 +6,7 @@ import { GraphqlQuerySelectedFieldsAggregateParser } from 'src/engine/api/graphq import { GraphqlQuerySelectedFieldsRelationParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-relation.parser'; import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; +import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory'; import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; @@ -32,7 +33,7 @@ export class GraphqlQuerySelectedFieldsParser { parse( // eslint-disable-next-line @typescript-eslint/no-explicit-any graphqlSelectedFields: Partial>, - fieldMetadataMapByName: Record, + objectMetadataMapItem: ObjectMetadataItemWithFieldMaps, ): GraphqlQuerySelectedFieldsResult { const accumulator: GraphqlQuerySelectedFieldsResult = { select: {}, @@ -43,7 +44,7 @@ export class GraphqlQuerySelectedFieldsParser { if (this.isRootConnection(graphqlSelectedFields)) { this.parseConnectionField( graphqlSelectedFields, - fieldMetadataMapByName, + objectMetadataMapItem, accumulator, ); @@ -52,13 +53,13 @@ export class GraphqlQuerySelectedFieldsParser { this.aggregateParser.parse( graphqlSelectedFields, - fieldMetadataMapByName, + objectMetadataMapItem, accumulator, ); this.parseRecordField( graphqlSelectedFields, - fieldMetadataMapByName, + objectMetadataMapItem, accumulator, ); @@ -68,13 +69,16 @@ export class GraphqlQuerySelectedFieldsParser { private parseRecordField( // eslint-disable-next-line @typescript-eslint/no-explicit-any graphqlSelectedFields: Partial>, - fieldMetadataMapByName: Record, + objectMetadataMapItem: ObjectMetadataItemWithFieldMaps, accumulator: GraphqlQuerySelectedFieldsResult, ): void { for (const [fieldKey, fieldValue] of Object.entries( graphqlSelectedFields, )) { - const fieldMetadata = fieldMetadataMapByName[fieldKey]; + const fieldMetadata = + objectMetadataMapItem.fieldsById[ + objectMetadataMapItem.fieldIdByName[fieldKey] + ]; if (!fieldMetadata) { continue; @@ -103,18 +107,18 @@ export class GraphqlQuerySelectedFieldsParser { private parseConnectionField( // eslint-disable-next-line @typescript-eslint/no-explicit-any graphqlSelectedFields: Partial>, - fieldMetadataMapByName: Record, + objectMetadataMapItem: ObjectMetadataItemWithFieldMaps, accumulator: GraphqlQuerySelectedFieldsResult, ): void { this.aggregateParser.parse( graphqlSelectedFields, - fieldMetadataMapByName, + objectMetadataMapItem, accumulator, ); const node = graphqlSelectedFields.edges.node; - this.parseRecordField(node, fieldMetadataMapByName, accumulator); + this.parseRecordField(node, objectMetadataMapItem, accumulator); } private isRootConnection( 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 1cf5eb4c5..3bd09a159 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 @@ -15,33 +15,29 @@ import { GraphqlQuerySelectedFieldsParser, GraphqlQuerySelectedFieldsResult, } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser'; -import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util'; import { WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder'; export class GraphqlQueryParser { - private fieldMetadataMapByName: FieldMetadataMap; - private fieldMetadataMapByJoinColumnName: FieldMetadataMap; + private objectMetadataMapItem: ObjectMetadataItemWithFieldMaps; private objectMetadataMaps: ObjectMetadataMaps; private filterConditionParser: GraphqlQueryFilterConditionParser; private orderFieldParser: GraphqlQueryOrderFieldParser; constructor( - fieldMetadataMapByName: FieldMetadataMap, - fieldMetadataMapByJoinColumnName: FieldMetadataMap, + objectMetadataMapItem: ObjectMetadataItemWithFieldMaps, objectMetadataMaps: ObjectMetadataMaps, ) { + this.objectMetadataMapItem = objectMetadataMapItem; this.objectMetadataMaps = objectMetadataMaps; - this.fieldMetadataMapByName = fieldMetadataMapByName; - this.fieldMetadataMapByJoinColumnName = fieldMetadataMapByJoinColumnName; + this.filterConditionParser = new GraphqlQueryFilterConditionParser( - this.fieldMetadataMapByName, - this.fieldMetadataMapByJoinColumnName, + this.objectMetadataMapItem, ); this.orderFieldParser = new GraphqlQueryOrderFieldParser( - this.fieldMetadataMapByName, + this.objectMetadataMapItem, ); } @@ -120,12 +116,12 @@ export class GraphqlQueryParser { // eslint-disable-next-line @typescript-eslint/no-explicit-any graphqlSelectedFields: Partial>, ): GraphqlQuerySelectedFieldsResult { - const parentFields = getObjectMetadataMapItemByNameSingular( + const objectMetadataMapItem = getObjectMetadataMapItemByNameSingular( this.objectMetadataMaps, parentObjectMetadata.nameSingular, - )?.fieldsByName; + ); - if (!parentFields) { + if (!objectMetadataMapItem) { throw new GraphqlQueryRunnerException( `Could not find object metadata for ${parentObjectMetadata.nameSingular}`, GraphqlQueryRunnerExceptionCode.OBJECT_METADATA_NOT_FOUND, @@ -136,6 +132,9 @@ export class GraphqlQueryParser { this.objectMetadataMaps, ); - return selectedFieldsParser.parse(graphqlSelectedFields, parentFields); + return selectedFieldsParser.parse( + graphqlSelectedFields, + objectMetadataMapItem, + ); } } 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 40ac4a419..33a45e255 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 @@ -168,7 +168,8 @@ export class ObjectRecordsToGraphqlConnectionHelper { const processedObjectRecord: Record = {}; for (const [key, value] of Object.entries(objectRecord)) { - const fieldMetadata = objectMetadata.fieldsByName[key]; + const fieldMetadataId = objectMetadata.fieldIdByName[key]; + const fieldMetadata = objectMetadata.fieldsById[fieldMetadataId]; if (!fieldMetadata) { processedObjectRecord[key] = value; 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 75e73a1f7..03bad6365 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 @@ -103,8 +103,10 @@ export class ProcessNestedRelationsV2Helper { shouldBypassPermissionChecks: boolean; roleId?: string; }): Promise { + const sourceFieldMetadataId = + parentObjectMetadataItem.fieldIdByName[sourceFieldName]; const sourceFieldMetadata = - parentObjectMetadataItem.fieldsByName[sourceFieldName]; + parentObjectMetadataItem.fieldsById[sourceFieldMetadataId]; if ( !isFieldMetadataInterfaceOfType( @@ -219,8 +221,11 @@ export class ProcessNestedRelationsV2Helper { parentObjectMetadataItem: ObjectMetadataItemWithFieldMaps; sourceFieldName: string; }) { + const targetFieldMetadataId = + parentObjectMetadataItem.fieldIdByName[sourceFieldName]; const targetFieldMetadata = - parentObjectMetadataItem.fieldsByName[sourceFieldName]; + parentObjectMetadataItem.fieldsById[targetFieldMetadataId]; + const targetObjectMetadata = getTargetObjectMetadataOrThrow( targetFieldMetadata, objectMetadataMaps, diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service.ts index e55e3feed..bf77b299e 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service.ts @@ -93,7 +93,6 @@ export abstract class GraphqlQueryBaseResolverService< const workspaceDataSource = await this.twentyORMGlobalManager.getDataSourceForWorkspace({ workspaceId: workspace.id, - shouldFailIfMetadataNotFound: false, }); const featureFlagsMap = workspaceDataSource.featureFlagMap; @@ -132,8 +131,7 @@ export abstract class GraphqlQueryBaseResolverService< ); const graphqlQueryParser = new GraphqlQueryParser( - objectMetadataItemWithFieldMaps.fieldsByName, - objectMetadataItemWithFieldMaps.fieldsByJoinColumnName, + objectMetadataItemWithFieldMaps, options.objectMetadataMaps, ); diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service.ts index 6009b2953..71752b667 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service.ts @@ -12,6 +12,10 @@ import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/int import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; import { CreateManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; +import { + GraphqlQueryRunnerException, + GraphqlQueryRunnerExceptionCode, +} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; @@ -19,6 +23,7 @@ import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-meta 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 { getObjectMetadataFromObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps'; import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; import { formatData } from 'src/engine/twenty-orm/utils/format-data.util'; import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; @@ -128,7 +133,7 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol fullPath: string; column: string; }[] { - return objectMetadataItemWithFieldMaps.fields + return Object.values(objectMetadataItemWithFieldMaps.fieldsById) .filter((field) => field.isUnique || field.name === 'id') .flatMap((field) => { const compositeType = compositeTypeDefinitions.get(field.type); @@ -330,7 +335,10 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol records: structuredClone([record]), updatedFields: Object.keys(formattedPartialRecordToUpdate), authContext, - objectMetadataItem: objectMetadataItemWithFieldMaps, + objectMetadataItem: + getObjectMetadataFromObjectMetadataItemWithFieldMaps( + objectMetadataItemWithFieldMaps, + ), }); } } @@ -373,7 +381,9 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol this.apiEventEmitterService.emitCreateEvents({ records: structuredClone(formattedInsertedRecords), authContext, - objectMetadataItem: objectMetadataItemWithFieldMaps, + objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps( + objectMetadataItemWithFieldMaps, + ), }); } @@ -450,11 +460,19 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol ) { let recordWithoutCreatedByUpdate = record; - if ( - 'createdBy' in record && - objectMetadataItemWithFieldMaps.fieldsByName['createdBy']?.isCustom === - false - ) { + const createdByFieldMetadataId = + objectMetadataItemWithFieldMaps.fieldIdByName['createdBy']; + const createdByFieldMetadata = + objectMetadataItemWithFieldMaps.fieldsById[createdByFieldMetadataId]; + + if (!isDefined(createdByFieldMetadata)) { + throw new GraphqlQueryRunnerException( + `Missing createdBy field metadata for object ${objectMetadataItemWithFieldMaps.nameSingular}`, + GraphqlQueryRunnerExceptionCode.MISSING_SYSTEM_FIELD, + ); + } + + if ('createdBy' in record && createdByFieldMetadata.isCustom === false) { const { createdBy: _createdBy, ...recordWithoutCreatedBy } = record; recordWithoutCreatedByUpdate = recordWithoutCreatedBy; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-one-resolver.service.ts index e9afc18af..cec42860f 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-one-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-one-resolver.service.ts @@ -14,6 +14,7 @@ import { CreateOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; +import { getObjectMetadataFromObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps'; import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; @Injectable() @@ -56,7 +57,9 @@ export class GraphqlQueryCreateOneResolverService extends GraphqlQueryBaseResolv this.apiEventEmitterService.emitCreateEvents({ records: structuredClone(upsertedRecords), authContext, - objectMetadataItem: objectMetadataItemWithFieldMaps, + objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps( + objectMetadataItemWithFieldMaps, + ), }); if (executionArgs.graphqlQuerySelectedFieldsResult.relations) { diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-many-resolver.service.ts index ba918a78c..30032b374 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-many-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-many-resolver.service.ts @@ -13,6 +13,7 @@ import { DeleteManyResolverArgs } from 'src/engine/api/graphql/workspace-resolve import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; +import { getObjectMetadataFromObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps'; import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; import { computeTableName } from 'src/engine/utils/compute-table-name.util'; @@ -58,7 +59,9 @@ export class GraphqlQueryDeleteManyResolverService extends GraphqlQueryBaseResol this.apiEventEmitterService.emitDeletedEvents({ records: structuredClone(formattedDeletedRecords), authContext, - objectMetadataItem: objectMetadataItemWithFieldMaps, + objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps( + objectMetadataItemWithFieldMaps, + ), }); if (executionArgs.graphqlQuerySelectedFieldsResult.relations) { diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-one-resolver.service.ts index 5f26b44c3..4b0ef3a9c 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-one-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-one-resolver.service.ts @@ -18,6 +18,7 @@ import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/g import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; +import { getObjectMetadataFromObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps'; @Injectable() export class GraphqlQueryDeleteOneResolverService extends GraphqlQueryBaseResolverService< @@ -60,7 +61,9 @@ export class GraphqlQueryDeleteOneResolverService extends GraphqlQueryBaseResolv this.apiEventEmitterService.emitDeletedEvents({ records: structuredClone(formattedDeletedRecords), authContext, - objectMetadataItem: objectMetadataItemWithFieldMaps, + objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps( + objectMetadataItemWithFieldMaps, + ), }); if (executionArgs.graphqlQuerySelectedFieldsResult.relations) { diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service.ts index d6f44bdfc..ae23c6dff 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service.ts @@ -11,6 +11,7 @@ import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-qu import { DestroyManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; +import { getObjectMetadataFromObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps'; import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; import { computeTableName } from 'src/engine/utils/compute-table-name.util'; @@ -56,7 +57,9 @@ export class GraphqlQueryDestroyManyResolverService extends GraphqlQueryBaseReso this.apiEventEmitterService.emitDestroyEvents({ records: structuredClone(deletedRecords), authContext, - objectMetadataItem: objectMetadataItemWithFieldMaps, + objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps( + objectMetadataItemWithFieldMaps, + ), }); if (executionArgs.graphqlQuerySelectedFieldsResult.relations) { diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service.ts index 9e86daaaa..6b032f4fa 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service.ts @@ -15,6 +15,7 @@ import { GraphqlQueryRunnerExceptionCode, } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; +import { getObjectMetadataFromObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps'; import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; @Injectable() @@ -56,7 +57,9 @@ export class GraphqlQueryDestroyOneResolverService extends GraphqlQueryBaseResol this.apiEventEmitterService.emitDestroyEvents({ records: structuredClone(deletedRecords), authContext, - objectMetadataItem: objectMetadataItemWithFieldMaps, + objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps( + objectMetadataItemWithFieldMaps, + ), }); if (executionArgs.graphqlQuerySelectedFieldsResult.relations) { diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service.ts index e35d2cdac..0fea44808 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service.ts @@ -21,10 +21,10 @@ import { } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser'; import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; +import { buildDuplicateConditions } from 'src/engine/api/utils/build-duplicate-conditions.utils'; import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util'; import { formatData } from 'src/engine/twenty-orm/utils/format-data.util'; import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; -import { buildDuplicateConditions } from 'src/engine/api/utils/build-duplicate-conditions.utils'; @Injectable() export class GraphqlQueryFindDuplicatesResolverService extends GraphqlQueryBaseResolverService< @@ -56,8 +56,7 @@ export class GraphqlQueryFindDuplicatesResolverService extends GraphqlQueryBaseR } const graphqlQueryParser = new GraphqlQueryParser( - objectMetadataItemWithFieldsMaps?.fieldsByName, - objectMetadataItemWithFieldsMaps?.fieldsByJoinColumnName, + objectMetadataItemWithFieldMaps, objectMetadataMaps, ); diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service.ts index 85fc489ba..cf5173ce2 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service.ts @@ -80,7 +80,7 @@ export class GraphqlQueryFindManyResolverService extends GraphqlQueryBaseResolve const cursorArgFilter = computeCursorArgFilter( cursor, orderByWithIdCondition, - objectMetadataItemWithFieldMaps.fieldsByName, + objectMetadataItemWithFieldMaps, isForwardPagination, ); diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-many-resolver.service.ts index 45c521d78..501d33c2a 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-many-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-many-resolver.service.ts @@ -15,6 +15,7 @@ import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; import { computeTableName } from 'src/engine/utils/compute-table-name.util'; +import { getObjectMetadataFromObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps'; @Injectable() export class GraphqlQueryRestoreManyResolverService extends GraphqlQueryBaseResolverService< @@ -58,7 +59,9 @@ export class GraphqlQueryRestoreManyResolverService extends GraphqlQueryBaseReso this.apiEventEmitterService.emitRestoreEvents({ records: structuredClone(formattedRestoredRecords), authContext, - objectMetadataItem: objectMetadataItemWithFieldMaps, + objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps( + objectMetadataItemWithFieldMaps, + ), }); if (executionArgs.graphqlQuerySelectedFieldsResult.relations) { diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-one-resolver.service.ts index c4c123695..b8dbbde08 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-one-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-one-resolver.service.ts @@ -17,6 +17,7 @@ import { import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; +import { getObjectMetadataFromObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps'; import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; @Injectable() @@ -60,7 +61,9 @@ export class GraphqlQueryRestoreOneResolverService extends GraphqlQueryBaseResol this.apiEventEmitterService.emitRestoreEvents({ records: structuredClone(formattedRestoredRecords), authContext, - objectMetadataItem: objectMetadataItemWithFieldMaps, + objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps( + objectMetadataItemWithFieldMaps, + ), }); if (executionArgs.graphqlQuerySelectedFieldsResult.relations) { diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service.ts index f66d5b518..f1e5e324b 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service.ts @@ -21,6 +21,7 @@ import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/obj import { formatData } from 'src/engine/twenty-orm/utils/format-data.util'; import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; import { computeTableName } from 'src/engine/utils/compute-table-name.util'; +import { getObjectMetadataFromObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps'; @Injectable() export class GraphqlQueryUpdateManyResolverService extends GraphqlQueryBaseResolverService< @@ -94,7 +95,9 @@ export class GraphqlQueryUpdateManyResolverService extends GraphqlQueryBaseResol records: structuredClone(formattedUpdatedRecords), updatedFields: Object.keys(executionArgs.args.data), authContext, - objectMetadataItem: objectMetadataItemWithFieldMaps, + objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps( + objectMetadataItemWithFieldMaps, + ), }); if (executionArgs.graphqlQuerySelectedFieldsResult.relations) { diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service.ts index 5a5775119..3b5d2a40a 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service.ts @@ -18,6 +18,7 @@ import { import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; +import { getObjectMetadataFromObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps'; import { formatData } from 'src/engine/twenty-orm/utils/format-data.util'; import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; @@ -89,7 +90,9 @@ export class GraphqlQueryUpdateOneResolverService extends GraphqlQueryBaseResolv records: structuredClone(formattedUpdatedRecords), updatedFields: Object.keys(executionArgs.args.data), authContext, - objectMetadataItem: objectMetadataItemWithFieldMaps, + objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps( + objectMetadataItemWithFieldMaps, + ), }); if (executionArgs.graphqlQuerySelectedFieldsResult.relations) { diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/__tests__/query-runner-args.factory.spec.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/__tests__/query-runner-args.factory.spec.ts index b207a1cb1..01fbc443c 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/__tests__/query-runner-args.factory.spec.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/__tests__/query-runner-args.factory.spec.ts @@ -23,40 +23,29 @@ describe('QueryRunnerArgsFactory', () => { objectMetadataItemWithFieldMaps: { isCustom: true, nameSingular: 'testNumber', - fields: [ - { + fieldsById: { + 'position-id': { type: FieldMetadataType.POSITION, isCustom: true, name: 'position', }, - { + 'testNumber-id': { type: FieldMetadataType.NUMBER, isCustom: true, name: 'testNumber', }, - { - type: FieldMetadataType.TEXT, - isCustom: true, - name: 'otherField', - }, - ], - fieldsByName: { - position: { - type: FieldMetadataType.POSITION, - isCustom: true, - name: 'position', - }, - testNumber: { - type: FieldMetadataType.NUMBER, - isCustom: true, - name: 'testNumber', - }, - otherField: { + 'otherField-id': { type: FieldMetadataType.TEXT, isCustom: true, name: 'otherField', }, } as unknown as FieldMetadataMap, + fieldIdByName: { + position: 'position-id', + testNumber: 'testNumber-id', + otherField: 'otherField-id', + }, + fieldIdByJoinColumnName: {}, }, } as unknown as WorkspaceQueryRunnerOptions; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory.ts index 0444f2140..4d267cc96 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory.ts @@ -8,7 +8,6 @@ import { QueryResultFieldValue } from 'src/engine/api/graphql/workspace-query-ru import { QueryResultGetterHandlerInterface } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-getter-handler.interface'; import { IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface'; import { IEdge } from 'src/engine/api/graphql/workspace-query-runner/interfaces/edge.interface'; -import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; import { isQueryResultFieldValueAConnection } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/guards/is-query-result-field-value-a-connection.guard'; import { isQueryResultFieldValueANestedRecordArray } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/guards/is-query-result-field-value-a-nested-record-array.guard'; @@ -22,6 +21,7 @@ import { CompositeInputTypeDefinitionFactory } from 'src/engine/api/graphql/work import { FileService } from 'src/engine/core-modules/file/services/file.service'; import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { isFieldMetadataInterfaceOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; +import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; // 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 @@ -126,7 +126,9 @@ export class QueryResultGettersFactory { const relationFields = Object.keys(record) .map( (recordFieldName) => - objectMetadataMapItem.fieldsByName[recordFieldName], + objectMetadataMapItem.fieldsById[ + objectMetadataMapItem.fieldIdByName[recordFieldName] + ], ) .filter(isDefined) .filter((fieldMetadata) => @@ -214,7 +216,7 @@ export class QueryResultGettersFactory { async create( result: QueryResultFieldValue, - objectMetadataItem: ObjectMetadataInterface, + objectMetadataItem: ObjectMetadataItemWithFieldMaps, workspaceId: string, objectMetadataMaps: ObjectMetadataMaps, // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory.ts index 7569ea44b..e2cce404c 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory.ts @@ -19,12 +19,12 @@ import { UpdateManyResolverArgs, UpdateOneResolverArgs, } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; -import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.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 { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate'; import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; +import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; type ArgPositionBackfillInput = { argIndex?: number; @@ -44,14 +44,14 @@ export class QueryRunnerArgsFactory { resolverArgsType: ResolverArgsType, ) { const fieldMetadataMapByNameByName = - options.objectMetadataItemWithFieldMaps.fieldsByName; + options.objectMetadataItemWithFieldMaps.fieldsById; - const shouldBackfillPosition = - options.objectMetadataItemWithFieldMaps.fields.some( - (field) => - field.type === FieldMetadataType.POSITION && - field.name === 'position', - ); + const shouldBackfillPosition = Object.values( + options.objectMetadataItemWithFieldMaps.fieldsById, + ).some( + (field) => + field.type === FieldMetadataType.POSITION && field.name === 'position', + ); switch (resolverArgsType) { case ResolverArgsType.CreateOne: @@ -60,7 +60,6 @@ export class QueryRunnerArgsFactory { data: await this.overrideDataByFieldMetadata( (args as CreateOneResolverArgs).data, options, - fieldMetadataMapByNameByName, { argIndex: 0, shouldBackfillPosition, @@ -72,15 +71,10 @@ export class QueryRunnerArgsFactory { ...args, data: await Promise.all( (args as CreateManyResolverArgs).data?.map((arg, index) => - this.overrideDataByFieldMetadata( - arg, - options, - fieldMetadataMapByNameByName, - { - argIndex: index, - shouldBackfillPosition, - }, - ), + this.overrideDataByFieldMetadata(arg, options, { + argIndex: index, + shouldBackfillPosition, + }), ) ?? [], ), } satisfies CreateManyResolverArgs; @@ -91,7 +85,6 @@ export class QueryRunnerArgsFactory { data: await this.overrideDataByFieldMetadata( (args as UpdateOneResolverArgs).data, options, - fieldMetadataMapByNameByName, { argIndex: 0, shouldBackfillPosition: false, @@ -103,12 +96,11 @@ export class QueryRunnerArgsFactory { ...args, filter: this.overrideFilterByFieldMetadata( (args as UpdateManyResolverArgs).filter, - fieldMetadataMapByNameByName, + options.objectMetadataItemWithFieldMaps, ), data: await this.overrideDataByFieldMetadata( (args as UpdateManyResolverArgs).data, options, - fieldMetadataMapByNameByName, { argIndex: 0, shouldBackfillPosition: false, @@ -120,7 +112,7 @@ export class QueryRunnerArgsFactory { ...args, filter: this.overrideFilterByFieldMetadata( (args as FindOneResolverArgs).filter, - fieldMetadataMapByNameByName, + options.objectMetadataItemWithFieldMaps, ), }; case ResolverArgsType.FindMany: @@ -128,7 +120,7 @@ export class QueryRunnerArgsFactory { ...args, filter: this.overrideFilterByFieldMetadata( (args as FindManyResolverArgs).filter, - fieldMetadataMapByNameByName, + options.objectMetadataItemWithFieldMaps, ), }; @@ -146,15 +138,10 @@ export class QueryRunnerArgsFactory { )) as string[], data: await Promise.all( (args as FindDuplicatesResolverArgs).data?.map((arg, index) => - this.overrideDataByFieldMetadata( - arg, - options, - fieldMetadataMapByNameByName, - { - argIndex: index, - shouldBackfillPosition, - }, - ), + this.overrideDataByFieldMetadata(arg, options, { + argIndex: index, + shouldBackfillPosition, + }), ) ?? [], ), } satisfies FindDuplicatesResolverArgs; @@ -166,7 +153,6 @@ export class QueryRunnerArgsFactory { private async overrideDataByFieldMetadata( data: Partial | undefined, options: WorkspaceQueryRunnerOptions, - fieldMetadataMapByNameByName: Record, argPositionBackfillInput: ArgPositionBackfillInput, ): Promise> { if (!isDefined(data)) { @@ -184,7 +170,10 @@ export class QueryRunnerArgsFactory { data, // eslint-disable-next-line @typescript-eslint/no-explicit-any ).map(async ([key, value]): Promise<[string, any]> => { - const fieldMetadata = fieldMetadataMapByNameByName[key]; + const fieldMetadataId = + options.objectMetadataItemWithFieldMaps.fieldIdByName[key]; + const fieldMetadata = + options.objectMetadataItemWithFieldMaps.fieldsById[fieldMetadataId]; if (!fieldMetadata) { return [key, value]; @@ -257,7 +246,7 @@ export class QueryRunnerArgsFactory { private overrideFilterByFieldMetadata( filter: ObjectRecordFilter | undefined, - fieldMetadataMapByName: Record, + objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, ) { if (!filter) { return; @@ -278,7 +267,7 @@ export class QueryRunnerArgsFactory { acc[key] = this.transformFilterValueByType( key, value, - fieldMetadataMapByName, + objectMetadataItemWithFieldMaps, ); } @@ -293,9 +282,11 @@ export class QueryRunnerArgsFactory { key: string, // eslint-disable-next-line @typescript-eslint/no-explicit-any value: any, - fieldMetadataMapByName: FieldMetadataMap, + objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, ) { - const fieldMetadata = fieldMetadataMapByName[key]; + const fieldMetadataId = objectMetadataItemWithFieldMaps.fieldIdByName[key]; + const fieldMetadata = + objectMetadataItemWithFieldMaps.fieldsById[fieldMetadataId]; if (!fieldMetadata) { return value; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/graphql-query-runner-exception-handler.util.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/graphql-query-runner-exception-handler.util.ts index 40ba98d20..70486a6df 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/graphql-query-runner-exception-handler.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/graphql-query-runner-exception-handler.util.ts @@ -28,6 +28,7 @@ export const graphqlQueryRunnerExceptionHandler = ( case GraphqlQueryRunnerExceptionCode.RELATION_SETTINGS_NOT_FOUND: case GraphqlQueryRunnerExceptionCode.RELATION_TARGET_OBJECT_METADATA_NOT_FOUND: case GraphqlQueryRunnerExceptionCode.INVALID_POST_HOOK_PAYLOAD: + case GraphqlQueryRunnerExceptionCode.MISSING_SYSTEM_FIELD: throw error; default: { const _exhaustiveCheck: never = error.code; 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 f8938355a..cb66f2b87 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,5 +1,5 @@ -import { QueryFailedError } from 'typeorm'; import { isDefined } from 'twenty-shared/utils'; +import { QueryFailedError } from 'typeorm'; import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; @@ -14,14 +14,14 @@ export const handleDuplicateKeyError = ( if (indexNameMatch) { const indexName = indexNameMatch[1]; - const deletedAtFieldMetadata = - context.objectMetadataItemWithFieldMaps.fieldsByName['deletedAt']; + const deletedAtFieldMetadataId = + context.objectMetadataItemWithFieldMaps.fieldIdByName['deletedAt']; const affectedColumns = context.objectMetadataItemWithFieldMaps.indexMetadatas .find((index) => index.name === indexName) ?.indexFieldMetadatas?.filter( - (field) => field.fieldMetadataId !== deletedAtFieldMetadata?.id, + (field) => field.fieldMetadataId !== deletedAtFieldMetadataId, ) .map((indexField) => { const fieldMetadata = diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/workspace-resolver-builder.service.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/workspace-resolver-builder.service.ts index 8fe346ca5..6e3a71174 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/workspace-resolver-builder.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/workspace-resolver-builder.service.ts @@ -12,7 +12,7 @@ export class WorkspaceResolverBuilderService { constructor() {} shouldBuildResolver( - objectMetadata: ObjectMetadataInterface, + objectMetadata: Pick, methodName: WorkspaceResolverBuilderMethodNames, ) { switch (methodName) { 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 4cd778086..ce301ae30 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 @@ -3,26 +3,18 @@ import { Injectable } from '@nestjs/common'; import { makeExecutableSchema } from '@graphql-tools/schema'; import { GraphQLSchema, printSchema } from 'graphql'; import { gql } from 'graphql-tag'; -import { isDefined } from 'twenty-shared/utils'; import { ScalarsExplorerService } from 'src/engine/api/graphql/services/scalars-explorer.service'; import { workspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/factories/factories'; import { WorkspaceResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/workspace-resolver.factory'; import { WorkspaceGraphQLSchemaFactory } from 'src/engine/api/graphql/workspace-schema-builder/workspace-graphql-schema.factory'; import { 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 { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { WorkspaceMetadataCacheException, WorkspaceMetadataCacheExceptionCode, } from 'src/engine/metadata-modules/workspace-metadata-cache/exceptions/workspace-metadata-cache.exception'; import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service'; -import { - WorkspaceMetadataVersionException, - WorkspaceMetadataVersionExceptionCode, -} from 'src/engine/metadata-modules/workspace-metadata-version/exceptions/workspace-metadata-version.exception'; import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; @Injectable() @@ -34,8 +26,6 @@ export class WorkspaceSchemaFactory { private readonly workspaceResolverFactory: WorkspaceResolverFactory, private readonly workspaceCacheStorageService: WorkspaceCacheStorageService, private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService, - private readonly featureFlagService: FeatureFlagService, - private readonly twentyConfigService: TwentyConfigService, ) {} async createGraphQLSchema(authContext: AuthContext): Promise { @@ -52,38 +42,12 @@ export class WorkspaceSchemaFactory { return new GraphQLSchema({}); } - let currentCacheVersion = - await this.workspaceCacheStorageService.getMetadataVersion( - authContext.workspace.id, - ); - - let objectMetadataMaps: ObjectMetadataMaps | undefined; - - if (currentCacheVersion === undefined) { - const recomputed = - await this.workspaceMetadataCacheService.recomputeMetadataCache({ + const { objectMetadataMaps, metadataVersion } = + await this.workspaceMetadataCacheService.getExistingOrRecomputeMetadataMaps( + { workspaceId: authContext.workspace.id, - }); - - objectMetadataMaps = recomputed?.recomputedObjectMetadataMaps; - currentCacheVersion = recomputed?.recomputedMetadataVersion; - } else { - objectMetadataMaps = - await this.workspaceCacheStorageService.getObjectMetadataMaps( - authContext.workspace.id, - currentCacheVersion, - ); - - if (!isDefined(objectMetadataMaps)) { - const recomputed = - await this.workspaceMetadataCacheService.recomputeMetadataCache({ - workspaceId: authContext.workspace.id, - }); - - objectMetadataMaps = recomputed?.recomputedObjectMetadataMaps; - currentCacheVersion = recomputed?.recomputedMetadataVersion; - } - } + }, + ); if (!objectMetadataMaps) { throw new WorkspaceMetadataCacheException( @@ -92,17 +56,10 @@ export class WorkspaceSchemaFactory { ); } - if (!currentCacheVersion) { - throw new WorkspaceMetadataVersionException( - 'Metadata cache version not found', - WorkspaceMetadataVersionExceptionCode.METADATA_VERSION_NOT_FOUND, - ); - } - const objectMetadataCollection = Object.values(objectMetadataMaps.byId).map( (objectMetadataItem) => ({ ...objectMetadataItem, - fields: objectMetadataItem.fields, + fields: Object.values(objectMetadataItem.fieldsById), indexes: objectMetadataItem.indexMetadatas, }), ); @@ -110,12 +67,12 @@ export class WorkspaceSchemaFactory { // Get typeDefs from cache let typeDefs = await this.workspaceCacheStorageService.getGraphQLTypeDefs( authContext.workspace.id, - currentCacheVersion, + metadataVersion, ); let usedScalarNames = await this.workspaceCacheStorageService.getGraphQLUsedScalarNames( authContext.workspace.id, - currentCacheVersion, + metadataVersion, ); // If typeDefs are not cached, generate them @@ -133,12 +90,12 @@ export class WorkspaceSchemaFactory { await this.workspaceCacheStorageService.setGraphQLTypeDefs( authContext.workspace.id, - currentCacheVersion, + metadataVersion, typeDefs, ); await this.workspaceCacheStorageService.setGraphQLUsedScalarNames( authContext.workspace.id, - currentCacheVersion, + metadataVersion, usedScalarNames, ); } 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 fd3e9bf4d..dc5b1de2d 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 @@ -5,6 +5,8 @@ import { isDefined } from 'twenty-shared/utils'; import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler'; +import { getObjectMetadataFromObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps'; + @Injectable() export class RestApiCreateManyHandler extends RestApiBaseHandler { async handle(request: Request) { @@ -57,7 +59,9 @@ export class RestApiCreateManyHandler extends RestApiBaseHandler { this.apiEventEmitterService.emitCreateEvents({ records: createdRecords, authContext: this.getAuthContextFromRequest(request), - objectMetadataItem: objectMetadata.objectMetadataMapItem, + objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps( + objectMetadata.objectMetadataMapItem, + ), }); const records = await this.getRecord({ 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 943b5459b..1d99e49ac 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 @@ -5,6 +5,8 @@ import { isDefined } from 'twenty-shared/utils'; import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler'; +import { getObjectMetadataFromObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps'; + @Injectable() export class RestApiCreateOneHandler extends RestApiBaseHandler { async handle(request: Request) { @@ -40,7 +42,9 @@ export class RestApiCreateOneHandler extends RestApiBaseHandler { this.apiEventEmitterService.emitCreateEvents({ records: [createdRecord], authContext: this.getAuthContextFromRequest(request), - objectMetadataItem: objectMetadata.objectMetadataMapItem, + objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps( + objectMetadata.objectMetadataMapItem, + ), }); const records = await this.getRecord({ 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 e9a152a3d..0b48778df 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 @@ -5,6 +5,7 @@ import { Request } from 'express'; import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler'; import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils'; +import { getObjectMetadataFromObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps'; @Injectable() export class RestApiDeleteOneHandler extends RestApiBaseHandler { @@ -26,7 +27,9 @@ export class RestApiDeleteOneHandler extends RestApiBaseHandler { this.apiEventEmitterService.emitDestroyEvents({ records: [recordToDelete], authContext: this.getAuthContextFromRequest(request), - objectMetadataItem: objectMetadata.objectMetadataMapItem, + objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps( + objectMetadata.objectMetadataMapItem, + ), }); return this.formatResult({ 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 02556f335..550b4b183 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 @@ -4,14 +4,14 @@ import { Request } from 'express'; import isEmpty from 'lodash.isempty'; import { In } from 'typeorm'; +import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; import { FormatResult, RestApiBaseHandler, } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler'; -import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; -import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; import { buildDuplicateConditions } from 'src/engine/api/utils/build-duplicate-conditions.utils'; +import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; @Injectable() export class RestApiFindDuplicatesHandler extends RestApiBaseHandler { 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 f5539f634..4d236a2c9 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 @@ -6,6 +6,7 @@ import { isDefined } from 'twenty-shared/utils'; import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler'; import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils'; +import { getObjectMetadataFromObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps'; @Injectable() export class RestApiUpdateOneHandler extends RestApiBaseHandler { @@ -38,7 +39,9 @@ export class RestApiUpdateOneHandler extends RestApiBaseHandler { records: [updatedRecord], updatedFields: Object.keys(request.body), authContext: this.getAuthContextFromRequest(request), - objectMetadataItem: objectMetadata.objectMetadataMapItem, + objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps( + objectMetadata.objectMetadataMapItem, + ), }); const records = await this.getRecord({ diff --git a/packages/twenty-server/src/engine/api/rest/core/interfaces/rest-api-base.handler.ts b/packages/twenty-server/src/engine/api/rest/core/interfaces/rest-api-base.handler.ts index e132c3e42..2e65fdc49 100644 --- a/packages/twenty-server/src/engine/api/rest/core/interfaces/rest-api-base.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/interfaces/rest-api-base.handler.ts @@ -160,32 +160,34 @@ export abstract class RestApiBaseHandler { const relations: string[] = []; - objectMetadata.objectMetadataMapItem.fields.forEach((field) => { - if (field.type === FieldMetadataType.RELATION) { - if ( - depth === MAX_DEPTH && - isDefined(field.relationTargetObjectMetadataId) - ) { - const relationTargetObjectMetadata = - objectMetadata.objectMetadataMaps.byId[ - field.relationTargetObjectMetadataId - ]; - const depth2Relations = this.getRelations({ - objectMetadata: { - objectMetadataMaps: objectMetadata.objectMetadataMaps, - objectMetadataMapItem: relationTargetObjectMetadata, - }, - depth: 1, - }); + Object.values(objectMetadata.objectMetadataMapItem.fieldsById).forEach( + (field) => { + if (field.type === FieldMetadataType.RELATION) { + if ( + depth === MAX_DEPTH && + isDefined(field.relationTargetObjectMetadataId) + ) { + const relationTargetObjectMetadata = + objectMetadata.objectMetadataMaps.byId[ + field.relationTargetObjectMetadataId + ]; + const depth2Relations = this.getRelations({ + objectMetadata: { + objectMetadataMaps: objectMetadata.objectMetadataMaps, + objectMetadataMapItem: relationTargetObjectMetadata, + }, + depth: 1, + }); - depth2Relations.forEach((depth2Relation) => { - relations.push(`${field.name}.${depth2Relation}`); - }); - } else { - relations.push(`${field.name}`); + depth2Relations.forEach((depth2Relation) => { + relations.push(`${field.name}.${depth2Relation}`); + }); + } else { + relations.push(`${field.name}`); + } } - } - }); + }, + ); return relations; } @@ -305,9 +307,7 @@ export abstract class RestApiBaseHandler { objectMetadataMaps: ObjectMetadataMaps; objectMetadataMapItem: ObjectMetadataItemWithFieldMaps; }; - objectMetadataItemWithFieldsMaps: - | ObjectMetadataItemWithFieldMaps - | undefined; + objectMetadataItemWithFieldsMaps: ObjectMetadataItemWithFieldMaps; extraFilters?: Partial; }) { const objectMetadataNameSingular = @@ -321,17 +321,10 @@ export abstract class RestApiBaseHandler { objectMetadata, ); - const fieldMetadataMapByName = - objectMetadataItemWithFieldsMaps?.fieldsByName || {}; - - const fieldMetadataMapByJoinColumnName = - objectMetadataItemWithFieldsMaps?.fieldsByJoinColumnName || {}; - const isForwardPagination = !inputs.endingBefore; const graphqlQueryParser = new GraphqlQueryParser( - fieldMetadataMapByName, - fieldMetadataMapByJoinColumnName, + objectMetadataItemWithFieldsMaps, objectMetadata.objectMetadataMaps, ); @@ -442,7 +435,7 @@ export abstract class RestApiBaseHandler { const cursorArgFilter = computeCursorArgFilter( this.parseCursor(cursor), inputs.orderBy || [], - objectMetadata.objectMetadataMapItem.fieldsByName, + objectMetadata.objectMetadataMapItem, isForwardPagination, ); diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/factories/create-many-query.factory.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/factories/create-many-query.factory.ts index 22ec1030a..c4385c3b9 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/factories/create-many-query.factory.ts +++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/factories/create-many-query.factory.ts @@ -26,7 +26,7 @@ export class CreateManyQueryFactory { mutation Create${objectNamePlural}($data: [${objectNameSingular}CreateInput!]) { create${objectNamePlural}(data: $data) { id - ${objectMetadata.objectMetadataMapItem.fields + ${Object.values(objectMetadata.objectMetadataMapItem.fieldsById) .map((field) => mapFieldMetadataToGraphqlQuery( objectMetadata.objectMetadataMaps, diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/factories/find-duplicates-query.factory.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/factories/find-duplicates-query.factory.ts index dbd41fcab..99db9a30e 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/factories/find-duplicates-query.factory.ts +++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/factories/find-duplicates-query.factory.ts @@ -31,7 +31,7 @@ export class FindDuplicatesQueryFactory { } edges{ node { - ${objectMetadata.objectMetadataMapItem.fields + ${Object.values(objectMetadata.objectMetadataMapItem.fieldsById) .map((field) => mapFieldMetadataToGraphqlQuery( objectMetadata.objectMetadataMaps, diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/check-fields.utils.spec.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/check-fields.utils.spec.ts index b907989b6..b825d0d6b 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/check-fields.utils.spec.ts +++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/check-fields.utils.spec.ts @@ -17,21 +17,23 @@ describe('checkFields', () => { objectMetadataId: 'object-metadata-id', isNullable: fieldNumberMock.isNullable, defaultValue: fieldNumberMock.defaultValue, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), }; const fieldsById: FieldMetadataMap = { 'field-number-id': completeFieldNumberMock, }; - const fieldsByName: FieldMetadataMap = { - [completeFieldNumberMock.name]: completeFieldNumberMock, - }; - const mockObjectMetadataWithFieldMaps = { ...objectMetadataItemMock, fieldsById, - fieldsByName, - fieldsByJoinColumnName: {}, + fieldIdByName: { + [completeFieldNumberMock.name]: completeFieldNumberMock.id, + }, + fieldIdByJoinColumnName: {}, + indexMetadatas: [], }; it('should check field types', () => { diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/get-field-type.utils.spec.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/get-field-type.utils.spec.ts index 7370917e8..9ba6edb74 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/get-field-type.utils.spec.ts +++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/get-field-type.utils.spec.ts @@ -18,21 +18,23 @@ describe('getFieldType', () => { objectMetadataId: 'object-metadata-id', isNullable: fieldNumberMock.isNullable, defaultValue: fieldNumberMock.defaultValue, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), }; const fieldsById: FieldMetadataMap = { 'field-number-id': completeFieldNumberMock, }; - const fieldsByName: FieldMetadataMap = { - [completeFieldNumberMock.name]: completeFieldNumberMock, - }; - const mockObjectMetadataWithFieldMaps = { ...objectMetadataItemMock, fieldsById, - fieldsByName, - fieldsByJoinColumnName: {}, + fieldIdByName: { + [completeFieldNumberMock.name]: completeFieldNumberMock.id, + }, + fieldIdByJoinColumnName: {}, + indexMetadatas: [], }; it('should get field type', () => { diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/map-field-metadata-to-graphql-query.utils.spec.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/map-field-metadata-to-graphql-query.utils.spec.ts index 113328510..a8e4fff5f 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/map-field-metadata-to-graphql-query.utils.spec.ts +++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/map-field-metadata-to-graphql-query.utils.spec.ts @@ -24,6 +24,9 @@ describe('mapFieldMetadataToGraphqlQuery', () => { objectMetadataId: 'object-metadata-id', isNullable: fieldNumberMock.isNullable, defaultValue: fieldNumberMock.defaultValue, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), }; const typedFieldTextMock: FieldMetadataInterface = { @@ -34,6 +37,9 @@ describe('mapFieldMetadataToGraphqlQuery', () => { objectMetadataId: 'object-metadata-id', isNullable: fieldTextMock.isNullable, defaultValue: fieldTextMock.defaultValue, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), }; const typedFieldCurrencyMock: FieldMetadataInterface = { @@ -44,6 +50,9 @@ describe('mapFieldMetadataToGraphqlQuery', () => { objectMetadataId: 'object-metadata-id', isNullable: fieldCurrencyMock.isNullable, defaultValue: fieldCurrencyMock.defaultValue, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), }; const fieldsById: FieldMetadataMap = { @@ -52,17 +61,16 @@ describe('mapFieldMetadataToGraphqlQuery', () => { 'field-currency-id': typedFieldCurrencyMock, }; - const fieldsByName: FieldMetadataMap = { - [typedFieldNumberMock.name]: typedFieldNumberMock, - [typedFieldTextMock.name]: typedFieldTextMock, - [typedFieldCurrencyMock.name]: typedFieldCurrencyMock, - }; - const typedObjectMetadataItem: ObjectMetadataItemWithFieldMaps = { ...objectMetadataItemMock, fieldsById, - fieldsByName, - fieldsByJoinColumnName: {}, + fieldIdByName: { + [typedFieldNumberMock.name]: typedFieldNumberMock.id, + [typedFieldTextMock.name]: typedFieldTextMock.id, + [typedFieldCurrencyMock.name]: typedFieldCurrencyMock.id, + }, + fieldIdByJoinColumnName: {}, + indexMetadatas: [], }; const objectMetadataMapsMock: ObjectMetadataMaps = { @@ -110,6 +118,10 @@ describe('mapFieldMetadataToGraphqlQuery', () => { name: 'toObjectMetadataName', label: 'Test Field', objectMetadataId: 'object-metadata-id', + isNullable: true, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), }; if (fieldMetadataType === FieldMetadataType.RELATION) { diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/check-fields.utils.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/check-fields.utils.ts index e9a10d105..a23e341d6 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/check-fields.utils.ts +++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/check-fields.utils.ts @@ -9,7 +9,7 @@ export const checkFields = ( objectMetadataItem: ObjectMetadataItemWithFieldMaps, fieldNames: string[], ): void => { - const fieldMetadataNames = objectMetadataItem.fields + const fieldMetadataNames = Object.values(objectMetadataItem.fieldsById) .map((field) => { if (isCompositeFieldMetadataType(field.type)) { const compositeType = compositeTypeDefinitions.get(field.type); diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/check-order-by.utils.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/check-order-by.utils.ts index b128ad11b..14388513b 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/check-order-by.utils.ts +++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/check-order-by.utils.ts @@ -11,7 +11,7 @@ export const checkArrayFields = ( objectMetadataItem: ObjectMetadataItemWithFieldMaps, fields: Array>, ): void => { - const fieldMetadataNames = objectMetadataItem.fields + const fieldMetadataNames = Object.values(objectMetadataItem.fieldsById) .map((field) => { if (isCompositeFieldMetadataType(field.type)) { const compositeType = compositeTypeDefinitions.get(field.type); diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/check-filter-enum-values.spec.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/check-filter-enum-values.spec.ts index 2968d0387..3fae2eebd 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/check-filter-enum-values.spec.ts +++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/check-filter-enum-values.spec.ts @@ -19,21 +19,23 @@ describe('checkFilterEnumValues', () => { 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 fieldsByName: FieldMetadataMap = { - [completeFieldSelectMock.name]: completeFieldSelectMock, - }; - const mockObjectMetadataWithFieldMaps = { ...objectMetadataItemMock, fieldsById, - fieldsByName, - fieldsByJoinColumnName: {}, + fieldIdByName: { + [completeFieldSelectMock.name]: completeFieldSelectMock.id, + }, + fieldIdByJoinColumnName: {}, + indexMetadatas: [], }; it('should check properly', () => { diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/parse-filter.utils.spec.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/parse-filter.utils.spec.ts index 74ff334e8..0dc6630db 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/parse-filter.utils.spec.ts +++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/parse-filter.utils.spec.ts @@ -7,6 +7,7 @@ import { } from 'src/engine/api/__mocks__/object-metadata-item.mock'; import { parseFilter } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/parse-filter.utils'; import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; +import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; describe('parseFilter', () => { const completeFieldNumberMock: FieldMetadataInterface = { @@ -17,6 +18,9 @@ describe('parseFilter', () => { objectMetadataId: 'object-metadata-id', isNullable: fieldNumberMock.isNullable, defaultValue: fieldNumberMock.defaultValue, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), }; const completeFieldTextMock: FieldMetadataInterface = { @@ -27,6 +31,9 @@ describe('parseFilter', () => { objectMetadataId: 'object-metadata-id', isNullable: fieldTextMock.isNullable, defaultValue: fieldTextMock.defaultValue, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), }; const fieldsById: FieldMetadataMap = { @@ -34,16 +41,15 @@ describe('parseFilter', () => { 'field-text-id': completeFieldTextMock, }; - const fieldsByName: FieldMetadataMap = { - [completeFieldNumberMock.name]: completeFieldNumberMock, - [completeFieldTextMock.name]: completeFieldTextMock, - }; - - const mockObjectMetadataWithFieldMaps = { + const mockObjectMetadataWithFieldMaps: ObjectMetadataItemWithFieldMaps = { ...objectMetadataItemMock, fieldsById, - fieldsByName, - fieldsByJoinColumnName: {}, + fieldIdByName: { + [completeFieldNumberMock.name]: completeFieldNumberMock.id, + [completeFieldTextMock.name]: completeFieldTextMock.id, + }, + fieldIdByJoinColumnName: {}, + indexMetadatas: [], }; it('should parse string filter test 1', () => { diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/check-filter-enum-values.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/check-filter-enum-values.ts index de0e7d2fb..98f39cebe 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/check-filter-enum-values.ts +++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/check-filter-enum-values.ts @@ -18,7 +18,8 @@ export const checkFilterEnumValues = ( ) { return; } - const field = objectMetadataItem.fieldsByName[fieldName]; + const fieldMetadataId = objectMetadataItem.fieldIdByName[fieldName]; + const field = objectMetadataItem.fieldsById[fieldMetadataId]; const values = /^\[.*\]$/.test(value) ? value.slice(1, -1).split(',') diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/get-field-type.utils.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/get-field-type.utils.ts index a6ddc469a..cd2953038 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/get-field-type.utils.ts +++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/get-field-type.utils.ts @@ -6,5 +6,8 @@ export const getFieldType = ( objectMetadataItem: ObjectMetadataItemWithFieldMaps, fieldName: string, ): FieldMetadataType | undefined => { - return objectMetadataItem.fieldsByName[fieldName]?.type; + const fieldMetadataId = objectMetadataItem.fieldIdByName[fieldName]; + const field = objectMetadataItem.fieldsById[fieldMetadataId]; + + return field?.type; }; diff --git a/packages/twenty-server/src/engine/api/rest/input-factories/__tests__/filter-input.factory.spec.ts b/packages/twenty-server/src/engine/api/rest/input-factories/__tests__/filter-input.factory.spec.ts index 6ae900349..63231a374 100644 --- a/packages/twenty-server/src/engine/api/rest/input-factories/__tests__/filter-input.factory.spec.ts +++ b/packages/twenty-server/src/engine/api/rest/input-factories/__tests__/filter-input.factory.spec.ts @@ -10,6 +10,7 @@ import { } from 'src/engine/api/__mocks__/object-metadata-item.mock'; import { FilterInputFactory } from 'src/engine/api/rest/input-factories/filter-input.factory'; import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; +import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; describe('FilterInputFactory', () => { const completeFieldNumberMock: FieldMetadataInterface = { @@ -20,6 +21,9 @@ describe('FilterInputFactory', () => { objectMetadataId: 'object-metadata-id', isNullable: fieldNumberMock.isNullable, defaultValue: fieldNumberMock.defaultValue, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), }; const completeFieldTextMock: FieldMetadataInterface = { @@ -30,6 +34,9 @@ describe('FilterInputFactory', () => { objectMetadataId: 'object-metadata-id', isNullable: fieldTextMock.isNullable, defaultValue: fieldTextMock.defaultValue, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), }; const completeFieldCurrencyMock: FieldMetadataInterface = { @@ -40,6 +47,9 @@ describe('FilterInputFactory', () => { objectMetadataId: 'object-metadata-id', isNullable: fieldCurrencyMock.isNullable, defaultValue: fieldCurrencyMock.defaultValue, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), }; const fieldsById: FieldMetadataMap = { @@ -48,16 +58,15 @@ describe('FilterInputFactory', () => { 'field-currency-id': completeFieldCurrencyMock, }; - const fieldsByName: FieldMetadataMap = { - [completeFieldNumberMock.name]: completeFieldNumberMock, - [completeFieldTextMock.name]: completeFieldTextMock, - [completeFieldCurrencyMock.name]: completeFieldCurrencyMock, - }; - - const objectMetadataMapItem = { + const objectMetadataMapItem: ObjectMetadataItemWithFieldMaps = { ...objectMetadataMapItemMock, fieldsById, - fieldsByName, + fieldIdByName: { + [completeFieldNumberMock.name]: completeFieldNumberMock.id, + [completeFieldTextMock.name]: completeFieldTextMock.id, + [completeFieldCurrencyMock.name]: completeFieldCurrencyMock.id, + }, + fieldIdByJoinColumnName: {}, }; const objectMetadataMaps = { 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 4ba57dd2c..042d88552 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,11 @@ -import { mockPersonObjectMetadata } from 'src/engine/api/graphql/graphql-query-runner/__mocks__/mockPersonObjectMetadata'; -import { buildDuplicateConditions } from 'src/engine/api/utils/build-duplicate-conditions.utils'; +import { mockPersonObjectMetadataWithFieldMaps } 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( - mockPersonObjectMetadata([['emailsPrimaryEmail']]), + mockPersonObjectMetadataWithFieldMaps([['emailsPrimaryEmail']]), mockPersonRecords, 'recordId', ); @@ -13,8 +13,10 @@ describe('buildDuplicateConditions', () => { expect(duplicateConditons).toEqual({ or: [ { - emailsPrimaryEmail: { - eq: 'test@test.fr', + emails: { + primaryEmail: { + eq: 'test@test.fr', + }, }, }, ], @@ -26,7 +28,7 @@ describe('buildDuplicateConditions', () => { it('should build conditions based on duplicate criteria from basic field', () => { const duplicateConditons = buildDuplicateConditions( - mockPersonObjectMetadata([['jobTitle']]), + mockPersonObjectMetadataWithFieldMaps([['jobTitle']]), mockPersonRecords, 'recordId', ); @@ -47,7 +49,7 @@ describe('buildDuplicateConditions', () => { it('should not build conditions based on duplicate criteria if record value is null or too small', () => { const duplicateConditons = buildDuplicateConditions( - mockPersonObjectMetadata([['linkedinLinkPrimaryLinkUrl']]), + mockPersonObjectMetadataWithFieldMaps([['linkedinLinkPrimaryLinkUrl']]), mockPersonRecords, 'recordId', ); @@ -57,7 +59,7 @@ describe('buildDuplicateConditions', () => { it('should build conditions based on duplicate criteria and without recordId filter', () => { const duplicateConditons = buildDuplicateConditions( - mockPersonObjectMetadata([['jobTitle']]), + mockPersonObjectMetadataWithFieldMaps([['jobTitle']]), 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 ebb0bf42f..285f4714f 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 @@ -4,35 +4,76 @@ import { OrderByDirection } from 'src/engine/api/graphql/workspace-query-builder 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 { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; describe('computeCursorArgFilter', () => { - const mockFieldMetadataMap = { - name: { - type: FieldMetadataType.TEXT, - id: 'name-id', - name: 'name', - label: 'Name', - objectMetadataId: 'object-id', + const objectMetadataItemWithFieldMaps = { + 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', }, - age: { - type: FieldMetadataType.NUMBER, - id: 'age-id', - name: 'age', - label: 'Age', - objectMetadataId: 'object-id', + fieldsById: { + 'name-id': { + type: FieldMetadataType.TEXT, + id: 'name-id', + name: 'name', + label: 'Name', + objectMetadataId: 'object-id', + isLabelSyncedWithName: true, + isNullable: true, + createdAt: new Date(), + updatedAt: new Date(), + }, + 'age-id': { + type: FieldMetadataType.NUMBER, + id: 'age-id', + name: 'age', + label: 'Age', + objectMetadataId: 'object-id', + isLabelSyncedWithName: true, + isNullable: true, + createdAt: new Date(), + updatedAt: new Date(), + }, + 'fullname-id': { + type: FieldMetadataType.FULL_NAME, + id: 'fullname-id', + name: 'fullName', + label: 'Full Name', + objectMetadataId: 'object-id', + isLabelSyncedWithName: true, + isNullable: true, + createdAt: new Date(), + updatedAt: new Date(), + }, }, - fullName: { - type: FieldMetadataType.FULL_NAME, - id: 'fullname-id', - name: 'fullName', - label: 'Full Name', - objectMetadataId: 'object-id', - }, - }; + } satisfies ObjectMetadataItemWithFieldMaps; describe('basic cursor filtering', () => { it('should return empty array when cursor is empty', () => { - const result = computeCursorArgFilter({}, [], mockFieldMetadataMap, true); + const result = computeCursorArgFilter( + {}, + [], + objectMetadataItemWithFieldMaps, + true, + ); expect(result).toEqual([]); }); @@ -44,7 +85,7 @@ describe('computeCursorArgFilter', () => { const result = computeCursorArgFilter( cursor, orderBy, - mockFieldMetadataMap, + objectMetadataItemWithFieldMaps, true, ); @@ -58,7 +99,7 @@ describe('computeCursorArgFilter', () => { const result = computeCursorArgFilter( cursor, orderBy, - mockFieldMetadataMap, + objectMetadataItemWithFieldMaps, false, ); @@ -77,7 +118,7 @@ describe('computeCursorArgFilter', () => { const result = computeCursorArgFilter( cursor, orderBy, - mockFieldMetadataMap, + objectMetadataItemWithFieldMaps, true, ); @@ -105,7 +146,7 @@ describe('computeCursorArgFilter', () => { const result = computeCursorArgFilter( cursor, orderBy, - mockFieldMetadataMap, + objectMetadataItemWithFieldMaps, true, ); @@ -151,7 +192,7 @@ describe('computeCursorArgFilter', () => { const result = computeCursorArgFilter( cursor, orderBy, - mockFieldMetadataMap, + objectMetadataItemWithFieldMaps, true, ); @@ -180,7 +221,7 @@ describe('computeCursorArgFilter', () => { const result = computeCursorArgFilter( cursor, orderBy, - mockFieldMetadataMap, + objectMetadataItemWithFieldMaps, false, ); @@ -218,7 +259,12 @@ describe('computeCursorArgFilter', () => { const orderBy = [{ invalidField: OrderByDirection.AscNullsLast }]; expect(() => - computeCursorArgFilter(cursor, orderBy, mockFieldMetadataMap, true), + computeCursorArgFilter( + cursor, + orderBy, + objectMetadataItemWithFieldMaps, + true, + ), ).toThrow(GraphqlQueryRunnerException); }); @@ -227,7 +273,12 @@ describe('computeCursorArgFilter', () => { const orderBy = [{ age: OrderByDirection.AscNullsLast }]; expect(() => - computeCursorArgFilter(cursor, orderBy, mockFieldMetadataMap, true), + computeCursorArgFilter( + cursor, + orderBy, + objectMetadataItemWithFieldMaps, + 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 a1f5fb249..d3c5eb72f 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 @@ -11,19 +11,19 @@ import { GraphqlQueryRunnerException, GraphqlQueryRunnerExceptionCode, } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; +import { buildCursorCompositeFieldWhereCondition } from 'src/engine/api/utils/build-cursor-composite-field-where-condition.utils'; 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 { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; -import { buildCursorCompositeFieldWhereCondition } from 'src/engine/api/utils/build-cursor-composite-field-where-condition.utils'; +import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; type BuildCursorWhereConditionParams = { cursorKey: keyof ObjectRecord; cursorValue: | ObjectRecordCursorLeafScalarValue | ObjectRecordCursorLeafCompositeValue; - fieldMetadataMapByName: FieldMetadataMap; + objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; orderBy: ObjectRecordOrderBy; isForwardPagination: boolean; isEqualityCondition?: boolean; @@ -32,12 +32,15 @@ type BuildCursorWhereConditionParams = { export const buildCursorWhereCondition = ({ cursorKey, cursorValue, - fieldMetadataMapByName, + objectMetadataItemWithFieldMaps, orderBy, isForwardPagination, isEqualityCondition = false, }: BuildCursorWhereConditionParams): Record => { - const fieldMetadata = fieldMetadataMapByName[cursorKey]; + const fieldMetadataId = + objectMetadataItemWithFieldMaps.fieldIdByName[cursorKey]; + const fieldMetadata = + objectMetadataItemWithFieldMaps.fieldsById[fieldMetadataId]; if (!fieldMetadata) { throw new GraphqlQueryRunnerException( 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 c41a31faf..6c73bfb22 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 @@ -9,13 +9,13 @@ import { } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; import { buildCursorCumulativeWhereCondition } from 'src/engine/api/utils/build-cursor-cumulative-where-conditions.utils'; -import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; import { buildCursorWhereCondition } from 'src/engine/api/utils/build-cursor-where-condition.utils'; +import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; export const computeCursorArgFilter = ( cursor: ObjectRecordCursor, orderBy: ObjectRecordOrderBy, - fieldMetadataMapByName: FieldMetadataMap, + objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, isForwardPagination = true, ): ObjectRecordFilter[] => { const cursorEntries = Object.entries(cursor) @@ -42,7 +42,7 @@ export const computeCursorArgFilter = ( buildCursorWhereCondition({ cursorKey, cursorValue, - fieldMetadataMapByName, + objectMetadataItemWithFieldMaps, orderBy, isForwardPagination: true, isEqualityCondition: true, @@ -51,7 +51,7 @@ export const computeCursorArgFilter = ( buildCursorWhereCondition({ cursorKey, cursorValue, - fieldMetadataMapByName, + objectMetadataItemWithFieldMaps, orderBy, isForwardPagination, }), 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 da5bd12b1..cdf42c5fa 100644 --- a/packages/twenty-server/src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps.ts +++ b/packages/twenty-server/src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps.ts @@ -23,25 +23,6 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa labelIdentifierFieldMetadataId: 'nameFieldMetadataId', imageIdentifierFieldMetadataId: '', workspaceId: '', - fields: [ - { - id: 'nameFieldMetadataId', - objectMetadataId: '', - 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, - workspaceId: '', - }, - ], indexMetadatas: [], fieldsById: { nameFieldMetadataId: { @@ -60,28 +41,15 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa isNullable: true, isUnique: false, workspaceId: '', + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), }, }, - fieldsByName: { - name: { - id: 'nameFieldMetadataId', - objectMetadataId: '', - 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, - workspaceId: '', - }, + fieldIdByName: { + name: 'nameFieldMetadataId', }, - fieldsByJoinColumnName: {}, + fieldIdByJoinColumnName: {}, }, { id: '', @@ -102,34 +70,6 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa labelIdentifierFieldMetadataId: 'nameFieldMetadataId', imageIdentifierFieldMetadataId: '', workspaceId: '', - fields: [ - { - id: 'nameFieldMetadataId', - objectMetadataId: '', - type: FieldMetadataType.TEXT, - icon: 'test-field-icon', - name: 'name', - label: 'Name', - defaultValue: '', - isCustom: false, - isNullable: true, - isUnique: false, - workspaceId: '', - }, - { - id: 'domainNameFieldMetadataId', - objectMetadataId: '', - type: FieldMetadataType.LINKS, - icon: 'test-field-icon', - name: 'domainName', - label: 'Domain Name', - defaultValue: '', - isCustom: false, - isNullable: true, - isUnique: false, - workspaceId: '', - }, - ], indexMetadatas: [], fieldsById: { nameFieldMetadataId: { @@ -144,6 +84,9 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa isNullable: true, isUnique: false, workspaceId: '', + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), }, domainNameFieldMetadataId: { id: 'domainNameFieldMetadataId', @@ -157,40 +100,16 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa isNullable: true, isUnique: false, workspaceId: '', + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), }, }, - fieldsByName: { - name: { - id: 'nameFieldMetadataId', - objectMetadataId: '', - type: FieldMetadataType.TEXT, - icon: 'test-field-icon', - name: 'name', - label: 'Name', - defaultValue: { - lastName: "''", - firstName: "''", - }, - isCustom: false, - isNullable: true, - isUnique: false, - workspaceId: '', - }, - domainName: { - id: 'domainNameFieldMetadataId', - objectMetadataId: '', - type: FieldMetadataType.LINKS, - icon: 'test-field-icon', - name: 'domainName', - label: 'Domain Name', - defaultValue: '', - isCustom: false, - isNullable: true, - isUnique: false, - workspaceId: '', - }, + fieldIdByName: { + name: 'nameFieldMetadataId', + domainName: 'domainNameFieldMetadataId', }, - fieldsByJoinColumnName: {}, + fieldIdByJoinColumnName: {}, }, { id: '', @@ -211,34 +130,6 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa labelIdentifierFieldMetadataId: 'nameFieldMetadataId', imageIdentifierFieldMetadataId: 'imageIdentifierFieldMetadataId', workspaceId: '', - fields: [ - { - id: 'nameFieldMetadataId', - objectMetadataId: '', - type: FieldMetadataType.TEXT, - icon: 'test-field-icon', - name: 'name', - label: 'Name', - defaultValue: '', - isCustom: false, - isNullable: true, - isUnique: false, - workspaceId: '', - }, - { - id: 'imageIdentifierFieldMetadataId', - objectMetadataId: '', - type: FieldMetadataType.TEXT, - icon: 'test-field-icon', - name: 'imageIdentifierFieldName', - label: 'Image Identifier Field Name', - defaultValue: '', - isCustom: false, - isNullable: true, - isUnique: false, - workspaceId: '', - }, - ], indexMetadatas: [], fieldsById: { nameFieldMetadataId: { @@ -253,6 +144,9 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa isNullable: true, isUnique: false, workspaceId: '', + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), }, imageIdentifierFieldMetadataId: { id: 'imageIdentifierFieldMetadataId', @@ -266,40 +160,16 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa isNullable: true, isUnique: false, workspaceId: '', + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), }, }, - fieldsByName: { - name: { - id: 'nameFieldMetadataId', - objectMetadataId: '', - type: FieldMetadataType.TEXT, - icon: 'test-field-icon', - name: 'name', - label: 'Name', - defaultValue: { - lastName: "''", - firstName: "''", - }, - isCustom: false, - isNullable: true, - isUnique: false, - workspaceId: '', - }, - imageIdentifierFieldName: { - id: 'imageIdentifierFieldMetadataId', - objectMetadataId: '', - type: FieldMetadataType.TEXT, - icon: 'test-field-icon', - name: 'imageIdentifierFieldName', - label: 'Image Identifier Field Name', - defaultValue: '', - isCustom: false, - isNullable: true, - isUnique: false, - workspaceId: '', - }, + fieldIdByName: { + name: 'nameFieldMetadataId', + imageIdentifierFieldName: 'imageIdentifierFieldMetadataId', }, - fieldsByJoinColumnName: {}, + fieldIdByJoinColumnName: {}, }, { id: '', @@ -320,10 +190,9 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa labelIdentifierFieldMetadataId: '', imageIdentifierFieldMetadataId: '', workspaceId: '', - fields: [], indexMetadatas: [], fieldsById: {}, - fieldsByName: {}, - fieldsByJoinColumnName: {}, + fieldIdByName: {}, + fieldIdByJoinColumnName: {}, }, ]; diff --git a/packages/twenty-server/src/engine/core-modules/auth/token/services/access-token.service.ts b/packages/twenty-server/src/engine/core-modules/auth/token/services/access-token.service.ts index 0e1c7c3ad..22e047584 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/token/services/access-token.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/token/services/access-token.service.ts @@ -21,13 +21,13 @@ import { import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; +import { userWorkspaceValidator } from 'src/engine/core-modules/user-workspace/user-workspace.validate'; import { User } from 'src/engine/core-modules/user/user.entity'; import { userValidator } from 'src/engine/core-modules/user/user.validate'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; -import { userWorkspaceValidator } from 'src/engine/core-modules/user-workspace/user-workspace.validate'; @Injectable() export class AccessTokenService { @@ -78,9 +78,6 @@ export class AccessTokenService { await this.twentyORMGlobalManager.getRepositoryForWorkspace( workspaceId, 'workspaceMember', - { - shouldFailIfMetadataNotFound: false, - }, ); const workspaceMember = await workspaceMemberRepository.findOne({ diff --git a/packages/twenty-server/src/engine/core-modules/event-emitter/types/object-record.base.event.ts b/packages/twenty-server/src/engine/core-modules/event-emitter/types/object-record.base.event.ts index fa9a4a89e..c10a598cf 100644 --- a/packages/twenty-server/src/engine/core-modules/event-emitter/types/object-record.base.event.ts +++ b/packages/twenty-server/src/engine/core-modules/event-emitter/types/object-record.base.event.ts @@ -13,6 +13,6 @@ export class ObjectRecordBaseEvent { recordId: string; userId?: string; workspaceMemberId?: string; - objectMetadata: ObjectMetadataInterface; + objectMetadata: Omit; properties: Properties; } 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 e31925833..775fc64b1 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 @@ -4,6 +4,7 @@ import { objectRecordChangedValues } from 'src/engine/core-modules/event-emitter const mockObjectMetadata: ObjectMetadataInterface = { id: '1', + icon: 'Icon123', nameSingular: 'Object', namePlural: 'Objects', labelSingular: 'Object', diff --git a/packages/twenty-server/src/engine/core-modules/feature-flag/services/feature-flag.service.ts b/packages/twenty-server/src/engine/core-modules/feature-flag/services/feature-flag.service.ts index 4cbe27ece..7587e63a7 100644 --- a/packages/twenty-server/src/engine/core-modules/feature-flag/services/feature-flag.service.ts +++ b/packages/twenty-server/src/engine/core-modules/feature-flag/services/feature-flag.service.ts @@ -74,10 +74,7 @@ export class FeatureFlagService { ); await this.workspaceFeatureFlagsMapCacheService.recomputeFeatureFlagsMapCache( - { - workspaceId, - ignoreLock: true, - }, + { workspaceId }, ); } } @@ -132,10 +129,7 @@ export class FeatureFlagService { const result = await this.featureFlagRepository.save(featureFlagToSave); await this.workspaceFeatureFlagsMapCacheService.recomputeFeatureFlagsMapCache( - { - workspaceId, - ignoreLock: true, - }, + { workspaceId }, ); return result; 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 97335dc03..7aa94c6d6 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 @@ -3,8 +3,6 @@ import { Injectable } from '@nestjs/common'; import { FieldMetadataType } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; -import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; - 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 { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; @@ -29,19 +27,11 @@ export class RecordInputTransformerService { return recordInput; } - const fieldMetadataByFieldName = objectMetadataMapItem.fields.reduce( - (acc, field) => { - acc[field.name] = field; - - return acc; - }, - {} as Record, - ); - let transformedEntries = {}; for (const [key, value] of Object.entries(recordInput)) { - const fieldMetadata = fieldMetadataByFieldName[key]; + const fieldMetadataId = objectMetadataMapItem.fieldIdByName[key]; + const fieldMetadata = objectMetadataMapItem.fieldsById[fieldMetadataId]; if (!fieldMetadata) { transformedEntries = { ...transformedEntries, [key]: value }; 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 58d1869f9..43bf1f771 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 @@ -163,9 +163,13 @@ export class SearchService { const queryBuilder = entityManager.createQueryBuilder(); const queryParser = new GraphqlQueryParser( - objectMetadataItem.fieldsByName, - objectMetadataItem.fieldsByJoinColumnName, - generateObjectMetadataMaps([objectMetadataItem]), + objectMetadataItem, + generateObjectMetadataMaps([ + { + ...objectMetadataItem, + fields: Object.values(objectMetadataItem.fieldsById), + }, + ]), ); queryParser.applyFilterToBuilder( diff --git a/packages/twenty-server/src/engine/dataloaders/dataloader.interface.ts b/packages/twenty-server/src/engine/dataloaders/dataloader.interface.ts index 210ac0b28..4d983937b 100644 --- a/packages/twenty-server/src/engine/dataloaders/dataloader.interface.ts +++ b/packages/twenty-server/src/engine/dataloaders/dataloader.interface.ts @@ -2,9 +2,12 @@ import DataLoader from 'dataloader'; import { FieldMetadataLoaderPayload, + IndexMetadataLoaderPayload, RelationLoaderPayload, } from 'src/engine/dataloaders/dataloader.service'; +import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { IndexMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-metadata.dto'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; export interface IDataloaders { @@ -20,6 +23,11 @@ export interface IDataloaders { fieldMetadataLoader: DataLoader< FieldMetadataLoaderPayload, - FieldMetadataEntity[] + FieldMetadataDTO[] + >; + + indexMetadataLoader: DataLoader< + IndexMetadataLoaderPayload, + IndexMetadataDTO[] >; } diff --git a/packages/twenty-server/src/engine/dataloaders/dataloader.module.ts b/packages/twenty-server/src/engine/dataloaders/dataloader.module.ts index 8b6fe8ac0..ce677d17e 100644 --- a/packages/twenty-server/src/engine/dataloaders/dataloader.module.ts +++ b/packages/twenty-server/src/engine/dataloaders/dataloader.module.ts @@ -2,9 +2,10 @@ import { Module } from '@nestjs/common'; import { DataloaderService } from 'src/engine/dataloaders/dataloader.service'; import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module'; +import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module'; @Module({ - imports: [FieldMetadataModule], + imports: [FieldMetadataModule, WorkspaceMetadataCacheModule], providers: [DataloaderService], exports: [DataloaderService], }) diff --git a/packages/twenty-server/src/engine/dataloaders/dataloader.service.ts b/packages/twenty-server/src/engine/dataloaders/dataloader.service.ts index 5d7e91248..631bee561 100644 --- a/packages/twenty-server/src/engine/dataloaders/dataloader.service.ts +++ b/packages/twenty-server/src/engine/dataloaders/dataloader.service.ts @@ -6,10 +6,13 @@ import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metada import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; import { IDataloaders } from 'src/engine/dataloaders/dataloader.interface'; +import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/field-metadata.service'; import { FieldMetadataRelationService } from 'src/engine/metadata-modules/field-metadata/relation/field-metadata-relation.service'; +import { IndexMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-metadata.dto'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service'; export type RelationMetadataLoaderPayload = { workspaceId: string; @@ -36,20 +39,28 @@ export type FieldMetadataLoaderPayload = { objectMetadata: Pick; }; +export type IndexMetadataLoaderPayload = { + workspaceId: string; + objectMetadata: Pick; +}; + @Injectable() export class DataloaderService { constructor( private readonly fieldMetadataRelationService: FieldMetadataRelationService, private readonly fieldMetadataService: FieldMetadataService, + private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService, ) {} createLoaders(): IDataloaders { const relationLoader = this.createRelationLoader(); const fieldMetadataLoader = this.createFieldMetadataLoader(); + const indexMetadataLoader = this.createIndexMetadataLoader(); return { relationLoader, fieldMetadataLoader, + indexMetadataLoader, }; } @@ -78,20 +89,67 @@ export class DataloaderService { }); } - private createFieldMetadataLoader() { - return new DataLoader( - async (dataLoaderParams: FieldMetadataLoaderPayload[]) => { + private createIndexMetadataLoader() { + return new DataLoader( + async (dataLoaderParams: IndexMetadataLoaderPayload[]) => { const workspaceId = dataLoaderParams[0].workspaceId; - const objectMetadataItems = dataLoaderParams.map( - (dataLoaderParam) => dataLoaderParam.objectMetadata, + const objectMetadataIds = dataLoaderParams.map( + (dataLoaderParam) => dataLoaderParam.objectMetadata.id, ); - const fieldMetadataCollection = - await this.fieldMetadataService.getFieldMetadataItemsByBatch( - objectMetadataItems.map((item) => item.id), - workspaceId, + const { objectMetadataMaps } = + await this.workspaceMetadataCacheService.getExistingOrRecomputeMetadataMaps( + { workspaceId }, ); + const indexMetadataCollection = objectMetadataIds.map((id) => + Object.values(objectMetadataMaps.byId[id].indexMetadatas).map( + (indexMetadata) => { + return { + ...indexMetadata, + createdAt: new Date(indexMetadata.createdAt), + updatedAt: new Date(indexMetadata.updatedAt), + id: indexMetadata.id, + indexWhereClause: indexMetadata.indexWhereClause ?? undefined, + objectMetadataId: id, + workspaceId: workspaceId, + }; + }, + ), + ); + + return indexMetadataCollection; + }, + ); + } + + private createFieldMetadataLoader() { + return new DataLoader( + async (dataLoaderParams: FieldMetadataLoaderPayload[]) => { + const workspaceId = dataLoaderParams[0].workspaceId; + const objectMetadataIds = dataLoaderParams.map( + (dataLoaderParam) => dataLoaderParam.objectMetadata.id, + ); + + const { objectMetadataMaps } = + await this.workspaceMetadataCacheService.getExistingOrRecomputeMetadataMaps( + { workspaceId }, + ); + + const fieldMetadataCollection = objectMetadataIds.map((id) => + Object.values(objectMetadataMaps.byId[id].fieldsById).map( + // TODO: fix this as we should merge FieldMetadataEntity and FieldMetadataInterface + (fieldMetadata) => { + return { + ...fieldMetadata, + createdAt: new Date(fieldMetadata.createdAt), + updatedAt: new Date(fieldMetadata.updatedAt), + workspaceId: workspaceId, + }; + }, + ), + ); + return fieldMetadataCollection; }, ); diff --git a/packages/twenty-server/src/engine/metadata-modules/data-source/data-source.entity.ts b/packages/twenty-server/src/engine/metadata-modules/data-source/data-source.entity.ts index 3553cf594..41eb83afa 100644 --- a/packages/twenty-server/src/engine/metadata-modules/data-source/data-source.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/data-source/data-source.entity.ts @@ -1,11 +1,12 @@ import { Column, CreateDateColumn, + DataSourceOptions, Entity, + Index, + OneToMany, PrimaryGeneratedColumn, UpdateDateColumn, - DataSourceOptions, - OneToMany, } from 'typeorm'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; @@ -13,6 +14,7 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat export type DataSourceType = DataSourceOptions['type']; @Entity('dataSource') +@Index('IDX_DATA_SOURCE_WORKSPACE_ID_CREATED_AT', ['workspaceId', 'createdAt']) export class DataSourceEntity { @PrimaryGeneratedColumn('uuid') id: string; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts index a346cf97b..5a1460654 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts @@ -17,7 +17,6 @@ import { import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface'; import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface'; import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface'; -import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; import { FieldStandardOverridesDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-standard-overrides.dto'; import { IndexFieldMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-field-metadata.entity'; @@ -41,8 +40,7 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat ]) export class FieldMetadataEntity< T extends FieldMetadataType = FieldMetadataType, -> implements FieldMetadataInterface -{ +> { @PrimaryGeneratedColumn('uuid') id: string; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.module.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.module.ts index e17fd871c..2a423aebb 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.module.ts @@ -24,6 +24,7 @@ import { IsFieldMetadataOptions } from 'src/engine/metadata-modules/field-metada import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module'; import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; +import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module'; import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module'; import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module'; import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; @@ -54,6 +55,7 @@ import { UpdateFieldInput } from './dtos/update-field.input'; ActorModule, ViewModule, PermissionsModule, + WorkspaceMetadataCacheModule, ], services: [ IsFieldMetadataDefaultValue, diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts index 29e1c4147..321422da2 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts @@ -10,6 +10,7 @@ import { isDefined } from 'twenty-shared/utils'; import { DataSource, FindOneOptions, In, Repository } from 'typeorm'; import { v4 as uuidV4, v4 } from 'uuid'; +import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { settings } from 'src/engine/constants/settings'; @@ -40,9 +41,9 @@ import { generateNullable } from 'src/engine/metadata-modules/field-metadata/uti 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 { isSelectOrMultiSelectFieldMetadata } from 'src/engine/metadata-modules/field-metadata/utils/is-select-or-multi-select-field-metadata.util'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-on-delete-action.type'; +import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; 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.utils'; @@ -50,6 +51,7 @@ import { computeMetadataNameFromLabel, validateNameAndLabelAreSyncOrThrow, } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util'; +import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service'; import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service'; import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util'; import { @@ -78,8 +80,8 @@ type ValidateFieldMetadataArgs = { fieldMetadataType: FieldMetadataType; fieldMetadataInput: T; - objectMetadata: ObjectMetadataEntity; - existingFieldMetadata?: FieldMetadataEntity; + objectMetadata: ObjectMetadataItemWithFieldMaps; + existingFieldMetadata?: FieldMetadataInterface; }; @Injectable() @@ -89,8 +91,6 @@ export class FieldMetadataService extends TypeOrmQueryService, - @InjectRepository(ObjectMetadataEntity, 'core') - private readonly objectMetadataRepository: Repository, private readonly workspaceMigrationFactory: WorkspaceMigrationFactory, private readonly workspaceMigrationService: WorkspaceMigrationService, private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService, @@ -100,6 +100,7 @@ export class FieldMetadataService extends TypeOrmQueryService { + const { objectMetadataMaps } = + await this.workspaceMetadataCacheService.getExistingOrRecomputeMetadataMaps( + { workspaceId: fieldMetadataInput.workspaceId }, + ); + + let existingFieldMetadata: FieldMetadataInterface | undefined; + + for (const objectMetadataItem of Object.values(objectMetadataMaps.byId)) { + const fieldMetadata = objectMetadataItem.fieldsById[id]; + + if (fieldMetadata) { + existingFieldMetadata = fieldMetadata; + break; + } + } + + if (!isDefined(existingFieldMetadata)) { + throw new FieldMetadataException( + 'Field does not exist', + FieldMetadataExceptionCode.FIELD_METADATA_NOT_FOUND, + ); + } + + const objectMetadataItemWithFieldMaps = + objectMetadataMaps.byId[existingFieldMetadata.objectMetadataId]; + const queryRunner = this.coreDataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { - const fieldMetadataRepository = - queryRunner.manager.getRepository( - FieldMetadataEntity, - ); - - const [existingFieldMetadata] = await fieldMetadataRepository.find({ - where: { - id, - workspaceId: fieldMetadataInput.workspaceId, - }, - }); - - if (!isDefined(existingFieldMetadata)) { - throw new FieldMetadataException( - 'Field does not exist', - FieldMetadataExceptionCode.FIELD_METADATA_NOT_FOUND, - ); - } - - const [objectMetadata] = await this.objectMetadataRepository.find({ - where: { - id: existingFieldMetadata.objectMetadataId, - workspaceId: fieldMetadataInput.workspaceId, - }, - relations: ['fields'], - order: {}, - }); - - if (!isDefined(objectMetadata)) { - throw new FieldMetadataException( - 'Object metadata does not exist', - FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND, - ); - } - - if (!isDefined(objectMetadata.labelIdentifierFieldMetadataId)) { + if ( + !isDefined( + objectMetadataItemWithFieldMaps.labelIdentifierFieldMetadataId, + ) + ) { throw new FieldMetadataException( 'Label identifier field metadata id does not exist', FieldMetadataExceptionCode.LABEL_IDENTIFIER_FIELD_METADATA_ID_NOT_FOUND, ); } - assertMutationNotOnRemoteObject(objectMetadata); + assertMutationNotOnRemoteObject(objectMetadataItemWithFieldMaps); assertDoesNotNullifyDefaultValueForNonNullableField({ isNullable: existingFieldMetadata.isNullable, @@ -180,19 +176,9 @@ export class FieldMetadataService extends TypeOrmQueryService, - ) { - const [fieldMetadata] = await this.fieldMetadataRepository.find({ - ...options, - where: { - ...options?.where, - id, - }, - }); - - if (!fieldMetadata) { - throw new FieldMetadataException( - 'Field does not exist', - FieldMetadataExceptionCode.FIELD_METADATA_NOT_FOUND, - ); - } - - return fieldMetadata; - } - public async findOneWithinWorkspace( workspaceId: string, options: FindOneOptions, @@ -509,7 +486,10 @@ export class FieldMetadataService extends TypeOrmQueryService, ) { const updatableStandardFieldInput: UpdateFieldInput & { standardOverrides?: FieldStandardOverridesDTO; @@ -754,7 +734,7 @@ export class FieldMetadataService extends TypeOrmQueryService, ): Promise { if (!fieldMetadataInput.isRemoteCreation) { @@ -841,7 +821,7 @@ export class FieldMetadataService extends TypeOrmQueryService; + objectMetadataMap: Record; isRemoteCreation: boolean; }): Promise { if (isRemoteCreation) { @@ -886,6 +866,11 @@ export class FieldMetadataService extends TypeOrmQueryService ({ ...acc, [obj.id]: obj }), - {} as Record, - ); - const createdFieldMetadatas: FieldMetadataEntity[] = []; const migrationActions: WorkspaceMigrationTableAction[] = []; for (const objectMetadataId of objectMetadataIds) { - const objectMetadata = objectMetadataMap[objectMetadataId]; + const objectMetadata = objectMetadataMaps.byId[objectMetadataId]; if (!isDefined(objectMetadata)) { throw new FieldMetadataException( @@ -941,7 +914,7 @@ export class FieldMetadataService extends TypeOrmQueryService = { validator: (str: T) => boolean; message: string }; type FieldMetadataUpdateCreateInput = CreateFieldInput | UpdateFieldInput; type ValidateEnumFieldMetadataArgs = { - existingFieldMetadata?: FieldMetadataEntity; + existingFieldMetadata?: Pick< + FieldMetadataInterface, + 'type' | 'isNullable' | 'defaultValue' | 'options' + >; fieldMetadataInput: FieldMetadataUpdateCreateInput; fieldMetadataType: EnumFieldMetadataUnionType; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-select-or-multi-select-field-metadata.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-select-or-multi-select-field-metadata.util.ts index be36683f7..34e584729 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-select-or-multi-select-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-select-or-multi-select-field-metadata.util.ts @@ -1,17 +1,15 @@ import { FieldMetadataType } from 'twenty-shared/types'; +import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; + import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; export type SelectOrMultiSelectFieldMetadataEntity = FieldMetadataEntity< FieldMetadataType.SELECT | FieldMetadataType.MULTI_SELECT >; export const isSelectOrMultiSelectFieldMetadata = ( - fieldMetadata: unknown, + fieldMetadata: FieldMetadataInterface, ): fieldMetadata is SelectOrMultiSelectFieldMetadataEntity => { - if (!(fieldMetadata instanceof FieldMetadataEntity)) { - return false; - } - return [FieldMetadataType.SELECT, FieldMetadataType.MULTI_SELECT].includes( fieldMetadata.type, ); diff --git a/packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.entity.ts b/packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.entity.ts index 6d1f021d0..7a2f58b34 100644 --- a/packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.entity.ts @@ -80,5 +80,5 @@ export class IndexMetadataEntity { default: IndexType.BTREE, nullable: false, }) - indexType?: IndexType; + indexType: IndexType; } diff --git a/packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.service.ts index 7617d85ef..dc7d43bff 100644 --- a/packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.service.ts @@ -106,7 +106,10 @@ export class IndexMetadataService { async recomputeIndexMetadataForObject( workspaceId: string, - updatedObjectMetadata: ObjectMetadataEntity, + updatedObjectMetadata: Pick< + ObjectMetadataEntity, + 'nameSingular' | 'isCustom' | 'id' + >, ) { const indexesToRecompute = await this.indexMetadataRepository.find({ where: { @@ -232,7 +235,10 @@ export class IndexMetadataService { async createIndexRecomputeMigrations( workspaceId: string, - objectMetadata: ObjectMetadataEntity, + objectMetadata: Pick< + ObjectMetadataEntity, + 'nameSingular' | 'isCustom' | 'id' + >, recomputedIndexes: { indexMetadata: IndexMetadataEntity; previousName: string; diff --git a/packages/twenty-server/src/engine/metadata-modules/index-metadata/interfaces/index-field-metadata.interface.ts b/packages/twenty-server/src/engine/metadata-modules/index-metadata/interfaces/index-field-metadata.interface.ts index f9fd9c6f3..30bd90bbb 100644 --- a/packages/twenty-server/src/engine/metadata-modules/index-metadata/interfaces/index-field-metadata.interface.ts +++ b/packages/twenty-server/src/engine/metadata-modules/index-metadata/interfaces/index-field-metadata.interface.ts @@ -8,4 +8,6 @@ export interface IndexFieldMetadataInterface { fieldMetadata: FieldMetadataInterface; indexMetadata: IndexMetadataInterface; order: number; + createdAt: Date; + updatedAt: Date; } diff --git a/packages/twenty-server/src/engine/metadata-modules/index-metadata/interfaces/index-metadata.interface.ts b/packages/twenty-server/src/engine/metadata-modules/index-metadata/interfaces/index-metadata.interface.ts index 2b928a967..01c3afef2 100644 --- a/packages/twenty-server/src/engine/metadata-modules/index-metadata/interfaces/index-metadata.interface.ts +++ b/packages/twenty-server/src/engine/metadata-modules/index-metadata/interfaces/index-metadata.interface.ts @@ -1,7 +1,14 @@ import { IndexFieldMetadataInterface } from 'src/engine/metadata-modules/index-metadata/interfaces/index-field-metadata.interface'; +import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; + export interface IndexMetadataInterface { + id: string; name: string; isUnique: boolean; indexFieldMetadatas: IndexFieldMetadataInterface[]; + createdAt: Date; + updatedAt: Date; + indexWhereClause: string | null; + indexType: IndexType; } diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts index edb32510e..aa69cea79 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts @@ -26,6 +26,7 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; import { RemoteTableRelationsModule } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/remote-table-relations.module'; import { SearchVectorModule } from 'src/engine/metadata-modules/search-vector/search-vector.module'; +import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module'; import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module'; import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module'; import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module'; @@ -59,6 +60,7 @@ import { UpdateObjectPayload } from './dtos/update-object.input'; PermissionsModule, WorkspacePermissionsCacheModule, WorkspaceCacheStorageModule, + WorkspaceMetadataCacheModule, ], services: [ ObjectMetadataService, diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.resolver.ts index 797c90a19..3b87fe4db 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.resolver.ts @@ -17,6 +17,7 @@ import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorat import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto'; +import { IndexMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-metadata.dto'; import { DeleteOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/delete-object.input'; import { ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto'; import { @@ -150,4 +151,26 @@ export class ObjectMetadataResolver { return []; } } + + @ResolveField(() => [IndexMetadataDTO], { nullable: false }) + async indexMetadataList( + @AuthWorkspace() workspace: Workspace, + @Parent() objectMetadata: ObjectMetadataDTO, + @Context() context: { loaders: IDataloaders }, + ): Promise { + try { + const indexMetadataItems = await context.loaders.indexMetadataLoader.load( + { + objectMetadata, + workspaceId: workspace.id, + }, + ); + + return indexMetadataItems; + } catch (error) { + objectMetadataGraphqlApiExceptionHandler(error); + + return []; + } + } } diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts index 7799c602f..f7b2a6cf0 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts @@ -4,11 +4,9 @@ import { InjectRepository } from '@nestjs/typeorm'; import { i18n } from '@lingui/core'; import { Query, QueryOptions } from '@ptc-org/nestjs-query-core'; import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; -import { APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations'; +import { APP_LOCALES } from 'twenty-shared/translations'; import { capitalize, isDefined } from 'twenty-shared/utils'; -import { FindManyOptions, FindOneOptions, In, Not, Repository } from 'typeorm'; - -import { ObjectMetadataStandardIdToIdMap } from 'src/engine/metadata-modules/object-metadata/interfaces/object-metadata-standard-id-to-id-map'; +import { FindManyOptions, FindOneOptions, In, Repository } from 'typeorm'; import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; @@ -35,7 +33,10 @@ import { } from 'src/engine/metadata-modules/object-metadata/utils/validate-object-metadata-input.util'; import { RemoteTableRelationsService } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/remote-table-relations.service'; import { SearchVectorService } from 'src/engine/metadata-modules/search-vector/search-vector.service'; +import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { validateNameAndLabelAreSyncOrThrow } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util'; +import { validatesNoOtherObjectWithSameNameExistsOrThrows } from 'src/engine/metadata-modules/utils/validate-no-other-object-with-same-name-exists-or-throw.util'; +import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service'; import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service'; import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; @@ -58,6 +59,7 @@ export class ObjectMetadataService extends TypeOrmQueryService { + const { objectMetadataMaps } = + await this.workspaceMetadataCacheService.getExistingOrRecomputeMetadataMaps( + { + workspaceId: objectMetadataInput.workspaceId, + }, + ); + const lastDataSourceMetadata = await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail( objectMetadataInput.workspaceId, @@ -131,13 +140,28 @@ export class ObjectMetadataService extends TypeOrmQueryService field.standardId === CUSTOM_OBJECT_STANDARD_FIELD_IDS.name, + )?.id; + + if (!labelIdentifierFieldMetadataId) { + throw new ObjectMetadataException( + 'Label identifier field metadata not created properly', + ObjectMetadataExceptionCode.MISSING_CUSTOM_OBJECT_DEFAULT_LABEL_IDENTIFIER_FIELD, + ); + } + + const createdObjectMetadata = await this.objectMetadataRepository.save({ ...objectMetadataInput, dataSourceId: lastDataSourceMetadata.id, targetTableName: 'DEPRECATED', @@ -146,24 +170,8 @@ export class ObjectMetadataService extends TypeOrmQueryService field.standardId === CUSTOM_OBJECT_STANDARD_FIELD_IDS.name, - ); - - if (!labelIdentifierFieldMetadata) { - throw new ObjectMetadataException( - 'Label identifier field metadata not created properly', - ObjectMetadataExceptionCode.MISSING_CUSTOM_OBJECT_DEFAULT_LABEL_IDENTIFIER_FIELD, - ); - } - - await this.objectMetadataRepository.update(createdObjectMetadata.id, { - labelIdentifierFieldMetadataId: labelIdentifierFieldMetadata.id, + fields: objectMetadataInput.isRemote ? [] : baseCustomFields, + labelIdentifierFieldMetadataId, }); if (objectMetadataInput.isRemote) { @@ -174,6 +182,13 @@ export class ObjectMetadataService extends TypeOrmQueryService { + const { objectMetadataMaps } = + await this.workspaceMetadataCacheService.getExistingOrRecomputeMetadataMaps( + { workspaceId }, + ); + const inputId = input.id; const inputPayload = { @@ -238,9 +251,7 @@ export class ObjectMetadataService extends TypeOrmQueryService) { - return this.objectMetadataRepository.find({ - relations: ['fields'], - ...options, - where: { - ...options?.where, - }, - }); - } - public async deleteObjectsMetadata(workspaceId: string) { await this.objectMetadataRepository.delete({ workspaceId }); } - public async getObjectMetadataStandardIdToIdMap(workspaceId: string) { - const objectMetadata = await this.findManyWithinWorkspace(workspaceId); - - const objectMetadataStandardIdToIdMap = - objectMetadata.reduce((acc, object) => { - acc[object.standardId ?? ''] = { - id: object.id, - fields: object.fields.reduce((acc, field) => { - // @ts-expect-error legacy noImplicitAny - acc[field.standardId ?? ''] = field.id; - - return acc; - }, {}), - }; - - return acc; - }, {}); - - return { objectMetadataStandardIdToIdMap }; - } - private async handleObjectNameAndLabelUpdates( - existingObjectMetadata: ObjectMetadataEntity, - objectMetadataForUpdate: ObjectMetadataEntity, + existingObjectMetadata: Pick< + ObjectMetadataItemWithFieldMaps, + 'nameSingular' | 'isCustom' | 'id' | 'labelPlural' | 'icon' | 'fieldsById' + >, + objectMetadataForUpdate: Pick< + ObjectMetadataItemWithFieldMaps, + | 'nameSingular' + | 'isCustom' + | 'workspaceId' + | 'id' + | 'labelSingular' + | 'labelPlural' + | 'icon' + | 'fieldsById' + >, inputPayload: UpdateObjectPayload, ) { const newTargetTableName = computeObjectTargetTable( @@ -533,45 +525,6 @@ export class ObjectMetadataService extends TypeOrmQueryService => { - const baseWhereConditions = [ - { nameSingular: objectMetadataNameSingular, workspaceId }, - { nameSingular: objectMetadataNamePlural, workspaceId }, - { namePlural: objectMetadataNameSingular, workspaceId }, - { namePlural: objectMetadataNamePlural, workspaceId }, - ]; - - const whereConditions = baseWhereConditions.map((condition) => { - return { - ...condition, - ...(isDefined(existingObjectMetadataId) - ? { id: Not(In([existingObjectMetadataId])) } - : {}), - }; - }); - - const objectAlreadyExists = await this.objectMetadataRepository.findOne({ - where: whereConditions, - }); - - if (objectAlreadyExists) { - throw new ObjectMetadataException( - 'Object already exists', - ObjectMetadataExceptionCode.OBJECT_ALREADY_EXISTS, - ); - } - }; - async resolveOverridableString( objectMetadata: ObjectMetadataDTO, labelKey: 'labelPlural' | 'labelSingular' | 'description' | 'icon', @@ -581,17 +534,6 @@ export class ObjectMetadataService extends TypeOrmQueryService, + objectMetadataMaps: ObjectMetadataMaps, ) { const relatedObjectMetadataCollection = await Promise.all( DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS.map( async (relationObjectMetadataStandardId) => - this.createRelationAndForeignKeyMetadata( + this.createRelationAndForeignKeyMetadata({ workspaceId, sourceObjectMetadata, relationObjectMetadataStandardId, - ), + objectMetadataMaps, + }), ), ); return relatedObjectMetadataCollection; } - private async createRelationAndForeignKeyMetadata( - workspaceId: string, - sourceObjectMetadata: ObjectMetadataEntity, - relationObjectMetadataStandardId: string, - ) { - const targetObjectMetadata = - await this.objectMetadataRepository.findOneByOrFail({ - standardId: relationObjectMetadataStandardId, - workspaceId: workspaceId, - isCustom: false, - }); + private async createRelationAndForeignKeyMetadata({ + workspaceId, + sourceObjectMetadata, + relationObjectMetadataStandardId, + objectMetadataMaps, + }: { + workspaceId: string; + sourceObjectMetadata: Pick< + ObjectMetadataItemWithFieldMaps, + 'id' | 'nameSingular' | 'labelSingular' + >; + objectMetadataMaps: ObjectMetadataMaps; + relationObjectMetadataStandardId: string; + }) { + const targetObjectMetadata = Object.values(objectMetadataMaps.byId).find( + (objectMetadata) => + objectMetadata.standardId === relationObjectMetadataStandardId, + ); + + if (!targetObjectMetadata) { + throw new Error( + `Target object metadata not found for standard ID: ${relationObjectMetadataStandardId}`, + ); + } await this.createFieldMetadataRelation( workspaceId, @@ -80,8 +100,11 @@ export class ObjectMetadataFieldRelationService { private async createFieldMetadataRelation( workspaceId: string, - sourceObjectMetadata: ObjectMetadataEntity, - targetObjectMetadata: ObjectMetadataEntity, + sourceObjectMetadata: Pick< + ObjectMetadataItemWithFieldMaps, + 'id' | 'nameSingular' | 'labelSingular' + >, + targetObjectMetadata: ObjectMetadataItemWithFieldMaps, ): Promise[]> { const sourceFieldMetadata = this.createSourceFieldMetadata( workspaceId, @@ -119,7 +142,10 @@ export class ObjectMetadataFieldRelationService { public async updateRelationsAndForeignKeysMetadata( workspaceId: string, - updatedObjectMetadata: ObjectMetadataEntity, + updatedObjectMetadata: Pick< + ObjectMetadataEntity, + 'nameSingular' | 'isCustom' | 'id' | 'labelSingular' + >, ): Promise< { targetObjectMetadata: ObjectMetadataEntity; @@ -141,7 +167,10 @@ export class ObjectMetadataFieldRelationService { private async updateRelationAndForeignKeyMetadata( workspaceId: string, - sourceObjectMetadata: ObjectMetadataEntity, + sourceObjectMetadata: Pick< + ObjectMetadataEntity, + 'nameSingular' | 'id' | 'isCustom' | 'labelSingular' + >, targetObjectMetadataStandardId: string, ) { const targetObjectMetadata = @@ -226,8 +255,14 @@ export class ObjectMetadataFieldRelationService { private createSourceFieldMetadata( workspaceId: string, - sourceObjectMetadata: ObjectMetadataEntity, - targetObjectMetadata: ObjectMetadataEntity, + sourceObjectMetadata: Pick< + ObjectMetadataItemWithFieldMaps, + 'labelSingular' | 'id' + >, + targetObjectMetadata: Pick< + ObjectMetadataItemWithFieldMaps, + 'namePlural' | 'labelSingular' + >, ): Partial> { const relationObjectMetadataNamePlural = targetObjectMetadata.namePlural; @@ -261,8 +296,8 @@ export class ObjectMetadataFieldRelationService { } private updateSourceFieldMetadata( - sourceObjectMetadata: ObjectMetadataEntity, - targetObjectMetadata: ObjectMetadataEntity, + sourceObjectMetadata: Pick, + targetObjectMetadata: Pick, ) { const relationObjectMetadataNamePlural = targetObjectMetadata.namePlural; @@ -280,8 +315,14 @@ export class ObjectMetadataFieldRelationService { private createTargetFieldMetadata( workspaceId: string, - sourceObjectMetadata: ObjectMetadataEntity, - targetObjectMetadata: ObjectMetadataEntity, + sourceObjectMetadata: Pick< + ObjectMetadataItemWithFieldMaps, + 'labelSingular' | 'id' | 'nameSingular' + >, + targetObjectMetadata: Pick< + ObjectMetadataItemWithFieldMaps, + 'namePlural' | 'labelSingular' | 'id' | 'nameSingular' + >, ): Partial> { const customStandardFieldId = // @ts-expect-error legacy noImplicitAny @@ -319,8 +360,14 @@ export class ObjectMetadataFieldRelationService { } private updateTargetFieldMetadata( - sourceObjectMetadata: ObjectMetadataEntity, - targetObjectMetadata: ObjectMetadataEntity, + sourceObjectMetadata: Pick< + ObjectMetadataEntity, + 'nameSingular' | 'labelSingular' + >, + targetObjectMetadata: Pick< + ObjectMetadataEntity, + 'nameSingular' | 'namePlural' + >, ) { const customStandardFieldId = // @ts-expect-error legacy noImplicitAny diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/services/object-metadata-migration.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/services/object-metadata-migration.service.ts index d1477f398..23683d271 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/services/object-metadata-migration.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/services/object-metadata-migration.service.ts @@ -2,13 +2,14 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { FieldMetadataType } from 'twenty-shared/types'; -import { In, Repository } from 'typeorm'; +import { Repository } from 'typeorm'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { buildMigrationsForCustomObjectRelations } from 'src/engine/metadata-modules/object-metadata/utils/build-migrations-for-custom-object-relations.util'; +import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util'; import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util'; import { @@ -73,8 +74,14 @@ export class ObjectMetadataMigrationService { } public async createRelationMigrations( - createdObjectMetadata: ObjectMetadataEntity, - relatedObjectMetadataCollection: ObjectMetadataEntity[], + createdObjectMetadata: Pick< + ObjectMetadataItemWithFieldMaps, + 'nameSingular' | 'workspaceId' | 'isCustom' + >, + relatedObjectMetadataCollection: Pick< + ObjectMetadataItemWithFieldMaps, + 'nameSingular' | 'isCustom' + >[], ) { await this.workspaceMigrationService.createCustomMigration( generateMigrationName( @@ -89,8 +96,14 @@ export class ObjectMetadataMigrationService { } public async createRenameTableMigration( - existingObjectMetadata: ObjectMetadataEntity, - objectMetadataForUpdate: ObjectMetadataEntity, + existingObjectMetadata: Pick< + ObjectMetadataEntity, + 'nameSingular' | 'isCustom' + >, + objectMetadataForUpdate: Pick< + ObjectMetadataEntity, + 'nameSingular' | 'isCustom' + >, workspaceId: string, ) { const newTargetTableName = computeObjectTargetTable( @@ -114,8 +127,8 @@ export class ObjectMetadataMigrationService { } public async updateRelationMigrations( - currentObjectMetadata: ObjectMetadataEntity, - alteredObjectMetadata: ObjectMetadataEntity, + currentObjectMetadata: Pick, + alteredObjectMetadata: Pick, relationMetadataCollection: { targetObjectMetadata: ObjectMetadataEntity; targetFieldMetadata: FieldMetadataEntity; @@ -282,21 +295,22 @@ export class ObjectMetadataMigrationService { } public async recomputeEnumNames( - updatedObjectMetadata: ObjectMetadataEntity, + updatedObjectMetadata: Pick< + ObjectMetadataItemWithFieldMaps, + 'nameSingular' | 'isCustom' | 'id' | 'fieldsById' + >, workspaceId: string, ) { - const fieldMetadataToUpdate = await this.fieldMetadataRepository.find({ - where: { - objectMetadataId: updatedObjectMetadata.id, - workspaceId, - type: In([ - FieldMetadataType.SELECT, - FieldMetadataType.MULTI_SELECT, - FieldMetadataType.RATING, - FieldMetadataType.ACTOR, - ]), - }, - }); + const enumFieldMetadataTypes = [ + FieldMetadataType.SELECT, + FieldMetadataType.MULTI_SELECT, + FieldMetadataType.RATING, + FieldMetadataType.ACTOR, + ]; + + const fieldMetadataToUpdate = Object.values( + updatedObjectMetadata.fieldsById, + ).filter((field) => enumFieldMetadataTypes.includes(field.type)); for (const fieldMetadata of fieldMetadataToUpdate) { await this.workspaceMigrationService.createCustomMigration( diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/services/object-metadata-related-records.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/services/object-metadata-related-records.service.ts index 6fc712541..27a02eaac 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/services/object-metadata-related-records.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/services/object-metadata-related-records.service.ts @@ -83,7 +83,10 @@ export class ObjectMetadataRelatedRecordsService { } public async updateObjectViews( - updatedObjectMetadata: ObjectMetadataEntity, + updatedObjectMetadata: Pick< + ObjectMetadataEntity, + 'id' | 'labelPlural' | 'icon' + >, workspaceId: string, ) { const viewRepository = diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-fields-for-custom-object.util.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-fields-for-custom-object.util.ts index c27e7c521..7a70cbdb3 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-fields-for-custom-object.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-fields-for-custom-object.util.ts @@ -1,4 +1,5 @@ import { FieldMetadataType } from 'twenty-shared/types'; +import { v4 } from 'uuid'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { @@ -10,6 +11,7 @@ export const buildDefaultFieldsForCustomObject = ( workspaceId: string, ): Partial[] => [ { + id: v4(), standardId: BASE_OBJECT_STANDARD_FIELD_IDS.id, type: FieldMetadataType.UUID, name: 'id', @@ -24,6 +26,7 @@ export const buildDefaultFieldsForCustomObject = ( defaultValue: 'uuid', }, { + id: v4(), standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.name, type: FieldMetadataType.TEXT, name: 'name', @@ -37,6 +40,7 @@ export const buildDefaultFieldsForCustomObject = ( defaultValue: "'Untitled'", }, { + id: v4(), standardId: BASE_OBJECT_STANDARD_FIELD_IDS.createdAt, type: FieldMetadataType.DATE_TIME, name: 'createdAt', @@ -50,6 +54,7 @@ export const buildDefaultFieldsForCustomObject = ( defaultValue: 'now', }, { + id: v4(), standardId: BASE_OBJECT_STANDARD_FIELD_IDS.updatedAt, type: FieldMetadataType.DATE_TIME, name: 'updatedAt', @@ -64,6 +69,7 @@ export const buildDefaultFieldsForCustomObject = ( defaultValue: 'now', }, { + id: v4(), standardId: BASE_OBJECT_STANDARD_FIELD_IDS.deletedAt, type: FieldMetadataType.DATE_TIME, name: 'deletedAt', @@ -78,6 +84,7 @@ export const buildDefaultFieldsForCustomObject = ( defaultValue: null, }, { + id: v4(), standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.createdBy, type: FieldMetadataType.ACTOR, name: 'createdBy', @@ -92,6 +99,7 @@ export const buildDefaultFieldsForCustomObject = ( defaultValue: { name: "''", source: "'MANUAL'" }, }, { + id: v4(), standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.position, type: FieldMetadataType.POSITION, name: 'position', diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-migrations-for-custom-object-relations.util.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-migrations-for-custom-object-relations.util.ts index 296628ab3..4c51e2701 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-migrations-for-custom-object-relations.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-migrations-for-custom-object-relations.util.ts @@ -1,6 +1,6 @@ import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-on-delete-action.type'; +import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { WorkspaceMigrationColumnActionType, WorkspaceMigrationColumnCreate, @@ -10,8 +10,14 @@ import { import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; export const buildMigrationsForCustomObjectRelations = ( - createdObjectMetadata: ObjectMetadataEntity, - relatedObjectMetadataCollection: ObjectMetadataEntity[], + createdObjectMetadata: Pick< + ObjectMetadataItemWithFieldMaps, + 'nameSingular' | 'isCustom' + >, + relatedObjectMetadataCollection: Pick< + ObjectMetadataItemWithFieldMaps, + 'nameSingular' | 'isCustom' + >[], ): WorkspaceMigrationTableAction[] => { const migrations: WorkspaceMigrationTableAction[] = []; diff --git a/packages/twenty-server/src/engine/metadata-modules/object-permission/__tests__/object-permission.service.spec.ts b/packages/twenty-server/src/engine/metadata-modules/object-permission/__tests__/object-permission.service.spec.ts index db67879b4..7834af334 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-permission/__tests__/object-permission.service.spec.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-permission/__tests__/object-permission.service.spec.ts @@ -218,7 +218,6 @@ describe('ObjectPermissionService', () => { ).toHaveBeenCalledWith({ workspaceId, roleIds: [roleId], - ignoreLock: true, }); }); diff --git a/packages/twenty-server/src/engine/metadata-modules/object-permission/object-permission.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-permission/object-permission.service.ts index ff8a15804..5691a4646 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-permission/object-permission.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-permission/object-permission.service.ts @@ -101,7 +101,6 @@ export class ObjectPermissionService { { workspaceId, roleIds: [input.roleId], - ignoreLock: true, }, ); diff --git a/packages/twenty-server/src/engine/metadata-modules/role/role.service.ts b/packages/twenty-server/src/engine/metadata-modules/role/role.service.ts index d842318ed..117aff634 100644 --- a/packages/twenty-server/src/engine/metadata-modules/role/role.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/role/role.service.ts @@ -82,7 +82,6 @@ export class RoleService { await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache({ workspaceId, roleIds: [role.id], - ignoreLock: true, }); return role; @@ -128,7 +127,6 @@ export class RoleService { await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache({ workspaceId, roleIds: [input.id], - ignoreLock: true, }); return { ...existingRole, ...updatedRole }; @@ -195,7 +193,6 @@ export class RoleService { await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache({ workspaceId, - ignoreLock: true, }); return roleId; diff --git a/packages/twenty-server/src/engine/metadata-modules/setting-permission/setting-permission.service.ts b/packages/twenty-server/src/engine/metadata-modules/setting-permission/setting-permission.service.ts index e02657d51..4d995cfca 100644 --- a/packages/twenty-server/src/engine/metadata-modules/setting-permission/setting-permission.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/setting-permission/setting-permission.service.ts @@ -122,7 +122,6 @@ export class SettingPermissionService { { workspaceId, roleIds: [input.roleId], - ignoreLock: true, }, ); } diff --git a/packages/twenty-server/src/engine/metadata-modules/types/object-metadata-item-with-field-maps.ts b/packages/twenty-server/src/engine/metadata-modules/types/object-metadata-item-with-field-maps.ts index 6460e3d37..15e6dd721 100644 --- a/packages/twenty-server/src/engine/metadata-modules/types/object-metadata-item-with-field-maps.ts +++ b/packages/twenty-server/src/engine/metadata-modules/types/object-metadata-item-with-field-maps.ts @@ -1,9 +1,14 @@ import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; +import { IndexMetadataInterface } from 'src/engine/metadata-modules/index-metadata/interfaces/index-metadata.interface'; import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; -export type ObjectMetadataItemWithFieldMaps = ObjectMetadataInterface & { +export type ObjectMetadataItemWithFieldMaps = Omit< + ObjectMetadataInterface, + 'fields' +> & { fieldsById: FieldMetadataMap; - fieldsByName: FieldMetadataMap; - fieldsByJoinColumnName: FieldMetadataMap; + fieldIdByJoinColumnName: Record; + fieldIdByName: Record; + indexMetadatas: IndexMetadataInterface[]; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/user-role/user-role.service.ts b/packages/twenty-server/src/engine/metadata-modules/user-role/user-role.service.ts index 1cf87146a..b9b595138 100644 --- a/packages/twenty-server/src/engine/metadata-modules/user-role/user-role.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/user-role/user-role.service.ts @@ -62,7 +62,6 @@ export class UserRoleService { await this.workspacePermissionsCacheService.recomputeUserWorkspaceRoleMapCache( { workspaceId, - ignoreLock: true, }, ); } 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 95201e893..50616fe8e 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 @@ -6,7 +6,7 @@ import { FIELD_CURRENCY_MOCK_NAME, FIELD_FULL_NAME_MOCK_NAME, FIELD_LINKS_MOCK_NAME, - objectMetadataItemMock, + objectMetadataMapItemMock, } from 'src/engine/api/__mocks__/object-metadata-item.mock'; import { validateFieldNameAvailabilityOrThrow } from 'src/engine/metadata-modules/utils/validate-field-name-availability.utils'; @@ -57,18 +57,22 @@ const validateFieldNameAvailabilityTestCases: ValidateFieldNameAvailabilityTestC ]; describe('validateFieldNameAvailabilityOrThrow', () => { - const objectMetadata = objectMetadataItemMock; - it.each(validateFieldNameAvailabilityTestCases)( '$title', ({ context: { input, shouldNotThrow } }) => { if (shouldNotThrow) { expect(() => - validateFieldNameAvailabilityOrThrow(input, objectMetadata), + validateFieldNameAvailabilityOrThrow( + input, + objectMetadataMapItemMock, + ), ).not.toThrow(); } else { expect(() => - validateFieldNameAvailabilityOrThrow(input, objectMetadata), + validateFieldNameAvailabilityOrThrow( + input, + objectMetadataMapItemMock, + ), ).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 index 464793fff..60c75bb4a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/utils/generate-object-metadata-maps.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/utils/generate-object-metadata-maps.util.ts @@ -1,3 +1,4 @@ +import omit from 'lodash.omit'; import { FieldMetadataType } from 'twenty-shared/types'; import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; @@ -16,9 +17,7 @@ export const generateObjectMetadataMaps = ( }; for (const objectMetadata of objectMetadataCollection) { - const fieldsByIdMap: FieldMetadataMap = {}; - const fieldsByNameMap: FieldMetadataMap = {}; - const fieldsByJoinColumnNameMap: FieldMetadataMap = {}; + const fieldIdByJoinColumnNameMap: Record = {}; for (const fieldMetadata of objectMetadata.fields) { if ( @@ -28,20 +27,25 @@ export const generateObjectMetadataMaps = ( ) ) { if (fieldMetadata.settings?.joinColumnName) { - fieldsByJoinColumnNameMap[fieldMetadata.settings.joinColumnName] = - fieldMetadata; + fieldIdByJoinColumnNameMap[fieldMetadata.settings.joinColumnName] = + fieldMetadata.id; } } - - fieldsByNameMap[fieldMetadata.name] = fieldMetadata; - fieldsByIdMap[fieldMetadata.id] = fieldMetadata; } + const fieldsByIdMap = objectMetadata.fields.reduce((acc, field) => { + acc[field.id] = field; + + return acc; + }, {} as FieldMetadataMap); + const processedObjectMetadata: ObjectMetadataItemWithFieldMaps = { - ...objectMetadata, + ...omit(objectMetadata, 'fields'), fieldsById: fieldsByIdMap, - fieldsByName: fieldsByNameMap, - fieldsByJoinColumnName: fieldsByJoinColumnNameMap, + fieldIdByName: Object.fromEntries( + Object.entries(fieldsByIdMap).map(([id, field]) => [field.name, id]), + ), + fieldIdByJoinColumnName: fieldIdByJoinColumnNameMap, }; objectMetadataMaps.byId[objectMetadata.id] = processedObjectMetadata; 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 new file mode 100644 index 000000000..fd77be619 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps.ts @@ -0,0 +1,18 @@ +import omit from 'lodash.omit'; + +import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; + +import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; + +export const getObjectMetadataFromObjectMetadataItemWithFieldMaps = ( + objectMetadataMapItem: ObjectMetadataItemWithFieldMaps, +): ObjectMetadataInterface => { + return { + ...omit(objectMetadataMapItem, [ + 'fieldsById', + 'fieldIdByName', + 'fieldIdByJoinColumnName', + ]), + fields: Object.values(objectMetadataMapItem.fieldsById), + }; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/remove-field-maps-from-object-metadata.util.ts b/packages/twenty-server/src/engine/metadata-modules/utils/remove-field-maps-from-object-metadata.util.ts deleted file mode 100644 index b84f0840a..000000000 --- a/packages/twenty-server/src/engine/metadata-modules/utils/remove-field-maps-from-object-metadata.util.ts +++ /dev/null @@ -1,14 +0,0 @@ -import omit from 'lodash.omit'; - -import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; - -import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; - -export const removeFieldMapsFromObjectMetadata = ( - objectMetadata: ObjectMetadataItemWithFieldMaps, -): ObjectMetadataInterface => - omit(objectMetadata, [ - 'fieldsById', - 'fieldsByName', - 'fieldsByJoinColumnName', - ]); 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 c728eef3b..778f9f7c9 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 @@ -1,18 +1,18 @@ import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; 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 { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { InvalidMetadataException, InvalidMetadataExceptionCode, } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception'; const getReservedCompositeFieldNames = ( - objectMetadata: ObjectMetadataEntity, + objectMetadata: ObjectMetadataItemWithFieldMaps, ) => { const reservedCompositeFieldsNames: string[] = []; - for (const field of objectMetadata.fields) { + for (const field of Object.values(objectMetadata.fieldsById)) { if (isCompositeFieldMetadataType(field.type)) { const base = field.name; const compositeType = compositeTypeDefinitions.get(field.type); @@ -30,12 +30,16 @@ const getReservedCompositeFieldNames = ( export const validateFieldNameAvailabilityOrThrow = ( name: string, - objectMetadata: ObjectMetadataEntity, + objectMetadata: ObjectMetadataItemWithFieldMaps, ) => { const reservedCompositeFieldsNames = getReservedCompositeFieldNames(objectMetadata); - if (objectMetadata.fields.some((field) => field.name === name)) { + if ( + Object.values(objectMetadata.fieldsById).some( + (field) => field.name === name, + ) + ) { throw new InvalidMetadataException( `Name "${name}" is not available`, InvalidMetadataExceptionCode.NOT_AVAILABLE, 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 new file mode 100644 index 000000000..fd4f29be8 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/utils/validate-no-other-object-with-same-name-exists-or-throw.util.ts @@ -0,0 +1,35 @@ +import { + ObjectMetadataException, + ObjectMetadataExceptionCode, +} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception'; +import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; + +type ValidateNoOtherObjectWithSameNameExistsOrThrowsParams = { + objectMetadataNameSingular: string; + objectMetadataNamePlural: string; + existingObjectMetadataId?: string; + objectMetadataMaps: ObjectMetadataMaps; +}; + +export const validatesNoOtherObjectWithSameNameExistsOrThrows = ({ + objectMetadataNameSingular, + objectMetadataNamePlural, + existingObjectMetadataId, + objectMetadataMaps, +}: ValidateNoOtherObjectWithSameNameExistsOrThrowsParams) => { + const objectAlreadyExists = Object.values(objectMetadataMaps.byId).find( + (objectMetadata) => + (objectMetadata.nameSingular === objectMetadataNameSingular || + objectMetadata.namePlural === objectMetadataNamePlural || + objectMetadata.nameSingular === objectMetadataNamePlural || + objectMetadata.namePlural === objectMetadataNameSingular) && + objectMetadata.id !== existingObjectMetadataId, + ); + + if (objectAlreadyExists) { + throw new ObjectMetadataException( + 'Object already exists', + ObjectMetadataExceptionCode.OBJECT_ALREADY_EXISTS, + ); + } +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.service.ts index 6013348ee..326a90819 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.service.ts @@ -55,34 +55,9 @@ export class WorkspaceFeatureFlagsMapCacheService { async recomputeFeatureFlagsMapCache({ workspaceId, - ignoreLock = false, }: { workspaceId: string; - ignoreLock?: boolean; }): Promise { - const isAlreadyCaching = - await this.workspaceCacheStorageService.getFeatureFlagsMapOngoingCachingLock( - workspaceId, - ); - - if (isAlreadyCaching) { - if (ignoreLock) { - this.logger.warn( - `Feature flags map cache is already being cached (workspace ${workspaceId}), respecting lock and returning no data`, - ); - - return; - } else { - this.logger.warn( - `Feature flags map cache is already being cached (workspace ${workspaceId}), ignoring lock`, - ); - } - } - - await this.workspaceCacheStorageService.addFeatureFlagMapOngoingCachingLock( - workspaceId, - ); - const freshFeatureFlagMap = await this.getFeatureFlagsMapFromDatabase(workspaceId); @@ -90,10 +65,6 @@ export class WorkspaceFeatureFlagsMapCacheService { workspaceId, freshFeatureFlagMap, ); - - await this.workspaceCacheStorageService.removeFeatureFlagsMapOngoingCachingLock( - workspaceId, - ); } private async getFeatureFlagsMapFromDatabase(workspaceId: string) { diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service.ts index ce7bdcb82..2b4547409 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service.ts @@ -2,9 +2,10 @@ import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { isDefined } from 'twenty-shared/utils'; -import { Repository } from 'typeorm'; +import { In, Repository } from 'typeorm'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; 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'; @@ -14,6 +15,11 @@ import { } from 'src/engine/metadata-modules/workspace-metadata-version/exceptions/workspace-metadata-version.exception'; import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; +type getExistingOrRecomputeMetadataMapsResult = { + objectMetadataMaps: ObjectMetadataMaps; + metadataVersion: number; +}; + @Injectable() export class WorkspaceMetadataCacheService { logger = new Logger(WorkspaceMetadataCacheService.name); @@ -24,21 +30,15 @@ export class WorkspaceMetadataCacheService { private readonly workspaceCacheStorageService: WorkspaceCacheStorageService, @InjectRepository(ObjectMetadataEntity, 'core') private readonly objectMetadataRepository: Repository, + @InjectRepository(IndexMetadataEntity, 'core') + private readonly indexMetadataRepository: Repository, ) {} - async recomputeMetadataCache({ + async getExistingOrRecomputeMetadataMaps({ workspaceId, - ignoreLock = false, }: { workspaceId: string; - ignoreLock?: boolean; - }): Promise< - | { - recomputedObjectMetadataMaps: ObjectMetadataMaps; - recomputedMetadataVersion: number; - } - | undefined - > { + }): Promise { const currentCacheVersion = await this.getMetadataVersionFromCache(workspaceId); @@ -52,68 +52,94 @@ export class WorkspaceMetadataCacheService { ); } - if (currentDatabaseVersion === currentCacheVersion) { - return; - } + const shouldRecompute = + !isDefined(currentCacheVersion) || + currentCacheVersion !== currentDatabaseVersion; - if (!ignoreLock) { - const isAlreadyCaching = - await this.workspaceCacheStorageService.getObjectMetadataOngoingCachingLock( - workspaceId, - currentDatabaseVersion, - ); - - if (isAlreadyCaching) { - return; - } - } - - if (currentCacheVersion !== undefined) { - this.workspaceCacheStorageService.flushVersionedMetadata( - workspaceId, - currentCacheVersion, - ); - } - - try { - await this.workspaceCacheStorageService.addObjectMetadataCollectionOngoingCachingLock( + const existingObjectMetadataMaps = + await this.workspaceCacheStorageService.getObjectMetadataMaps( workspaceId, currentDatabaseVersion, ); - const objectMetadataItems = await this.objectMetadataRepository.find({ - where: { workspaceId }, - relations: [ - 'fields', - 'indexMetadatas', - 'indexMetadatas.indexFieldMetadatas', - ], + if (isDefined(existingObjectMetadataMaps) && !shouldRecompute) { + return { + objectMetadataMaps: existingObjectMetadataMaps, + metadataVersion: currentDatabaseVersion, + }; + } + + const { objectMetadataMaps, metadataVersion } = + await this.recomputeMetadataCache({ + workspaceId, }); - const freshObjectMetadataMaps = - generateObjectMetadataMaps(objectMetadataItems); + return { + objectMetadataMaps, + metadataVersion, + }; + } - await this.workspaceCacheStorageService.setObjectMetadataMaps( - workspaceId, - currentDatabaseVersion, - freshObjectMetadataMaps, - ); + async recomputeMetadataCache({ + workspaceId, + }: { + workspaceId: string; + }): Promise { + const currentDatabaseVersion = + await this.getMetadataVersionFromDatabase(workspaceId); - await this.workspaceCacheStorageService.setMetadataVersion( - workspaceId, - currentDatabaseVersion, - ); - - return { - recomputedObjectMetadataMaps: freshObjectMetadataMaps, - recomputedMetadataVersion: currentDatabaseVersion, - }; - } finally { - await this.workspaceCacheStorageService.removeObjectMetadataOngoingCachingLock( - workspaceId, - currentDatabaseVersion, + if (!isDefined(currentDatabaseVersion)) { + throw new WorkspaceMetadataVersionException( + 'Metadata version not found in the database', + WorkspaceMetadataVersionExceptionCode.METADATA_VERSION_NOT_FOUND, ); } + + await this.workspaceCacheStorageService.flushVersionedMetadata(workspaceId); + + const objectMetadataItems = await this.objectMetadataRepository.find({ + where: { workspaceId }, + relations: ['fields'], + }); + + const objectMetadataItemsIds = objectMetadataItems.map( + (objectMetadataItem) => objectMetadataItem.id, + ); + + const indexMetadataItems = await this.indexMetadataRepository.find({ + where: { objectMetadataId: In(objectMetadataItemsIds) }, + relations: ['indexFieldMetadatas'], + }); + + const objectMetadataItemsWithIndexMetadatas = objectMetadataItems.map( + (objectMetadataItem) => ({ + ...objectMetadataItem, + indexMetadatas: indexMetadataItems.filter( + (indexMetadataItem) => + indexMetadataItem.objectMetadataId === objectMetadataItem.id, + ), + }), + ); + + const freshObjectMetadataMaps = generateObjectMetadataMaps( + objectMetadataItemsWithIndexMetadatas, + ); + + await this.workspaceCacheStorageService.setObjectMetadataMaps( + workspaceId, + currentDatabaseVersion, + freshObjectMetadataMaps, + ); + + await this.workspaceCacheStorageService.setMetadataVersion( + workspaceId, + currentDatabaseVersion, + ); + + return { + objectMetadataMaps: freshObjectMetadataMaps, + metadataVersion: currentDatabaseVersion, + }; } private async getMetadataVersionFromDatabase( diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module.ts index 647732591..ce0a478d4 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module.ts @@ -2,14 +2,17 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service'; import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; @Module({ imports: [ - TypeOrmModule.forFeature([Workspace], 'core'), - TypeOrmModule.forFeature([ObjectMetadataEntity], 'core'), + TypeOrmModule.forFeature( + [Workspace, ObjectMetadataEntity, IndexMetadataEntity], + 'core', + ), WorkspaceCacheStorageModule, ], exports: [WorkspaceMetadataCacheService], diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache-storage.service.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache-storage.service.ts index a4b044b5c..d16d0c6cf 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache-storage.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache-storage.service.ts @@ -64,28 +64,6 @@ export class WorkspacePermissionsCacheStorageService { ); } - addRolesPermissionsOngoingCachingLock(workspaceId: string) { - return this.cacheStorageService.set( - `${WorkspaceCacheKeys.MetadataPermissionsRolesPermissionsOngoingCachingLock}:${workspaceId}`, - true, - 1_000 * 60, // 1 minute - ); - } - - removeRolesPermissionsOngoingCachingLock(workspaceId: string) { - return this.cacheStorageService.del( - `${WorkspaceCacheKeys.MetadataPermissionsRolesPermissionsOngoingCachingLock}:${workspaceId}`, - ); - } - - getRolesPermissionsOngoingCachingLock( - workspaceId: string, - ): Promise { - return this.cacheStorageService.get( - `${WorkspaceCacheKeys.MetadataPermissionsRolesPermissionsOngoingCachingLock}:${workspaceId}`, - ); - } - async setUserWorkspaceRoleMap( workspaceId: string, userWorkspaceRoleMap: UserWorkspaceRoleMap, @@ -128,31 +106,9 @@ export class WorkspacePermissionsCacheStorageService { ); } - addUserWorkspaceRoleMapOngoingCachingLock(workspaceId: string) { - return this.cacheStorageService.set( - `${WorkspaceCacheKeys.MetadataPermissionsUserWorkspaceRoleMapOngoingCachingLock}:${workspaceId}`, - true, - 1_000 * 60, // 1 minute - ); - } - - removeUserWorkspaceRoleMapOngoingCachingLock(workspaceId: string) { - return this.cacheStorageService.del( - `${WorkspaceCacheKeys.MetadataPermissionsUserWorkspaceRoleMapOngoingCachingLock}:${workspaceId}`, - ); - } - removeUserWorkspaceRoleMap(workspaceId: string) { return this.cacheStorageService.del( `${WorkspaceCacheKeys.MetadataPermissionsUserWorkspaceRoleMap}:${workspaceId}`, ); } - - getUserWorkspaceRoleMapOngoingCachingLock( - workspaceId: string, - ): Promise { - return this.cacheStorageService.get( - `${WorkspaceCacheKeys.MetadataPermissionsUserWorkspaceRoleMapOngoingCachingLock}:${workspaceId}`, - ); - } } diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service.ts index 78333abf5..8bb7b79bc 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service.ts @@ -12,7 +12,6 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity'; -import { WorkspaceFeatureFlagsMapCacheService } from 'src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.service'; import { UserWorkspaceRoleMap } from 'src/engine/metadata-modules/workspace-permissions-cache/types/user-workspace-role-map.type'; import { WorkspacePermissionsCacheStorageService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache-storage.service'; import { TwentyORMExceptionCode } from 'src/engine/twenty-orm/exceptions/twenty-orm.exception'; @@ -39,118 +38,55 @@ export class WorkspacePermissionsCacheService { @InjectRepository(UserWorkspaceRoleEntity, 'core') private readonly userWorkspaceRoleRepository: Repository, private readonly workspacePermissionsCacheStorageService: WorkspacePermissionsCacheStorageService, - private readonly workspaceFeatureFlagsMapCacheService: WorkspaceFeatureFlagsMapCacheService, ) {} async recomputeRolesPermissionsCache({ workspaceId, - ignoreLock = false, roleIds, }: { workspaceId: string; - ignoreLock?: boolean; roleIds?: string[]; }): Promise { - const isAlreadyCaching = - await this.workspacePermissionsCacheStorageService.getRolesPermissionsOngoingCachingLock( - workspaceId, - ); + let currentRolesPermissions: ObjectRecordsPermissionsByRoleId | undefined; - if (isAlreadyCaching) { - if (ignoreLock) { - this.logger.warn( - `RolesPermissions data is already being cached (workspace ${workspaceId}), ignoring lock`, - ); - } else { - this.logger.warn( - `RolesPermissions data is already being cached (workspace ${workspaceId}), respecting lock and returning no data`, - ); - - return; - } - } - - await this.workspacePermissionsCacheStorageService.addRolesPermissionsOngoingCachingLock( - workspaceId, - ); - - try { - let currentRolesPermissions: ObjectRecordsPermissionsByRoleId | undefined; - - if (roleIds) { - currentRolesPermissions = - await this.workspacePermissionsCacheStorageService.getRolesPermissions( - workspaceId, - ); - } - - const recomputedRolesPermissions = - await this.getObjectRecordPermissionsForRoles({ + if (roleIds) { + currentRolesPermissions = + await this.workspacePermissionsCacheStorageService.getRolesPermissions( workspaceId, - roleIds, - }); - - const freshObjectRecordsPermissionsByRoleId = roleIds - ? { ...currentRolesPermissions, ...recomputedRolesPermissions } - : recomputedRolesPermissions; - - await this.workspacePermissionsCacheStorageService.setRolesPermissions( - workspaceId, - freshObjectRecordsPermissionsByRoleId, - ); - } finally { - await this.workspacePermissionsCacheStorageService.removeRolesPermissionsOngoingCachingLock( - workspaceId, - ); + ); } + + const recomputedRolesPermissions = + await this.getObjectRecordPermissionsForRoles({ + workspaceId, + roleIds, + }); + + const freshObjectRecordsPermissionsByRoleId = roleIds + ? { ...currentRolesPermissions, ...recomputedRolesPermissions } + : recomputedRolesPermissions; + + await this.workspacePermissionsCacheStorageService.setRolesPermissions( + workspaceId, + freshObjectRecordsPermissionsByRoleId, + ); } async recomputeUserWorkspaceRoleMapCache({ workspaceId, - ignoreLock = false, }: { workspaceId: string; - ignoreLock?: boolean; }): Promise { try { - const isAlreadyCaching = - await this.workspacePermissionsCacheStorageService.getUserWorkspaceRoleMapOngoingCachingLock( + const freshUserWorkspaceRoleMap = + await this.getUserWorkspaceRoleMapFromDatabase({ workspaceId, - ); + }); - if (isAlreadyCaching) { - if (ignoreLock) { - this.logger.warn( - `UserWorkspaceRoleMap data is already being cached (workspace ${workspaceId}), ignoring lock`, - ); - } else { - this.logger.warn( - `UserWorkspaceRoleMap data is already being cached (workspace ${workspaceId}), respecting lock and returning no data`, - ); - - return; - } - } - - await this.workspacePermissionsCacheStorageService.addUserWorkspaceRoleMapOngoingCachingLock( + await this.workspacePermissionsCacheStorageService.setUserWorkspaceRoleMap( workspaceId, + freshUserWorkspaceRoleMap, ); - - try { - const freshUserWorkspaceRoleMap = - await this.getUserWorkspaceRoleMapFromDatabase({ - workspaceId, - }); - - await this.workspacePermissionsCacheStorageService.setUserWorkspaceRoleMap( - workspaceId, - freshUserWorkspaceRoleMap, - ); - } finally { - await this.workspacePermissionsCacheStorageService.removeUserWorkspaceRoleMapOngoingCachingLock( - workspaceId, - ); - } } catch (error) { // Flush stale userWorkspaceRoleMap await this.workspacePermissionsCacheStorageService.removeUserWorkspaceRoleMap( diff --git a/packages/twenty-server/src/engine/twenty-orm/exceptions/twenty-orm.exception.ts b/packages/twenty-server/src/engine/twenty-orm/exceptions/twenty-orm.exception.ts index 099bd3587..103a7b0b5 100644 --- a/packages/twenty-server/src/engine/twenty-orm/exceptions/twenty-orm.exception.ts +++ b/packages/twenty-server/src/engine/twenty-orm/exceptions/twenty-orm.exception.ts @@ -13,4 +13,5 @@ export enum TwentyORMExceptionCode { FEATURE_FLAG_MAP_VERSION_NOT_FOUND = 'FEATURE_FLAG_MAP_VERSION_NOT_FOUND', USER_WORKSPACE_ROLE_MAP_VERSION_NOT_FOUND = 'USER_WORKSPACE_ROLE_MAP_VERSION_NOT_FOUND', MALFORMED_METADATA = 'MALFORMED_METADATA', + WORKSPACE_NOT_FOUND = 'WORKSPACE_NOT_FOUND', } 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 2a04b71ed..00e948c8e 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 @@ -12,7 +12,7 @@ import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-me 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 { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; +import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util'; import { TwentyORMException, @@ -26,10 +26,14 @@ type EntitySchemaColumnMap = { @Injectable() export class EntitySchemaColumnFactory { - create(fieldMetadataMapByName: FieldMetadataMap): EntitySchemaColumnMap { + create( + objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, + ): EntitySchemaColumnMap { let entitySchemaColumnMap: EntitySchemaColumnMap = {}; - const fieldMetadataCollection = Object.values(fieldMetadataMapByName); + const fieldMetadataCollection = Object.values( + objectMetadataItemWithFieldMaps.fieldsById, + ); for (const fieldMetadata of fieldMetadataCollection) { const key = fieldMetadata.name; 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 85cadee24..ec6c96246 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 @@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common'; import { FieldMetadataType } from 'twenty-shared/types'; import { EntitySchemaRelationOptions } from 'typeorm'; -import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; +import { 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 { determineSchemaRelationDetails } from 'src/engine/twenty-orm/utils/determine-schema-relation-details.util'; import { isFieldMetadataInterfaceOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; @@ -17,12 +17,14 @@ export class EntitySchemaRelationFactory { constructor() {} async create( - fieldMetadataMapByName: FieldMetadataMap, + objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, objectMetadataMaps: ObjectMetadataMaps, ): Promise { const entitySchemaRelationMap: EntitySchemaRelationMap = {}; - const fieldMetadataCollection = Object.values(fieldMetadataMapByName); + const fieldMetadataCollection = Object.values( + objectMetadataItemWithFieldMaps.fieldsById, + ); for (const fieldMetadata of fieldMetadataCollection) { if ( 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 03a3b2212..6b92e2d6f 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 @@ -24,12 +24,10 @@ export class EntitySchemaFactory { objectMetadata: ObjectMetadataItemWithFieldMaps, objectMetadataMaps: ObjectMetadataMaps, ): Promise { - const columns = this.entitySchemaColumnFactory.create( - objectMetadata.fieldsByName, - ); + const columns = this.entitySchemaColumnFactory.create(objectMetadata); const relations = await this.entitySchemaRelationFactory.create( - objectMetadata.fieldsByName, + objectMetadata, objectMetadataMaps, ); diff --git a/packages/twenty-server/src/engine/twenty-orm/factories/scoped-workspace-context.factory.ts b/packages/twenty-server/src/engine/twenty-orm/factories/scoped-workspace-context.factory.ts index b0c439df3..12a113290 100644 --- a/packages/twenty-server/src/engine/twenty-orm/factories/scoped-workspace-context.factory.ts +++ b/packages/twenty-server/src/engine/twenty-orm/factories/scoped-workspace-context.factory.ts @@ -11,7 +11,6 @@ export class ScopedWorkspaceContextFactory { public create(): { workspaceId: string | null; - workspaceMetadataVersion: number | null; userWorkspaceId: string | null; isExecutedByApiKey: boolean; } { @@ -22,13 +21,9 @@ export class ScopedWorkspaceContextFactory { this.request?.['params']?.['workspaceId'] || // @ts-expect-error legacy noImplicitAny this.request?.['workspace']?.['id']; // rest api - const workspaceMetadataVersion: number | undefined = - // @ts-expect-error legacy noImplicitAny - this.request?.['req']?.['workspaceMetadataVersion']; return { workspaceId: workspaceId ?? null, - workspaceMetadataVersion: workspaceMetadataVersion ?? null, userWorkspaceId: // @ts-expect-error legacy noImplicitAny this.request?.['req']?.['userWorkspaceId'] ?? 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 cf568813f..29bb6d35c 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 @@ -1,23 +1,17 @@ import { Injectable, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; import { ObjectRecordsPermissionsByRoleId } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; -import { EntitySchema } from 'typeorm'; +import { EntitySchema, Repository } from 'typeorm'; import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { WorkspaceFeatureFlagsMapCacheService } from 'src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.service'; -import { - WorkspaceMetadataCacheException, - WorkspaceMetadataCacheExceptionCode, -} from 'src/engine/metadata-modules/workspace-metadata-cache/exceptions/workspace-metadata-cache.exception'; import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service'; -import { - WorkspaceMetadataVersionException, - WorkspaceMetadataVersionExceptionCode, -} from 'src/engine/metadata-modules/workspace-metadata-version/exceptions/workspace-metadata-version.exception'; import { WorkspacePermissionsCacheStorageService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache-storage.service'; import { ROLES_PERMISSIONS, @@ -55,6 +49,8 @@ export class WorkspaceDatasourceFactory { private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService, private readonly workspacePermissionsCacheStorageService: WorkspacePermissionsCacheStorageService, private readonly workspaceFeatureFlagsMapCacheService: WorkspaceFeatureFlagsMapCacheService, + @InjectRepository(Workspace, 'core') + private readonly workspaceRepository: Repository, ) {} private async conditionalDestroyDataSource( @@ -103,16 +99,9 @@ export class WorkspaceDatasourceFactory { } } - public async create( - workspaceId: string, - workspaceMetadataVersion: number | null, - shouldFailIfMetadataNotFound = true, - ): Promise { - const cachedWorkspaceMetadataVersion = - await this.getWorkspaceMetadataVersionFromCache( - workspaceId, - shouldFailIfMetadataNotFound, - ); + public async create(workspaceId: string): Promise { + const dataSourceMetadataVersion = + await this.getWorkspaceMetadataVersionFromCacheOrFromDB(workspaceId); const { data: cachedFeatureFlagMap, version: cachedFeatureFlagMapVersion } = await this.workspaceFeatureFlagsMapCacheService.getWorkspaceFeatureFlagsMapAndVersion( @@ -126,17 +115,7 @@ export class WorkspaceDatasourceFactory { workspaceId, }); - if ( - workspaceMetadataVersion !== null && - cachedWorkspaceMetadataVersion !== workspaceMetadataVersion - ) { - throw new TwentyORMException( - `Workspace metadata version mismatch detected for workspace ${workspaceId}. Current version: ${cachedWorkspaceMetadataVersion}. Desired version: ${workspaceMetadataVersion}`, - TwentyORMExceptionCode.METADATA_VERSION_MISMATCH, - ); - } - - const cacheKey: CacheKey = `${workspaceId}-${cachedWorkspaceMetadataVersion}`; + const cacheKey: CacheKey = `${workspaceId}-${dataSourceMetadataVersion}`; const workspaceDataSource = await this.promiseMemoizer.memoizePromiseAndExecute( @@ -157,21 +136,27 @@ export class WorkspaceDatasourceFactory { const cachedEntitySchemaOptions = await this.workspaceCacheStorageService.getORMEntitySchema( workspaceId, - cachedWorkspaceMetadataVersion, + dataSourceMetadataVersion, ); let cachedEntitySchemas: EntitySchema[]; - const cachedObjectMetadataMaps = - await this.workspaceCacheStorageService.getObjectMetadataMaps( - workspaceId, - cachedWorkspaceMetadataVersion, + const { + objectMetadataMaps: cachedObjectMetadataMaps, + metadataVersion: metadataVersionForFinalUpToDateCheck, + } = + await this.workspaceMetadataCacheService.getExistingOrRecomputeMetadataMaps( + { + workspaceId, + }, ); - if (!cachedObjectMetadataMaps) { - throw new WorkspaceMetadataCacheException( - `Object metadata collection not found for workspace ${workspaceId}`, - WorkspaceMetadataCacheExceptionCode.OBJECT_METADATA_COLLECTION_NOT_FOUND, + if ( + metadataVersionForFinalUpToDateCheck !== dataSourceMetadataVersion + ) { + throw new TwentyORMException( + `Workspace metadata version mismatch detected for workspace ${workspaceId}. Latest version: ${metadataVersionForFinalUpToDateCheck}. Built version: ${dataSourceMetadataVersion}`, + TwentyORMExceptionCode.METADATA_VERSION_MISMATCH, ); } @@ -185,7 +170,7 @@ export class WorkspaceDatasourceFactory { (objectMetadata) => this.entitySchemaFactory.create( workspaceId, - cachedWorkspaceMetadataVersion, + dataSourceMetadataVersion, objectMetadata, cachedObjectMetadataMaps, ), @@ -194,7 +179,7 @@ export class WorkspaceDatasourceFactory { await this.workspaceCacheStorageService.setORMEntitySchema( workspaceId, - cachedWorkspaceMetadataVersion, + dataSourceMetadataVersion, entitySchemas.map((entitySchema) => entitySchema.options), ); @@ -354,39 +339,28 @@ export class WorkspaceDatasourceFactory { }); } - private async getWorkspaceMetadataVersionFromCache( + private async getWorkspaceMetadataVersionFromCacheOrFromDB( workspaceId: string, - shouldFailIfMetadataNotFound = true, ): Promise { - let latestWorkspaceMetadataVersion = + const latestWorkspaceMetadataVersion = await this.workspaceCacheStorageService.getMetadataVersion(workspaceId); - if (!isDefined(latestWorkspaceMetadataVersion)) { - if (shouldFailIfMetadataNotFound) { - throw new WorkspaceMetadataVersionException( - `Metadata version not found while fetching datasource for workspace ${workspaceId}`, - WorkspaceMetadataVersionExceptionCode.METADATA_VERSION_NOT_FOUND, - ); - } else { - await this.workspaceMetadataCacheService.recomputeMetadataCache({ - workspaceId, - ignoreLock: !shouldFailIfMetadataNotFound, - }); - latestWorkspaceMetadataVersion = - await this.workspaceCacheStorageService.getMetadataVersion( - workspaceId, - ); - } + if (isDefined(latestWorkspaceMetadataVersion)) { + return latestWorkspaceMetadataVersion; } - if (!isDefined(latestWorkspaceMetadataVersion)) { - throw new WorkspaceMetadataVersionException( - `Metadata version not found after recompute`, - WorkspaceMetadataVersionExceptionCode.METADATA_VERSION_NOT_FOUND, + const workspace = await this.workspaceRepository.findOne({ + where: { id: workspaceId }, + }); + + if (!workspace) { + throw new TwentyORMException( + `Workspace not found for workspace ${workspaceId}`, + TwentyORMExceptionCode.WORKSPACE_NOT_FOUND, ); } - return latestWorkspaceMetadataVersion; + return workspace.metadataVersion; } public async destroy(workspaceId: string) { diff --git a/packages/twenty-server/src/engine/twenty-orm/twenty-orm-global.manager.ts b/packages/twenty-server/src/engine/twenty-orm/twenty-orm-global.manager.ts index 682a5377c..0390ed3a5 100644 --- a/packages/twenty-server/src/engine/twenty-orm/twenty-orm-global.manager.ts +++ b/packages/twenty-server/src/engine/twenty-orm/twenty-orm-global.manager.ts @@ -17,7 +17,6 @@ export class TwentyORMGlobalManager { workspaceEntity: Type, options?: { shouldBypassPermissionChecks?: boolean; - shouldFailIfMetadataNotFound?: boolean; }, ): Promise>; @@ -26,7 +25,6 @@ export class TwentyORMGlobalManager { objectMetadataName: string, options?: { shouldBypassPermissionChecks?: boolean; - shouldFailIfMetadataNotFound?: boolean; }, ): Promise>; @@ -35,10 +33,8 @@ export class TwentyORMGlobalManager { workspaceEntityOrObjectMetadataName: Type | string, options: { shouldBypassPermissionChecks?: boolean; - shouldFailIfMetadataNotFound?: boolean; } = { shouldBypassPermissionChecks: false, - shouldFailIfMetadataNotFound: true, }, ): Promise> { let objectMetadataName: string; @@ -51,11 +47,8 @@ export class TwentyORMGlobalManager { ); } - const workspaceDataSource = await this.workspaceDataSourceFactory.create( - workspaceId, - null, - options.shouldFailIfMetadataNotFound, - ); + const workspaceDataSource = + await this.workspaceDataSourceFactory.create(workspaceId); const repository = workspaceDataSource.getRepository( objectMetadataName, @@ -65,18 +58,8 @@ export class TwentyORMGlobalManager { return repository; } - async getDataSourceForWorkspace({ - workspaceId, - shouldFailIfMetadataNotFound = true, - }: { - workspaceId: string; - shouldFailIfMetadataNotFound?: boolean; - }) { - return await this.workspaceDataSourceFactory.create( - workspaceId, - null, - shouldFailIfMetadataNotFound, - ); + async getDataSourceForWorkspace({ workspaceId }: { workspaceId: string }) { + return await this.workspaceDataSourceFactory.create(workspaceId); } async destroyDataSourceForWorkspace(workspaceId: string) { diff --git a/packages/twenty-server/src/engine/twenty-orm/twenty-orm.manager.ts b/packages/twenty-server/src/engine/twenty-orm/twenty-orm.manager.ts index 3ada96200..5e05d431a 100644 --- a/packages/twenty-server/src/engine/twenty-orm/twenty-orm.manager.ts +++ b/packages/twenty-server/src/engine/twenty-orm/twenty-orm.manager.ts @@ -30,12 +30,8 @@ export class TwentyORMManager { async getRepository( workspaceEntityOrObjectMetadataName: Type | string, ): Promise> { - const { - workspaceId, - workspaceMetadataVersion, - userWorkspaceId, - isExecutedByApiKey, - } = this.scopedWorkspaceContextFactory.create(); + const { workspaceId, userWorkspaceId, isExecutedByApiKey } = + this.scopedWorkspaceContextFactory.create(); let objectMetadataName: string; @@ -51,10 +47,8 @@ export class TwentyORMManager { throw new Error('Workspace not found'); } - const workspaceDataSource = await this.workspaceDataSourceFactory.create( - workspaceId, - workspaceMetadataVersion, - ); + const workspaceDataSource = + await this.workspaceDataSourceFactory.create(workspaceId); let roleId: string | undefined; @@ -79,16 +73,12 @@ export class TwentyORMManager { } async getDatasource() { - const { workspaceId, workspaceMetadataVersion } = - this.scopedWorkspaceContextFactory.create(); + const { workspaceId } = this.scopedWorkspaceContextFactory.create(); if (!workspaceId) { throw new Error('Workspace not found'); } - return this.workspaceDataSourceFactory.create( - workspaceId, - workspaceMetadataVersion, - ); + return this.workspaceDataSourceFactory.create(workspaceId); } } diff --git a/packages/twenty-server/src/engine/twenty-orm/twenty-orm.module.ts b/packages/twenty-server/src/engine/twenty-orm/twenty-orm.module.ts index f0c55bcaf..e2e47845a 100644 --- a/packages/twenty-server/src/engine/twenty-orm/twenty-orm.module.ts +++ b/packages/twenty-server/src/engine/twenty-orm/twenty-orm.module.ts @@ -3,6 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty-config.module'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; @@ -22,7 +23,7 @@ import { PgPoolSharedModule } from './pg-shared-pool/pg-shared-pool.module'; @Module({ imports: [ TypeOrmModule.forFeature( - [ObjectMetadataEntity, UserWorkspaceRoleEntity], + [ObjectMetadataEntity, UserWorkspaceRoleEntity, Workspace], 'core', ), DataSourceModule, 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 95049f208..9802a8dd8 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/format-data.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/format-data.util.ts @@ -7,7 +7,6 @@ import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-meta import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory'; -import { isFieldMetadataInterfaceOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; export function formatData( data: T, @@ -25,28 +24,14 @@ export function formatData( // eslint-disable-next-line @typescript-eslint/no-explicit-any const newData: Record = {}; - const fieldMetadataByJoinColumnName = - objectMetadataItemWithFieldMaps.fields.reduce((acc, fieldMetadata) => { - if ( - isFieldMetadataInterfaceOfType( - fieldMetadata, - FieldMetadataType.RELATION, - ) - ) { - const joinColumnName = fieldMetadata.settings?.joinColumnName; - - if (joinColumnName) { - acc.set(joinColumnName, fieldMetadata); - } - } - - return acc; - }, new Map()); for (const [key, value] of Object.entries(data)) { + const fieldMetadataId = + objectMetadataItemWithFieldMaps.fieldIdByName[key] || + objectMetadataItemWithFieldMaps.fieldIdByJoinColumnName[key]; + const fieldMetadata = - objectMetadataItemWithFieldMaps.fieldsByName[key] || - fieldMetadataByJoinColumnName.get(key); + objectMetadataItemWithFieldMaps.fieldsById[fieldMetadataId]; if (!fieldMetadata) { throw new Error( diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts index 5e00f5f76..4cf328269 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 @@ -44,15 +44,15 @@ export function formatResult( ); const newData: object = {}; - const objectMetadaItemFieldsByName = - objectMetadataMaps.byId[objectMetadataItemWithFieldMaps.id]?.fieldsByName; for (const [key, value] of Object.entries(data)) { const compositePropertyArgs = compositeFieldMetadataMap.get(key); - const fieldMetadata = objectMetadataItemWithFieldMaps.fieldsByName[key] as - | FieldMetadataInterface - | undefined; + const fieldMetadataId = objectMetadataItemWithFieldMaps.fieldIdByName[key]; + + const fieldMetadata = objectMetadataItemWithFieldMaps.fieldsById[ + fieldMetadataId + ] as FieldMetadataInterface | undefined; const isRelation = fieldMetadata ? isFieldMetadataInterfaceOfType( @@ -69,12 +69,9 @@ export function formatResult( objectMetadataItemWithFieldMaps, objectMetadataMaps, ); - } else if (objectMetadaItemFieldsByName[key]) { + } else if (fieldMetadata) { // @ts-expect-error legacy noImplicitAny - newData[key] = formatFieldMetadataValue( - value, - objectMetadaItemFieldsByName[key], - ); + newData[key] = formatFieldMetadataValue(value, fieldMetadata); } else { // @ts-expect-error legacy noImplicitAny newData[key] = value; @@ -123,10 +120,9 @@ export function formatResult( newData[parentField][compositeProperty.name] = value; } - const dateFieldMetadataCollection = - objectMetadataItemWithFieldMaps.fields.filter( - (field) => field.type === FieldMetadataType.DATE, - ); + const dateFieldMetadataCollection = Object.values( + objectMetadataItemWithFieldMaps.fieldsById, + ).filter((field) => field.type === FieldMetadataType.DATE); // This is a temporary fix to handle a bug in the frontend where the date gets returned in the wrong timezone, // thus returning the wrong date. diff --git a/packages/twenty-server/src/engine/utils/get-data-from-cache-with-recompute.util.ts b/packages/twenty-server/src/engine/utils/get-data-from-cache-with-recompute.util.ts index dfed3a206..a2ec78239 100644 --- a/packages/twenty-server/src/engine/utils/get-data-from-cache-with-recompute.util.ts +++ b/packages/twenty-server/src/engine/utils/get-data-from-cache-with-recompute.util.ts @@ -24,10 +24,7 @@ const getFromCacheWithRecompute = async ({ workspaceId: string; getCacheData: (workspaceId: string) => Promise; getCacheVersion?: (workspaceId: string) => Promise; - recomputeCache: (params: { - workspaceId: string; - ignoreLock?: boolean; - }) => Promise; + recomputeCache: (params: { workspaceId: string }) => Promise; cachedEntityName: string; exceptionCode: TwentyORMExceptionCode; logger: Logger; @@ -54,7 +51,7 @@ const getFromCacheWithRecompute = async ({ cachedData, }, ); - await recomputeCache({ workspaceId, ignoreLock: true }); + await recomputeCache({ workspaceId }); cachedData = await getCacheData(workspaceId); if (expectCacheVersion) { diff --git a/packages/twenty-server/src/engine/workspace-cache-storage/workspace-cache-storage.service.ts b/packages/twenty-server/src/engine/workspace-cache-storage/workspace-cache-storage.service.ts index 845ab3408..e974a3394 100644 --- a/packages/twenty-server/src/engine/workspace-cache-storage/workspace-cache-storage.service.ts +++ b/packages/twenty-server/src/engine/workspace-cache-storage/workspace-cache-storage.service.ts @@ -27,17 +27,13 @@ export enum WorkspaceCacheKeys { ORMEntitySchemas = 'orm:entity-schemas', GraphQLFeatureFlag = 'graphql:feature-flag', MetadataObjectMetadataMaps = 'metadata:object-metadata-maps', - MetadataObjectMetadataOngoingCachingLock = 'metadata:object-metadata-ongoing-caching-lock', MetadataVersion = 'metadata:workspace-metadata-version', FeatureFlagMap = 'feature-flag:feature-flag-map', FeatureFlagMapVersion = 'feature-flag:feature-flag-map-version', - FeatureFlagMapOngoingCachingLock = 'feature-flag-map-ongoing-caching-lock', MetadataPermissionsRolesPermissions = 'metadata:permissions:roles-permissions', MetadataPermissionsRolesPermissionsVersion = 'metadata:permissions:roles-permissions-version', - MetadataPermissionsRolesPermissionsOngoingCachingLock = 'metadata:permissions:roles-permissions-ongoing-caching-lock', MetadataPermissionsUserWorkspaceRoleMap = 'metadata:permissions:user-workspace-role-map', MetadataPermissionsUserWorkspaceRoleMapVersion = 'metadata:permissions:user-workspace-role-map-version', - MetadataPermissionsUserWorkspaceRoleMapOngoingCachingLock = 'metadata:permissions:user-workspace-role-map-ongoing-caching-lock', } const TTL_INFINITE = 0; @@ -91,35 +87,6 @@ export class WorkspaceCacheStorageService { ); } - addObjectMetadataCollectionOngoingCachingLock( - workspaceId: string, - metadataVersion: number, - ) { - return this.cacheStorageService.set( - `${WorkspaceCacheKeys.MetadataObjectMetadataOngoingCachingLock}:${workspaceId}:${metadataVersion}`, - true, - 1_000 * 60, // 1 minute - ); - } - - removeObjectMetadataOngoingCachingLock( - workspaceId: string, - metadataVersion: number, - ) { - return this.cacheStorageService.del( - `${WorkspaceCacheKeys.MetadataObjectMetadataOngoingCachingLock}:${workspaceId}:${metadataVersion}`, - ); - } - - getObjectMetadataOngoingCachingLock( - workspaceId: string, - metadataVersion: number, - ): Promise { - return this.cacheStorageService.get( - `${WorkspaceCacheKeys.MetadataObjectMetadataOngoingCachingLock}:${workspaceId}:${metadataVersion}`, - ); - } - setObjectMetadataMaps( workspaceId: string, metadataVersion: number, @@ -252,59 +219,33 @@ export class WorkspaceCacheStorageService { ); } - addFeatureFlagMapOngoingCachingLock(workspaceId: string) { - return this.cacheStorageService.set( - `${WorkspaceCacheKeys.FeatureFlagMapOngoingCachingLock}:${workspaceId}`, - true, - 1_000 * 60, // 1 minute - ); - } - - removeFeatureFlagsMapOngoingCachingLock(workspaceId: string) { - return this.cacheStorageService.del( - `${WorkspaceCacheKeys.FeatureFlagMapOngoingCachingLock}:${workspaceId}`, - ); - } - - getFeatureFlagsMapOngoingCachingLock( - workspaceId: string, - ): Promise { - return this.cacheStorageService.get( - `${WorkspaceCacheKeys.FeatureFlagMapOngoingCachingLock}:${workspaceId}`, - ); - } - async flushVersionedMetadata( workspaceId: string, - metadataVersion: number, + metadataVersion?: number, ): Promise { + const metadataVersionSuffix = isDefined(metadataVersion) + ? `${metadataVersion}` + : '*'; + await this.cacheStorageService.del( - `${WorkspaceCacheKeys.MetadataObjectMetadataMaps}:${workspaceId}:${metadataVersion}`, + `${WorkspaceCacheKeys.MetadataObjectMetadataMaps}:${workspaceId}:${metadataVersionSuffix}`, ); await this.cacheStorageService.del( - `${WorkspaceCacheKeys.MetadataVersion}:${workspaceId}:${metadataVersion}`, + `${WorkspaceCacheKeys.MetadataVersion}:${workspaceId}:${metadataVersionSuffix}`, ); await this.cacheStorageService.del( - `${WorkspaceCacheKeys.GraphQLTypeDefs}:${workspaceId}:${metadataVersion}`, + `${WorkspaceCacheKeys.GraphQLTypeDefs}:${workspaceId}:${metadataVersionSuffix}`, ); await this.cacheStorageService.del( - `${WorkspaceCacheKeys.GraphQLUsedScalarNames}:${workspaceId}:${metadataVersion}`, + `${WorkspaceCacheKeys.GraphQLUsedScalarNames}:${workspaceId}:${metadataVersionSuffix}`, ); await this.cacheStorageService.del( - `${WorkspaceCacheKeys.ORMEntitySchemas}:${workspaceId}:${metadataVersion}`, - ); - await this.cacheStorageService.del( - `${WorkspaceCacheKeys.MetadataObjectMetadataOngoingCachingLock}:${workspaceId}:${metadataVersion}`, + `${WorkspaceCacheKeys.ORMEntitySchemas}:${workspaceId}:${metadataVersionSuffix}`, ); } - async flush( - workspaceId: string, - metadataVersion: number | undefined, - ): Promise { - if (isDefined(metadataVersion)) { - await this.flushVersionedMetadata(workspaceId, metadataVersion); - } + async flush(workspaceId: string, metadataVersion?: number): Promise { + await this.flushVersionedMetadata(workspaceId, metadataVersion); await this.cacheStorageService.del( `${WorkspaceCacheKeys.MetadataPermissionsRolesPermissions}:${workspaceId}`, @@ -314,10 +255,6 @@ export class WorkspaceCacheStorageService { `${WorkspaceCacheKeys.MetadataPermissionsRolesPermissionsVersion}:${workspaceId}`, ); - await this.cacheStorageService.del( - `${WorkspaceCacheKeys.MetadataPermissionsRolesPermissionsOngoingCachingLock}:${workspaceId}`, - ); - await this.cacheStorageService.del( `${WorkspaceCacheKeys.MetadataPermissionsUserWorkspaceRoleMap}:${workspaceId}`, ); @@ -326,10 +263,6 @@ export class WorkspaceCacheStorageService { `${WorkspaceCacheKeys.MetadataPermissionsUserWorkspaceRoleMapVersion}:${workspaceId}`, ); - await this.cacheStorageService.del( - `${WorkspaceCacheKeys.MetadataPermissionsUserWorkspaceRoleMapOngoingCachingLock}:${workspaceId}`, - ); - await this.cacheStorageService.del( `${WorkspaceCacheKeys.FeatureFlagMap}:${workspaceId}`, ); @@ -337,9 +270,5 @@ export class WorkspaceCacheStorageService { await this.cacheStorageService.del( `${WorkspaceCacheKeys.FeatureFlagMapVersion}:${workspaceId}`, ); - - await this.cacheStorageService.del( - `${WorkspaceCacheKeys.FeatureFlagMapOngoingCachingLock}:${workspaceId}`, - ); } } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-field.factory.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-field.factory.ts index cb8266e98..c5870f52b 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-field.factory.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-field.factory.ts @@ -153,6 +153,7 @@ export class StandardFieldFactory { isActive: workspaceFieldMetadataArgs.isActive ?? true, asExpression: workspaceFieldMetadataArgs.asExpression, generatedType: workspaceFieldMetadataArgs.generatedType, + isLabelSyncedWithName: true, }, ]; } @@ -191,6 +192,7 @@ export class StandardFieldFactory { isNullable: true, isUnique: false, isActive: workspaceRelationMetadataArgs.isActive ?? true, + isLabelSyncedWithName: true, }); return fieldMetadataCollection; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-index.factory.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-index.factory.ts index 1da3582e1..6de124a3c 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-index.factory.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-index.factory.ts @@ -3,7 +3,10 @@ import { Injectable, Logger } from '@nestjs/common'; import { PartialIndexMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-index-metadata.interface'; import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface'; -import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; +import { + IndexMetadataEntity, + IndexType, +} from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; import { generateDeterministicIndexName } from 'src/engine/metadata-modules/index-metadata/utils/generate-deterministic-index-name'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; @@ -88,7 +91,7 @@ export class StandardIndexFactory { isUnique: workspaceIndexMetadataArgs.isUnique, isCustom: false, indexWhereClause: workspaceIndexMetadataArgs.whereClause, - indexType: workspaceIndexMetadataArgs.type, + indexType: workspaceIndexMetadataArgs.type ?? IndexType.BTREE, }; return indexMetadata; @@ -129,7 +132,7 @@ export class StandardIndexFactory { columns: workspaceIndexMetadataArgs.columns, isCustom: false, isUnique: workspaceIndexMetadataArgs.isUnique, - indexType: workspaceIndexMetadataArgs.type, + indexType: workspaceIndexMetadataArgs.type ?? IndexType.BTREE, indexWhereClause: workspaceIndexMetadataArgs.whereClause, }; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-object.factory.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-object.factory.ts index e572777cd..8c72d2df0 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-object.factory.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-object.factory.ts @@ -21,7 +21,9 @@ export class StandardObjectFactory { private createObjectMetadata( target: typeof BaseWorkspaceEntity, context: WorkspaceSyncContext, - ): Omit | undefined { + ): + | Omit + | undefined { const workspaceEntityMetadataArgs = metadataArgsStorage.filterEntities(target); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-field-metadata.interface.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-field-metadata.interface.ts index f39bc0abd..4701cfaa3 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-field-metadata.interface.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-field-metadata.interface.ts @@ -9,7 +9,12 @@ export type PartialFieldMetadata< T extends FieldMetadataType = FieldMetadataType, > = Omit< FieldMetadataInterface, - 'id' | 'label' | 'description' | 'objectMetadataId' + | 'id' + | 'label' + | 'description' + | 'objectMetadataId' + | 'createdAt' + | 'updatedAt' > & { standardId: string; label: string | ((objectMetadata: ObjectMetadataEntity) => string); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/compute-standard-fields.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/compute-standard-fields.util.ts index 0e5102f72..8710fb70a 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/compute-standard-fields.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/compute-standard-fields.util.ts @@ -16,8 +16,11 @@ export const computeStandardFields = ( )[], originalObjectMetadata: ObjectMetadataEntity, customObjectMetadataCollection: ObjectMetadataEntity[] = [], -): ComputedPartialFieldMetadata[] => { - const fields: ComputedPartialFieldMetadata[] = []; +): Omit[] => { + const fields: Omit< + ComputedPartialFieldMetadata, + 'createdAt' | 'updatedAt' + >[] = []; for (const partialFieldMetadata of standardFieldMetadataCollection) { // Relation from standard object to custom object @@ -43,6 +46,8 @@ export const computeStandardFields = ( ...rest, standardId: relationStandardId, defaultValue: null, + isNullable: true, + isLabelSyncedWithName: true, }); } } else { 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 3b48549e0..2a44d6670 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,6 +1,7 @@ import { FieldMetadataType } from 'twenty-shared/types'; -import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; + import { shouldGenerateFieldFakeValue } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/should-generate-field-fake-value'; describe('shouldGenerateFieldFakeValue', () => { @@ -10,7 +11,9 @@ describe('shouldGenerateFieldFakeValue', () => { isActive: true, type: FieldMetadataType.TEXT, name: 'testField', - } as FieldMetadataEntity; + createdAt: new Date(), + updatedAt: new Date(), + } as FieldMetadataInterface; expect(shouldGenerateFieldFakeValue(field)).toBe(true); }); @@ -21,7 +24,9 @@ describe('shouldGenerateFieldFakeValue', () => { isActive: true, type: FieldMetadataType.UUID, name: 'id', - } as FieldMetadataEntity; + createdAt: new Date(), + updatedAt: new Date(), + } as FieldMetadataInterface; expect(shouldGenerateFieldFakeValue(field)).toBe(true); }); @@ -32,7 +37,9 @@ describe('shouldGenerateFieldFakeValue', () => { isActive: false, type: FieldMetadataType.TEXT, name: 'testField', - } as FieldMetadataEntity; + createdAt: new Date(), + updatedAt: new Date(), + } as FieldMetadataInterface; expect(shouldGenerateFieldFakeValue(field)).toBe(false); }); @@ -43,7 +50,9 @@ describe('shouldGenerateFieldFakeValue', () => { isActive: true, type: FieldMetadataType.TEXT, name: 'testField', - } as FieldMetadataEntity; + createdAt: new Date(), + updatedAt: new Date(), + } as FieldMetadataInterface; expect(shouldGenerateFieldFakeValue(field)).toBe(false); }); @@ -54,7 +63,9 @@ describe('shouldGenerateFieldFakeValue', () => { isActive: true, type: FieldMetadataType.RELATION, name: 'testField', - } as FieldMetadataEntity; + createdAt: new Date(), + updatedAt: new Date(), + } as FieldMetadataInterface; expect(shouldGenerateFieldFakeValue(field)).toBe(false); }); 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 a20536f0e..a638f22c2 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 @@ -18,44 +18,47 @@ export const generateObjectRecordFields = ({ }): BaseOutputSchema => { const objectMetadata = objectMetadataInfo.objectMetadataItemWithFieldsMaps; - return objectMetadata.fields.reduce((acc: BaseOutputSchema, field) => { - if (!shouldGenerateFieldFakeValue(field)) { - return acc; - } + return Object.values(objectMetadata.fieldsById).reduce( + (acc: BaseOutputSchema, field) => { + if (!shouldGenerateFieldFakeValue(field)) { + return acc; + } - if (field.type !== FieldMetadataType.RELATION) { - acc[field.name] = generateFakeField({ - type: field.type, - label: field.label, - icon: field.icon, - }); + if (field.type !== FieldMetadataType.RELATION) { + acc[field.name] = generateFakeField({ + type: field.type, + label: field.label, + icon: field.icon, + }); + + return acc; + } + + if ( + depth < MAXIMUM_DEPTH && + isDefined(field.relationTargetObjectMetadataId) + ) { + const relationTargetObjectMetadata = + objectMetadataInfo.objectMetadataMaps.byId[ + field.relationTargetObjectMetadataId + ]; + + acc[field.name] = { + isLeaf: false, + icon: field.icon, + label: field.label, + value: generateFakeObjectRecord({ + objectMetadataInfo: { + objectMetadataItemWithFieldsMaps: relationTargetObjectMetadata, + objectMetadataMaps: objectMetadataInfo.objectMetadataMaps, + }, + depth: depth + 1, + }), + }; + } return acc; - } - - if ( - depth < MAXIMUM_DEPTH && - isDefined(field.relationTargetObjectMetadataId) - ) { - const relationTargetObjectMetadata = - objectMetadataInfo.objectMetadataMaps.byId[ - field.relationTargetObjectMetadataId - ]; - - acc[field.name] = { - isLeaf: false, - icon: field.icon, - label: field.label, - value: generateFakeObjectRecord({ - objectMetadataInfo: { - objectMetadataItemWithFieldsMaps: relationTargetObjectMetadata, - objectMetadataMaps: objectMetadataInfo.objectMetadataMaps, - }, - depth: depth + 1, - }), - }; - } - - return acc; - }, {} as BaseOutputSchema); + }, + {} as BaseOutputSchema, + ); }; diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/create-record.workflow-action.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/create-record.workflow-action.ts index 137011409..1f811508f 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/create-record.workflow-action.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/create-record.workflow-action.ts @@ -1,6 +1,7 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import { isDefined } from 'class-validator'; import { Repository } from 'typeorm'; import { WorkflowExecutor } from 'src/modules/workflow/workflow-executor/interfaces/workflow-executor.interface'; @@ -109,8 +110,8 @@ export class CreateRecordWorkflowAction implements WorkflowExecutor { ); const validObjectRecord = Object.fromEntries( - Object.entries(workflowActionInput.objectRecord).filter( - ([key]) => objectMetadataItemWithFieldsMaps.fieldsByName[key], + Object.entries(workflowActionInput.objectRecord).filter(([key]) => + isDefined(objectMetadataItemWithFieldsMaps.fieldIdByName[key]), ), ); diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/find-records.workflow-action.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/find-records.workflow-action.ts index 1b9cec496..b5ab53354 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/find-records.workflow-action.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/find-records.workflow-action.ts @@ -90,8 +90,7 @@ export class FindRecordsWorkflowAction implements WorkflowExecutor { ); const graphqlQueryParser = new GraphqlQueryParser( - objectMetadataItemWithFieldsMaps.fieldsByName, - objectMetadataItemWithFieldsMaps.fieldsByJoinColumnName, + objectMetadataItemWithFieldsMaps, objectMetadataMaps, ); diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/listeners/__tests__/database-event-trigger.listener.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/listeners/__tests__/database-event-trigger.listener.spec.ts index 994cd19e3..1029775f7 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/listeners/__tests__/database-event-trigger.listener.spec.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/listeners/__tests__/database-event-trigger.listener.spec.ts @@ -1,6 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; 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 { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; 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'; @@ -54,8 +55,26 @@ describe('DatabaseEventTriggerListener', () => { }, }, objectMetadataItemWithFieldsMaps: { - fieldsByJoinColumnName: {}, - }, + id: 'test-object-metadata', + workspaceId: 'test-workspace', + nameSingular: 'testObject', + namePlural: 'testObjects', + labelSingular: 'Test Object', + labelPlural: 'Test Objects', + description: 'Test object for testing', + fieldIdByJoinColumnName: {}, + fieldsById: {}, + fieldIdByName: {}, + indexMetadatas: [], + targetTableName: 'test_objects', + isSystem: false, + isCustom: false, + isActive: true, + isRemote: false, + isAuditLogged: true, + isSearchable: true, + icon: 'Icon123', + } satisfies ObjectMetadataItemWithFieldMaps, }), }, }, @@ -97,6 +116,7 @@ describe('DatabaseEventTriggerListener', () => { updatedAt: new Date(), fields: [], indexMetadatas: [], + icon: 'Icon123', }, properties: { updatedFields: ['field1', 'field2'], diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/listeners/database-event-trigger.listener.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/listeners/database-event-trigger.listener.ts index 315529682..321ea505e 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/listeners/database-event-trigger.listener.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/listeners/database-event-trigger.listener.ts @@ -183,13 +183,12 @@ export class DatabaseEventTriggerListener { workspaceId, ); - const fieldsByJoinColumnName = - objectMetadataItemWithFieldsMaps.fieldsByJoinColumnName; - - for (const [joinColumn, joinField] of Object.entries( - fieldsByJoinColumnName, + for (const [joinColumnName, joinFieldId] of Object.entries( + objectMetadataItemWithFieldsMaps.fieldIdByJoinColumnName, )) { - const joinRecordId = record[joinColumn]; + const joinField = + objectMetadataItemWithFieldsMaps.fieldsById[joinFieldId]; + const joinRecordId = record[joinColumnName]; const relatedObjectMetadataId = joinField.relationTargetObjectMetadataId; if (!isDefined(relatedObjectMetadataId)) {