diff --git a/packages/twenty-front/src/modules/apollo/optimistic-effect/group-by/utils/triggerUpdateGroupByQueriesOptimisticEffect.ts b/packages/twenty-front/src/modules/apollo/optimistic-effect/group-by/utils/triggerUpdateGroupByQueriesOptimisticEffect.ts index 266d2d1f76..629e443ed5 100644 --- a/packages/twenty-front/src/modules/apollo/optimistic-effect/group-by/utils/triggerUpdateGroupByQueriesOptimisticEffect.ts +++ b/packages/twenty-front/src/modules/apollo/optimistic-effect/group-by/utils/triggerUpdateGroupByQueriesOptimisticEffect.ts @@ -11,7 +11,7 @@ import { type RecordGqlGroupByConnection } from '@/object-record/graphql/types/R import { type RecordGqlOperationGroupByVariables } from '@/object-record/graphql/types/RecordGqlOperationGroupByVariables'; import { isRecordMatchingFilter } from '@/object-record/record-filter/utils/isRecordMatchingFilter'; import { isArray } from '@sniptt/guards'; -import { isDefined } from 'twenty-shared/utils'; +import { getGroupByConnectionTypename, isDefined } from 'twenty-shared/utils'; import { parseApolloStoreFieldName } from '~/utils/parseApolloStoreFieldName'; type TriggerUpdateGroupByQueriesOptimisticEffectArgs = { @@ -199,7 +199,9 @@ export const triggerUpdateGroupByQueriesOptimisticEffect = ({ for (const [_, groupData] of recordsToAddToNewGroups) { if (groupData.edges.length > 0) { const newGroupConnection = { - __typename: `${objectMetadataItem.nameSingular}Connection`, + __typename: getGroupByConnectionTypename( + objectMetadataItem.nameSingular, + ), edges: groupData.edges, pageInfo: { hasNextPage: false, diff --git a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect.ts b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect.ts index 188d709b0d..6cf1d30721 100644 --- a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect.ts +++ b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect.ts @@ -4,19 +4,17 @@ import { isNonEmptyString } from '@sniptt/guards'; import { triggerUpdateRelationsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRelationsOptimisticEffect'; import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem'; import { type RecordGqlRefEdge } from '@/object-record/cache/types/RecordGqlRefEdge'; -import { getEdgeTypename } from '@/object-record/cache/utils/getEdgeTypename'; -import { isObjectRecordConnectionWithRefs } from '@/object-record/cache/utils/isObjectRecordConnectionWithRefs'; -import { type RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode'; -import { isRecordMatchingFilter } from '@/object-record/record-filter/utils/isRecordMatchingFilter'; - +import { type ObjectPermissions } from 'twenty-shared/types'; +import { getEdgeTypename, isDefined } from 'twenty-shared/utils'; import { triggerUpdateGroupByQueriesOptimisticEffect } from '@/apollo/optimistic-effect/group-by/utils/triggerUpdateGroupByQueriesOptimisticEffect'; import { type CachedObjectRecordQueryVariables } from '@/apollo/types/CachedObjectRecordQueryVariables'; import { encodeCursor } from '@/apollo/utils/encodeCursor'; import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache'; import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord'; +import { isObjectRecordConnectionWithRefs } from '@/object-record/cache/utils/isObjectRecordConnectionWithRefs'; +import { type RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode'; import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { type ObjectPermissions } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; +import { isRecordMatchingFilter } from '@/object-record/record-filter/utils/isRecordMatchingFilter'; import { parseApolloStoreFieldName } from '~/utils/parseApolloStoreFieldName'; /* diff --git a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect.ts b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect.ts index 6a26b43c14..252d90135b 100644 --- a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect.ts +++ b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect.ts @@ -6,13 +6,12 @@ import { triggerUpdateRelationsOptimisticEffect } from '@/apollo/optimistic-effe import { type CachedObjectRecordQueryVariables } from '@/apollo/types/CachedObjectRecordQueryVariables'; import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem'; import { type RecordGqlRefEdge } from '@/object-record/cache/types/RecordGqlRefEdge'; -import { getEdgeTypename } from '@/object-record/cache/utils/getEdgeTypename'; import { isObjectRecordConnectionWithRefs } from '@/object-record/cache/utils/isObjectRecordConnectionWithRefs'; import { type RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode'; import { isRecordMatchingFilter } from '@/object-record/record-filter/utils/isRecordMatchingFilter'; import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; import { type ObjectPermissions } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; +import { getEdgeTypename, isDefined } from 'twenty-shared/utils'; import { parseApolloStoreFieldName } from '~/utils/parseApolloStoreFieldName'; // TODO: add extensive unit tests for this function // That will also serve as documentation diff --git a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffectByBatch.ts b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffectByBatch.ts index 7aab689109..41b233b0cb 100644 --- a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffectByBatch.ts +++ b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffectByBatch.ts @@ -6,13 +6,12 @@ import { triggerUpdateRelationsOptimisticEffect } from '@/apollo/optimistic-effe import { type CachedObjectRecordQueryVariables } from '@/apollo/types/CachedObjectRecordQueryVariables'; import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem'; import { type RecordGqlRefEdge } from '@/object-record/cache/types/RecordGqlRefEdge'; -import { getEdgeTypename } from '@/object-record/cache/utils/getEdgeTypename'; import { isObjectRecordConnectionWithRefs } from '@/object-record/cache/utils/isObjectRecordConnectionWithRefs'; import { type RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode'; import { isRecordMatchingFilter } from '@/object-record/record-filter/utils/isRecordMatchingFilter'; import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; import { type ObjectPermissions } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; +import { getEdgeTypename, isDefined } from 'twenty-shared/utils'; import { parseApolloStoreFieldName } from '~/utils/parseApolloStoreFieldName'; // TODO: add extensive unit tests for this function diff --git a/packages/twenty-front/src/modules/object-record/cache/utils/createCacheEdgeWithRecordRef.ts b/packages/twenty-front/src/modules/object-record/cache/utils/createCacheEdgeWithRecordRef.ts index cc560f7728..f5555cfe56 100644 --- a/packages/twenty-front/src/modules/object-record/cache/utils/createCacheEdgeWithRecordRef.ts +++ b/packages/twenty-front/src/modules/object-record/cache/utils/createCacheEdgeWithRecordRef.ts @@ -1,12 +1,11 @@ import { encodeCursor } from '@/apollo/utils/encodeCursor'; import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem'; import { type RecordGqlRefEdge } from '@/object-record/cache/types/RecordGqlRefEdge'; -import { getEdgeTypename } from '@/object-record/cache/utils/getEdgeTypename'; import { type RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode'; import { type FieldFunctionOptions } from '@apollo/client/cache'; type ToReferenceFunction = FieldFunctionOptions['toReference']; -import { isDefined } from 'twenty-shared/utils'; +import { getEdgeTypename, isDefined } from 'twenty-shared/utils'; type CreateCacheEdgeWithRecordRefParams = { record: RecordGqlNode; diff --git a/packages/twenty-front/src/modules/object-record/cache/utils/getConnectionTypename.ts b/packages/twenty-front/src/modules/object-record/cache/utils/getConnectionTypename.ts deleted file mode 100644 index b40ba44c09..0000000000 --- a/packages/twenty-front/src/modules/object-record/cache/utils/getConnectionTypename.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename'; -import { capitalize } from 'twenty-shared/utils'; - -export const getConnectionTypename = (objectNameSingular: string) => { - return `${capitalize(getObjectTypename(objectNameSingular))}Connection`; -}; diff --git a/packages/twenty-front/src/modules/object-record/cache/utils/getEdgeTypename.ts b/packages/twenty-front/src/modules/object-record/cache/utils/getEdgeTypename.ts deleted file mode 100644 index 138ba92ac4..0000000000 --- a/packages/twenty-front/src/modules/object-record/cache/utils/getEdgeTypename.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename'; -import { capitalize } from 'twenty-shared/utils'; - -export const getEdgeTypename = (objectNameSingular: string) => { - return `${capitalize(getObjectTypename(objectNameSingular))}Edge`; -}; diff --git a/packages/twenty-front/src/modules/object-record/cache/utils/getNodeTypename.ts b/packages/twenty-front/src/modules/object-record/cache/utils/getNodeTypename.ts deleted file mode 100644 index d27f773667..0000000000 --- a/packages/twenty-front/src/modules/object-record/cache/utils/getNodeTypename.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename'; -import { capitalize } from 'twenty-shared/utils'; - -export const getNodeTypename = (objectNameSingular: string) => { - return capitalize(getObjectTypename(objectNameSingular)); -}; diff --git a/packages/twenty-front/src/modules/object-record/cache/utils/getRecordConnectionFromRecords.ts b/packages/twenty-front/src/modules/object-record/cache/utils/getRecordConnectionFromRecords.ts index 870ee6b09f..181904cb33 100644 --- a/packages/twenty-front/src/modules/object-record/cache/utils/getRecordConnectionFromRecords.ts +++ b/packages/twenty-front/src/modules/object-record/cache/utils/getRecordConnectionFromRecords.ts @@ -1,10 +1,10 @@ import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem'; -import { getConnectionTypename } from '@/object-record/cache/utils/getConnectionTypename'; import { getEmptyPageInfo } from '@/object-record/cache/utils/getEmptyPageInfo'; import { getRecordEdgeFromRecord } from '@/object-record/cache/utils/getRecordEdgeFromRecord'; import { type RecordGqlConnectionEdgesRequired } from '@/object-record/graphql/types/RecordGqlConnectionEdgesRequired'; -import { type RecordGqlOperationGqlRecordFields } from 'twenty-shared/types'; import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { type RecordGqlOperationGqlRecordFields } from 'twenty-shared/types'; +import { getConnectionTypename } from 'twenty-shared/utils'; export const getRecordConnectionFromRecords = ({ objectMetadataItems, diff --git a/packages/twenty-front/src/modules/object-record/cache/utils/getRecordEdgeFromRecord.ts b/packages/twenty-front/src/modules/object-record/cache/utils/getRecordEdgeFromRecord.ts index e6ff3744c3..6f5c8c73cc 100644 --- a/packages/twenty-front/src/modules/object-record/cache/utils/getRecordEdgeFromRecord.ts +++ b/packages/twenty-front/src/modules/object-record/cache/utils/getRecordEdgeFromRecord.ts @@ -1,5 +1,5 @@ import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem'; -import { getEdgeTypename } from '@/object-record/cache/utils/getEdgeTypename'; +import { getEdgeTypename } from 'twenty-shared/utils'; import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord'; import { type RecordGqlEdge } from '@/object-record/graphql/types/RecordGqlEdge'; import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; diff --git a/packages/twenty-front/src/modules/object-record/cache/utils/getRecordNodeFromRecord.ts b/packages/twenty-front/src/modules/object-record/cache/utils/getRecordNodeFromRecord.ts index 5fbdd3f9ce..f0f081247f 100644 --- a/packages/twenty-front/src/modules/object-record/cache/utils/getRecordNodeFromRecord.ts +++ b/packages/twenty-front/src/modules/object-record/cache/utils/getRecordNodeFromRecord.ts @@ -4,13 +4,12 @@ import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/Enriche import { computePossibleMorphGqlFieldForFieldName } from '@/object-record/cache/utils/computePossibleMorphGqlFieldForFieldName'; import { getFieldMetadataFromGqlField } from '@/object-record/cache/utils/getFieldMetadataFromGqlField'; import { getMorphRelationFromFieldMetadataAndGqlField } from '@/object-record/cache/utils/getMorphRelationFromFieldMetadataAndGqlField'; -import { getNodeTypename } from '@/object-record/cache/utils/getNodeTypename'; import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename'; import { getRecordConnectionFromRecords } from '@/object-record/cache/utils/getRecordConnectionFromRecords'; import { getRefName } from '@/object-record/cache/utils/getRefName'; import { type RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode'; import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { isDefined, pascalCase } from 'twenty-shared/utils'; +import { getNodeTypename, isDefined, pascalCase } from 'twenty-shared/utils'; import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql'; export const getRecordNodeFromRecord = ({ @@ -206,7 +205,7 @@ export const getRecordNodeFromRecord = ({ return undefined; } - const typeName = getObjectTypename( + const typeName = getNodeTypename( morphRelation?.targetObjectMetadata?.nameSingular, ); diff --git a/packages/twenty-front/src/modules/object-record/cache/utils/getRefName.ts b/packages/twenty-front/src/modules/object-record/cache/utils/getRefName.ts index 3a2e12a889..565796a1c7 100644 --- a/packages/twenty-front/src/modules/object-record/cache/utils/getRefName.ts +++ b/packages/twenty-front/src/modules/object-record/cache/utils/getRefName.ts @@ -1,4 +1,4 @@ -import { getNodeTypename } from '@/object-record/cache/utils/getNodeTypename'; +import { getNodeTypename } from 'twenty-shared/utils'; export const getRefName = (objectNameSingular: string, id: string) => { const nodeTypeName = getNodeTypename(objectNameSingular); diff --git a/packages/twenty-front/src/modules/object-record/cache/utils/isObjectRecordConnection.ts b/packages/twenty-front/src/modules/object-record/cache/utils/isObjectRecordConnection.ts index fa5c01d75f..c68343aa24 100644 --- a/packages/twenty-front/src/modules/object-record/cache/utils/isObjectRecordConnection.ts +++ b/packages/twenty-front/src/modules/object-record/cache/utils/isObjectRecordConnection.ts @@ -2,15 +2,13 @@ import { type StoreValue } from '@apollo/client'; import { z } from 'zod'; import { type RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection'; -import { capitalize } from 'twenty-shared/utils'; +import { capitalize, getConnectionTypename } from 'twenty-shared/utils'; export const isObjectRecordConnection = ( objectNameSingular: string, storeValue: StoreValue, ): storeValue is RecordGqlConnection => { - const objectConnectionTypeName = `${capitalize( - objectNameSingular, - )}Connection`; + const objectConnectionTypeName = getConnectionTypename(objectNameSingular); const objectEdgeTypeName = `${capitalize(objectNameSingular)}Edge`; const cachedObjectConnectionSchema = z.object({ __typename: z.literal(objectConnectionTypeName), diff --git a/packages/twenty-front/src/modules/object-record/cache/utils/isObjectRecordConnectionWithRefs.ts b/packages/twenty-front/src/modules/object-record/cache/utils/isObjectRecordConnectionWithRefs.ts index 7381a704b1..99d7cc2f34 100644 --- a/packages/twenty-front/src/modules/object-record/cache/utils/isObjectRecordConnectionWithRefs.ts +++ b/packages/twenty-front/src/modules/object-record/cache/utils/isObjectRecordConnectionWithRefs.ts @@ -2,15 +2,13 @@ import { type StoreValue } from '@apollo/client'; import { z } from 'zod'; import { type RecordGqlRefConnection } from '@/object-record/cache/types/RecordGqlRefConnection'; -import { capitalize } from 'twenty-shared/utils'; +import { capitalize, getConnectionTypename } from 'twenty-shared/utils'; export const isObjectRecordConnectionWithRefs = ( objectNameSingular: string, storeValue: StoreValue, ): storeValue is RecordGqlRefConnection => { - const objectConnectionTypeName = `${capitalize( - objectNameSingular, - )}Connection`; + const objectConnectionTypeName = getConnectionTypename(objectNameSingular); const objectEdgeTypeName = `${capitalize(objectNameSingular)}Edge`; const cachedObjectConnectionSchema = z.object({ __typename: z.literal(objectConnectionTypeName), diff --git a/packages/twenty-front/src/modules/object-record/hooks/useFetchMoreRecordsWithPagination.ts b/packages/twenty-front/src/modules/object-record/hooks/useFetchMoreRecordsWithPagination.ts index 8d1b495d28..881819541f 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useFetchMoreRecordsWithPagination.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useFetchMoreRecordsWithPagination.ts @@ -29,8 +29,12 @@ import { hasNextPageFamilyState } from '@/object-record/states/hasNextPageFamily import { isFetchingMoreRecordsFamilyState } from '@/object-record/states/isFetchingMoreRecordsFamilyState'; import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue'; import { useSetAtomFamilyState } from '@/ui/utilities/state/jotai/hooks/useSetAtomFamilyState'; -import { capitalize, isDefined, isNonEmptyArray } from 'twenty-shared/utils'; import { useStore } from 'jotai'; +import { + getConnectionTypename, + isDefined, + isNonEmptyArray, +} from 'twenty-shared/utils'; export type UseFindManyRecordsParams = ObjectMetadataItemIdentifier & RecordGqlOperationVariables & { @@ -168,9 +172,9 @@ export const useFetchMoreRecordsWithPagination = < return Object.assign({}, prev, { [objectMetadataItem.namePlural]: { - __typename: `${capitalize( + __typename: getConnectionTypename( objectMetadataItem.nameSingular, - )}Connection`, + ), edges: newEdges, pageInfo: fetchMoreResult?.[objectMetadataItem.namePlural].pageInfo, diff --git a/packages/twenty-front/src/modules/object-record/hooks/useLazyFetchMoreRecordsWithPagination.ts b/packages/twenty-front/src/modules/object-record/hooks/useLazyFetchMoreRecordsWithPagination.ts index c09eebb1ea..b1850643ad 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useLazyFetchMoreRecordsWithPagination.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useLazyFetchMoreRecordsWithPagination.ts @@ -27,8 +27,12 @@ import { import { DEFAULT_SEARCH_REQUEST_LIMIT } from '@/object-record/constants/DefaultSearchRequestLimit'; import { cursorFamilyState } from '@/object-record/states/cursorFamilyState'; import { hasNextPageFamilyState } from '@/object-record/states/hasNextPageFamilyState'; -import { capitalize, isDefined, isNonEmptyArray } from 'twenty-shared/utils'; import { useStore } from 'jotai'; +import { + getConnectionTypename, + isDefined, + isNonEmptyArray, +} from 'twenty-shared/utils'; export type UseFindManyRecordsParams = ObjectMetadataItemIdentifier & RecordGqlOperationVariables & { @@ -143,9 +147,9 @@ export const useLazyFetchMoreRecordsWithPagination = < return Object.assign({}, prev, { [objectMetadataItem.namePlural]: { - __typename: `${capitalize( + __typename: getConnectionTypename( objectMetadataItem.nameSingular, - )}Connection`, + ), edges: newEdges, pageInfo: fetchMoreResult?.[objectMetadataItem.namePlural].pageInfo, diff --git a/packages/twenty-front/src/testing/graphqlMocks.ts b/packages/twenty-front/src/testing/graphqlMocks.ts index 175da5bf87..352dbb2575 100644 --- a/packages/twenty-front/src/testing/graphqlMocks.ts +++ b/packages/twenty-front/src/testing/graphqlMocks.ts @@ -17,7 +17,6 @@ import { mockedUserData } from '~/testing/mock-data/users'; import { GET_PUBLIC_WORKSPACE_DATA_BY_DOMAIN } from '@/auth/graphql/queries/getPublicWorkspaceDataByDomain'; import { LIST_PLANS } from '@/settings/billing/graphql/queries/listPlans'; import { GET_ROLES } from '@/settings/roles/graphql/queries/getRolesQuery'; -import { isDefined } from 'twenty-shared/utils'; import { mockBillingPlans } from '~/testing/mock-data/billing-plans'; import { mockedCompanyRecords } from '~/testing/mock-data/generated/data/companies/mock-companies-data'; import { mockedTaskRecords } from '~/testing/mock-data/generated/data/tasks/mock-tasks-data'; @@ -26,8 +25,11 @@ import { mockedRoles } from '~/testing/mock-data/generated/metadata/roles/mock-r import { type Task } from '@/activities/types/Task'; import { FIND_MINIMAL_METADATA } from '@/metadata-store/graphql/queries/findMinimalMetadata'; -import { getConnectionTypename } from '@/object-record/cache/utils/getConnectionTypename'; -import { getEdgeTypename } from '@/object-record/cache/utils/getEdgeTypename'; +import { + getConnectionTypename, + getEdgeTypename, + isDefined, +} from 'twenty-shared/utils'; import { getEmptyPageInfo } from '@/object-record/cache/utils/getEmptyPageInfo'; import { getRecordFromRecordNode } from '@/object-record/cache/utils/getRecordFromRecordNode'; import { mockedApiKeys } from '~/testing/mock-data/generated/metadata/api-keys/mock-api-keys-data'; diff --git a/packages/twenty-server/src/engine/api/graphql/direct-execution/direct-execution.service.ts b/packages/twenty-server/src/engine/api/graphql/direct-execution/direct-execution.service.ts index 6de5a8cc68..a05e64d464 100644 --- a/packages/twenty-server/src/engine/api/graphql/direct-execution/direct-execution.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/direct-execution/direct-execution.service.ts @@ -40,6 +40,7 @@ import { graphQLBuildFragmentMap } from 'src/engine/api/graphql/direct-execution import { graphQLBuildPartialResolveInfo } from 'src/engine/api/graphql/direct-execution/utils/graphql-build-partial-resolve-info.util'; import { graphQLExtractTopLevelFields } from 'src/engine/api/graphql/direct-execution/utils/graphql-extract-top-level-fields.util'; import { graphQLFormatResultFromSelectedFields } from 'src/engine/api/graphql/direct-execution/utils/graphql-format-result-from-selected-fields.util'; +import { ResolverOutput } from 'src/engine/api/graphql/workspace-query-runner/interfaces/resolver-output'; import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util'; import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names'; import { CreateManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/create-many-resolver.factory'; @@ -207,12 +208,12 @@ export class DirectExecutionService { objectIdByNameSingular, ); - const result = await this.executeField({ + const result = (await this.executeField({ entry, args, graphqlPartialResolveInfo, workspaceSchemaBuilderContext, - }); + })) as ResolverOutput; const formattedResult = graphQLFormatResultFromSelectedFields( result, diff --git a/packages/twenty-server/src/engine/api/graphql/direct-execution/errors/graphql-direct-execution.exception.ts b/packages/twenty-server/src/engine/api/graphql/direct-execution/errors/graphql-direct-execution.exception.ts index 1696f9b1fd..8e72d6a60c 100644 --- a/packages/twenty-server/src/engine/api/graphql/direct-execution/errors/graphql-direct-execution.exception.ts +++ b/packages/twenty-server/src/engine/api/graphql/direct-execution/errors/graphql-direct-execution.exception.ts @@ -5,6 +5,7 @@ import { CustomException } from 'src/utils/custom-exception'; export enum GraphqlDirectExecutionExceptionCode { INVALID_QUERY_INPUT = 'INVALID_QUERY_INPUT', UNKNOWN_METHOD = 'UNKNOWN_METHOD', + INVALID_RESULT_TYPE = 'INVALID_RESULT_TYPE', } export class GraphqlDirectExecutionException extends CustomException { diff --git a/packages/twenty-server/src/engine/api/graphql/direct-execution/utils/graphql-direct-execution-to-graphql-api-exception-handler.util.ts b/packages/twenty-server/src/engine/api/graphql/direct-execution/utils/graphql-direct-execution-to-graphql-api-exception-handler.util.ts index 7966aa91ad..4bf065b961 100644 --- a/packages/twenty-server/src/engine/api/graphql/direct-execution/utils/graphql-direct-execution-to-graphql-api-exception-handler.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/direct-execution/utils/graphql-direct-execution-to-graphql-api-exception-handler.util.ts @@ -15,6 +15,7 @@ export const graphqlDirectExecutionToGraphqlApiExceptionHandler = ( switch (error.code) { case GraphqlDirectExecutionExceptionCode.INVALID_QUERY_INPUT: throw new UserInputError(error); + case GraphqlDirectExecutionExceptionCode.INVALID_RESULT_TYPE: case GraphqlDirectExecutionExceptionCode.UNKNOWN_METHOD: throw new InternalServerError(error); default: { diff --git a/packages/twenty-server/src/engine/api/graphql/direct-execution/utils/graphql-format-result-from-selected-fields.util.ts b/packages/twenty-server/src/engine/api/graphql/direct-execution/utils/graphql-format-result-from-selected-fields.util.ts index 8e640dd532..3e66663db2 100644 --- a/packages/twenty-server/src/engine/api/graphql/direct-execution/utils/graphql-format-result-from-selected-fields.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/direct-execution/utils/graphql-format-result-from-selected-fields.util.ts @@ -1,15 +1,38 @@ -import { isNull, isObject } from '@sniptt/guards'; -import { RelationType } from 'twenty-shared/types'; -import { isDefined, isEmptyObject, pascalCase } from 'twenty-shared/utils'; +import { isNull } from '@sniptt/guards'; +import { ObjectRecord, RelationType } from 'twenty-shared/types'; -import { CONNECTION_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/connection-method-names'; -import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names'; +import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant'; +import { + GraphqlDirectExecutionException, + GraphqlDirectExecutionExceptionCode, +} from 'src/engine/api/graphql/direct-execution/errors/graphql-direct-execution.exception'; + +import { + isConnection, + isConnectionArray, + isGroupByConnection, + isObjectRecord, + isObjectRecordArray, +} from 'src/engine/api/graphql/direct-execution/utils/graphql-is-resolver-output-type.util'; +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 { IGroupByConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/group-by-connection.interface'; +import { ResolverOutput } from 'src/engine/api/graphql/workspace-query-runner/interfaces/resolver-output'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util'; import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; +import { + getConnectionTypename, + getEdgeTypename, + getGroupByConnectionTypename, + getNodeTypename, + isDefined, + isEmptyObject, + pascalCase, +} from 'twenty-shared/utils'; type GraphQLFormatInput = { flatObjectMetadataMaps: FlatEntityMaps; @@ -22,15 +45,8 @@ type GraphQLFormatContext = GraphQLFormatInput & { fieldMetadataByNameCache: Map>; }; -type GraphQLObjectTypeKind = - | 'connection' - | 'groupByConnection' - | 'edge' - | 'node' - | 'pageInfo'; - export const graphQLFormatResultFromSelectedFields = ( - result: unknown, + result: ResolverOutput, selectedFields: Record, objectNameSingular: string, input: GraphQLFormatInput, @@ -40,59 +56,75 @@ export const graphQLFormatResultFromSelectedFields = ( fieldMetadataByNameCache: new Map(), }; - const objectTypeKind = inferObjectTypeKind(context.method); - - return format( - result, - selectedFields, - objectNameSingular, - objectTypeKind, - context, - ); -}; - -const format = ( - value: unknown, - selectedFields: Record, - objectNameSingular: string, - objectTypeKind: GraphQLObjectTypeKind, - context: GraphQLFormatContext, -): unknown => { - if (value === null || value === undefined) { - return value; - } - - if (Array.isArray(value)) { - return value.map((item) => - format(item, selectedFields, objectNameSingular, objectTypeKind, context), - ); - } - - if (isObject(value)) { - return backfillNullValuesAndComputeTypeName( - value as Record, + if (isObjectRecord(result)) { + return backfillNullValuesAndComputeTypeNameForObjectRecord( + result, selectedFields, objectNameSingular, - objectTypeKind, context, ); } - return value; + if (isObjectRecordArray(result)) { + return result.map((item) => + backfillNullValuesAndComputeTypeNameForObjectRecord( + item, + selectedFields, + objectNameSingular, + context, + ), + ); + } + + if (isGroupByConnection(result)) { + return backfillNullValuesAndComputeTypeNameForGroupByConnection( + result, + selectedFields, + objectNameSingular, + context, + ); + } + + if (isConnection(result)) { + return backfillNullValuesAndComputeTypeNameForConnection( + result, + selectedFields, + objectNameSingular, + context, + ); + } + + if (isConnectionArray(result)) { + return backfillNullValuesAndComputeTypeNameForConnectionArray( + result, + selectedFields, + objectNameSingular, + context, + ); + } + + if (result === null || result === undefined) { + return result; + } + + throw new GraphqlDirectExecutionException( + 'Invalid result type', + GraphqlDirectExecutionExceptionCode.INVALID_RESULT_TYPE, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, + ); }; -const backfillNullValuesAndComputeTypeName = ( - record: Record, +const backfillNullValuesAndComputeTypeNameForObjectRecord = ( + record: ObjectRecord, selectedFields: Record, objectNameSingular: string, - objectTypeKind: GraphQLObjectTypeKind, context: GraphQLFormatContext, ): Record => { const formatted: Record = {}; for (const [key, subFields] of Object.entries(selectedFields)) { if (key === '__typename') { - formatted.__typename = deriveTypeName(objectNameSingular, objectTypeKind); + formatted.__typename = getNodeTypename(objectNameSingular); continue; } @@ -110,29 +142,24 @@ const backfillNullValuesAndComputeTypeName = ( continue; } - const childObjectTypeKind = CONNECTION_FIELD_TO_OBJECT_TYPE_KIND[key]; - - if (isDefined(childObjectTypeKind)) { - formatted[key] = format( - value, - subFields as Record, - objectNameSingular, - childObjectTypeKind, - context, - ); - continue; - } - const relationInfo = findRelationInfo(objectNameSingular, key, context); if (isDefined(relationInfo)) { - formatted[key] = format( - value, - subFields as Record, - relationInfo.targetObjectNameSingular, - relationInfo.objectTypeKind, - context, - ); + if (relationInfo.relationType === RelationType.ONE_TO_MANY) { + formatted[key] = backfillNullValuesAndComputeTypeNameForConnection( + value as IConnection>, + subFields as Record, + relationInfo.targetObjectNameSingular, + context, + ); + } else { + formatted[key] = backfillNullValuesAndComputeTypeNameForObjectRecord( + value, + subFields as Record, + relationInfo.targetObjectNameSingular, + context, + ); + } continue; } @@ -179,45 +206,121 @@ const backfillNullValuesAndComputeTypeNameForCompositeField = ( return formatted; }; -const deriveTypeName = ( +const backfillNullValuesAndComputeTypeNameForConnection = ( + connection: IConnection>, + selectedFields: Record, objectNameSingular: string, - objectTypeKind: GraphQLObjectTypeKind, -): string => { - const pascal = pascalCase(objectNameSingular); + context: GraphQLFormatContext, +): Record => { + const formatted: Record = {}; - switch (objectTypeKind) { - case 'connection': - return `${pascal}Connection`; - case 'groupByConnection': - return `${pascal}GroupByConnection`; - case 'edge': - return `${pascal}Edge`; - case 'node': - return pascal; - case 'pageInfo': - return 'PageInfo'; + for (const [key, subFields] of Object.entries(selectedFields)) { + if (key === '__typename') { + formatted.__typename = getConnectionTypename(objectNameSingular); + continue; + } + + if (key === 'edges') { + formatted.edges = connection.edges.map((edge) => { + const edgeFormatted: Record = {}; + + for (const [edgeKey, edgeSubFields] of Object.entries( + subFields as Record, + )) { + if (edgeKey === '__typename') { + edgeFormatted.__typename = getEdgeTypename(objectNameSingular); + continue; + } + + if (edgeKey === 'cursor') { + edgeFormatted.cursor = edge.cursor; + continue; + } + + if (edgeKey === 'node') { + edgeFormatted.node = + backfillNullValuesAndComputeTypeNameForObjectRecord( + edge.node, + edgeSubFields as Record, + objectNameSingular, + context, + ); + continue; + } + } + + return edgeFormatted; + }); + continue; + } + + if (key === 'pageInfo') { + const pageInfoFormatted: Record = {}; + + for (const pageInfoKey of Object.keys( + subFields as Record, + )) { + if (pageInfoKey === '__typename') { + pageInfoFormatted.__typename = 'PageInfo'; + continue; + } + + pageInfoFormatted[pageInfoKey] = + (connection.pageInfo as unknown as Record)[ + pageInfoKey + ] ?? null; + } + + formatted.pageInfo = pageInfoFormatted; + continue; + } + + //aggregate fields + formatted[key] = + (connection as unknown as Record)[key] ?? null; } + + return formatted; }; -const inferObjectTypeKind = (method: string): GraphQLObjectTypeKind => { - if (method === RESOLVER_METHOD_NAMES.GROUP_BY) { - return 'groupByConnection'; +const backfillNullValuesAndComputeTypeNameForGroupByConnection = ( + connection: IGroupByConnection>, + selectedFields: Record, + objectNameSingular: string, + context: GraphQLFormatContext, +): Record => { + const formatted = backfillNullValuesAndComputeTypeNameForConnection( + connection, + selectedFields, + objectNameSingular, + context, + ); + + if ('__typename' in selectedFields) { + formatted.__typename = getGroupByConnectionTypename(objectNameSingular); } - if (CONNECTION_METHOD_NAMES.has(method)) { - return 'connection'; + if ('groupByDimensionValues' in selectedFields) { + formatted.groupByDimensionValues = connection.groupByDimensionValues; } - return 'node'; + return formatted; }; -const CONNECTION_FIELD_TO_OBJECT_TYPE_KIND: Record< - string, - GraphQLObjectTypeKind -> = { - edges: 'edge', - node: 'node', - pageInfo: 'pageInfo', +const backfillNullValuesAndComputeTypeNameForConnectionArray = ( + connections: IConnection>[], + selectedFields: Record, + objectNameSingular: string, + context: GraphQLFormatContext, +): Record[] => { + return connections.map((connection) => + backfillNullValuesAndComputeTypeNameForConnection( + connection, + selectedFields, + objectNameSingular, + context, + ), + ); }; const getOrBuildFieldMetadataByNameMap = ( @@ -278,7 +381,7 @@ const findFieldMetadataByName = ( type RelationInfo = { targetObjectNameSingular: string; - objectTypeKind: GraphQLObjectTypeKind; + relationType: RelationType; }; const findRelationInfo = ( @@ -308,13 +411,8 @@ const findRelationInfo = ( return undefined; } - const objectTypeKind: GraphQLObjectTypeKind = - fieldMetadata.settings.relationType === RelationType.ONE_TO_MANY - ? 'connection' - : 'node'; - return { targetObjectNameSingular: targetObjectMetadata.nameSingular, - objectTypeKind, + relationType: fieldMetadata.settings.relationType, }; }; diff --git a/packages/twenty-server/src/engine/api/graphql/direct-execution/utils/graphql-is-resolver-output-type.util.ts b/packages/twenty-server/src/engine/api/graphql/direct-execution/utils/graphql-is-resolver-output-type.util.ts new file mode 100644 index 0000000000..6111802c3a --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/direct-execution/utils/graphql-is-resolver-output-type.util.ts @@ -0,0 +1,37 @@ +import { isObject } from '@sniptt/guards'; +import { ObjectRecord } from 'twenty-shared/types'; + +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 { IGroupByConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/group-by-connection.interface'; +import { ResolverOutput } from 'src/engine/api/graphql/workspace-query-runner/interfaces/resolver-output'; + +export const isObjectRecord = ( + result: ResolverOutput, +): result is ObjectRecord => { + return !Array.isArray(result) && isObject(result) && 'id' in result; +}; + +export const isObjectRecordArray = ( + result: ResolverOutput, +): result is ObjectRecord[] => { + return Array.isArray(result) && result.every((item) => isObjectRecord(item)); +}; + +export const isConnection = ( + result: ResolverOutput, +): result is IConnection> => { + return isObject(result) && 'edges' in result && 'pageInfo' in result; +}; + +export const isConnectionArray = ( + result: ResolverOutput, +): result is IConnection>[] => { + return Array.isArray(result) && result.every((item) => isConnection(item)); +}; + +export const isGroupByConnection = ( + result: ResolverOutput, +): result is IGroupByConnection> => { + return isConnection(result) && 'groupByDimensionValues' in result; +}; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/interfaces/resolver-output.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/interfaces/resolver-output.ts new file mode 100644 index 0000000000..9acdfb097a --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/interfaces/resolver-output.ts @@ -0,0 +1,11 @@ +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 { IGroupByConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/group-by-connection.interface'; +import { ObjectRecord } from 'twenty-shared/types'; + +export type ResolverOutput = + | ObjectRecord // findOne, createOne, updateOne, deleteOne, destroyOne, restoreOne, mergeMany + | ObjectRecord[] // createMany, updateMany, deleteMany, destroyMany, restoreMany + | IConnection> // findMany + | IConnection>[] // findDuplicates + | IGroupByConnection>; // groupBy diff --git a/packages/twenty-server/test/integration/metadata/suites/application/application-token-schema-filtering.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/application/application-token-schema-filtering.integration-spec.ts index 1e7667c6ec..b9a2c45531 100644 --- a/packages/twenty-server/test/integration/metadata/suites/application/application-token-schema-filtering.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/application/application-token-schema-filtering.integration-spec.ts @@ -3,6 +3,7 @@ import { findManyApplications } from 'test/integration/graphql/utils/find-many-a import { generateApplicationToken } from 'test/integration/metadata/suites/application/utils/generate-application-token.util'; import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications'; +import { getConnectionTypename, getEdgeTypename } from 'twenty-shared/utils'; const INTROSPECTION_QUERY = ` query IntrospectionQuery { @@ -75,8 +76,8 @@ describe('Application token schema filtering', () => { for (const customTypeName of CUSTOM_OBJECT_TYPE_NAMES) { expect(typeNames).not.toContain(customTypeName); - expect(typeNames).not.toContain(`${customTypeName}Edge`); - expect(typeNames).not.toContain(`${customTypeName}Connection`); + expect(typeNames).not.toContain(getEdgeTypename(customTypeName)); + expect(typeNames).not.toContain(getConnectionTypename(customTypeName)); } }); diff --git a/packages/twenty-shared/src/utils/graphql/graphql-get-typename.util.ts b/packages/twenty-shared/src/utils/graphql/graphql-get-typename.util.ts new file mode 100644 index 0000000000..b032e23311 --- /dev/null +++ b/packages/twenty-shared/src/utils/graphql/graphql-get-typename.util.ts @@ -0,0 +1,13 @@ +import { pascalCase } from '../strings/pascalCase'; + +export const getNodeTypename = (name: string): string => pascalCase(name); + +export const getConnectionTypename = (objectNameSingular: string): string => + `${pascalCase(objectNameSingular)}Connection`; + +export const getEdgeTypename = (objectNameSingular: string): string => + `${pascalCase(objectNameSingular)}Edge`; + +export const getGroupByConnectionTypename = ( + objectNameSingular: string, +): string => `${pascalCase(objectNameSingular)}GroupByConnection`; diff --git a/packages/twenty-shared/src/utils/index.ts b/packages/twenty-shared/src/utils/index.ts index 01517cad0b..957d571273 100644 --- a/packages/twenty-shared/src/utils/index.ts +++ b/packages/twenty-shared/src/utils/index.ts @@ -137,6 +137,12 @@ export { formatToShortNumber } from './format/formatToShortNumber'; export { fromArrayToUniqueKeyRecord } from './from-array-to-unique-key-record.util'; export { fromArrayToValuesByKeyRecord } from './fromArrayToValuesByKeyRecord.util'; export { getURLSafely } from './getURLSafely'; +export { + getNodeTypename, + getConnectionTypename, + getEdgeTypename, + getGroupByConnectionTypename, +} from './graphql/graphql-get-typename.util'; export { getImageAbsoluteURI } from './image/getImageAbsoluteURI'; export { sanitizeURL,