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
<img width="222" alt="image"
src="https://github.com/user-attachments/assets/2370dc80-49b6-4b63-8d5e-30c5ebdaa062"
/>

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
This commit is contained in:
Charles Bochet
2025-06-23 21:06:17 +02:00
committed by GitHub
parent 6aee42ab22
commit d5c974054d
145 changed files with 1485 additions and 2245 deletions

View File

@ -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 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 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 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 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 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, "\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. * 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. * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */

File diff suppressed because one or more lines are too long

View File

@ -338,34 +338,6 @@ export type ClientAiModelConfig = {
provider: ModelProvider; provider: ModelProvider;
}; };
export type ClientConfig = {
__typename?: 'ClientConfig';
aiModels: Array<ClientAiModelConfig>;
analyticsEnabled: Scalars['Boolean'];
api: ApiConfig;
authProviders: AuthProviders;
billing: Billing;
calendarBookingPageId?: Maybe<Scalars['String']>;
canManageFeatureFlags: Scalars['Boolean'];
captcha: Captcha;
chromeExtensionId?: Maybe<Scalars['String']>;
debugMode: Scalars['Boolean'];
defaultSubdomain?: Maybe<Scalars['String']>;
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<PublicFeatureFlag>;
sentry: Sentry;
signInPrefilled: Scalars['Boolean'];
support: Support;
};
export type ComputeStepOutputSchemaInput = { export type ComputeStepOutputSchemaInput = {
/** Step JSON format */ /** Step JSON format */
step: Scalars['JSON']; step: Scalars['JSON'];
@ -1410,6 +1382,7 @@ export type Object = {
icon?: Maybe<Scalars['String']>; icon?: Maybe<Scalars['String']>;
id: Scalars['UUID']; id: Scalars['UUID'];
imageIdentifierFieldMetadataId?: Maybe<Scalars['String']>; imageIdentifierFieldMetadataId?: Maybe<Scalars['String']>;
indexMetadataList: Array<Index>;
indexMetadatas: ObjectIndexMetadatasConnection; indexMetadatas: ObjectIndexMetadatasConnection;
isActive: Scalars['Boolean']; isActive: Scalars['Boolean'];
isCustom: Scalars['Boolean']; isCustom: Scalars['Boolean'];
@ -1609,7 +1582,6 @@ export type Query = {
billingPortalSession: BillingSessionOutput; billingPortalSession: BillingSessionOutput;
checkUserExists: CheckUserExistOutput; checkUserExists: CheckUserExistOutput;
checkWorkspaceInviteHashIsValid: WorkspaceInviteHashValid; checkWorkspaceInviteHashIsValid: WorkspaceInviteHashValid;
clientConfig: ClientConfig;
currentUser: User; currentUser: User;
currentWorkspace: Workspace; currentWorkspace: Workspace;
field: Field; 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 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<{ export type SearchQueryVariables = Exact<{
searchInput: Scalars['String']; searchInput: Scalars['String'];
limit: Scalars['Int']; limit: Scalars['Int'];
@ -4792,106 +4759,6 @@ export function useGetMeteredProductsUsageLazyQuery(baseOptions?: Apollo.LazyQue
export type GetMeteredProductsUsageQueryHookResult = ReturnType<typeof useGetMeteredProductsUsageQuery>; export type GetMeteredProductsUsageQueryHookResult = ReturnType<typeof useGetMeteredProductsUsageQuery>;
export type GetMeteredProductsUsageLazyQueryHookResult = ReturnType<typeof useGetMeteredProductsUsageLazyQuery>; export type GetMeteredProductsUsageLazyQueryHookResult = ReturnType<typeof useGetMeteredProductsUsageLazyQuery>;
export type GetMeteredProductsUsageQueryResult = Apollo.QueryResult<GetMeteredProductsUsageQuery, GetMeteredProductsUsageQueryVariables>; export type GetMeteredProductsUsageQueryResult = Apollo.QueryResult<GetMeteredProductsUsageQuery, GetMeteredProductsUsageQueryVariables>;
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<GetClientConfigQuery, GetClientConfigQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetClientConfigQuery, GetClientConfigQueryVariables>(GetClientConfigDocument, options);
}
export function useGetClientConfigLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetClientConfigQuery, GetClientConfigQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetClientConfigQuery, GetClientConfigQueryVariables>(GetClientConfigDocument, options);
}
export type GetClientConfigQueryHookResult = ReturnType<typeof useGetClientConfigQuery>;
export type GetClientConfigLazyQueryHookResult = ReturnType<typeof useGetClientConfigLazyQuery>;
export type GetClientConfigQueryResult = Apollo.QueryResult<GetClientConfigQuery, GetClientConfigQueryVariables>;
export const SearchDocument = gql` export const SearchDocument = gql`
query Search($searchInput: String!, $limit: Int!, $after: String, $excludedObjectNameSingulars: [String!], $includedObjectNameSingulars: [String!], $filter: ObjectRecordFilterInput) { query Search($searchInput: String!, $limit: Int!, $after: String, $excludedObjectNameSingulars: [String!], $includedObjectNameSingulars: [String!], $filter: ObjectRecordFilterInput) {
search( search(

View File

@ -5,7 +5,6 @@ import { within } from '@storybook/test';
import { HttpResponse, graphql, http } from 'msw'; import { HttpResponse, graphql, http } from 'msw';
import { GET_PUBLIC_WORKSPACE_DATA_BY_DOMAIN } from '@/auth/graphql/queries/getPublicWorkspaceDataByDomain'; 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 { FIND_MANY_OBJECT_METADATA_ITEMS } from '@/object-metadata/graphql/queries';
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser'; import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
import { REACT_APP_SERVER_BASE_URL } from '~/config'; 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( graphql.query(
getOperationName(GET_PUBLIC_WORKSPACE_DATA_BY_DOMAIN) ?? '', getOperationName(GET_PUBLIC_WORKSPACE_DATA_BY_DOMAIN) ?? '',
() => { () => {

View File

@ -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
}
}
`;

View File

@ -1,6 +1,6 @@
import { ClientConfig } from '@/client-config/types/ClientConfig';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil'; import { useRecoilValue, useSetRecoilState } from 'recoil';
import { ClientConfig } from '~/generated/graphql';
import { clientConfigApiStatusState } from '../states/clientConfigApiStatusState'; import { clientConfigApiStatusState } from '../states/clientConfigApiStatusState';
import { getClientConfig } from '../utils/getClientConfig'; import { getClientConfig } from '../utils/getClientConfig';

View File

@ -1,5 +1,5 @@
import { ClientConfig } from '@/client-config/types/ClientConfig';
import { createState } from 'twenty-ui/utilities'; import { createState } from 'twenty-ui/utilities';
import { ClientConfig } from '~/generated/graphql';
type ClientConfigApiStatus = { type ClientConfigApiStatus = {
isLoadedOnce: boolean; isLoadedOnce: boolean;

View File

@ -0,0 +1,37 @@
import {
ApiConfig,
AuthProviders,
Billing,
Captcha,
ClientAiModelConfig,
PublicFeatureFlag,
Sentry,
Support,
} from '~/generated-metadata/graphql';
export type ClientConfig = {
aiModels: Array<ClientAiModelConfig>;
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<PublicFeatureFlag>;
sentry: Sentry;
signInPrefilled: boolean;
support: Support;
};

View File

@ -1,5 +1,5 @@
import { ClientConfig } from '@/client-config/types/ClientConfig';
import { REACT_APP_SERVER_BASE_URL } from '~/config'; import { REACT_APP_SERVER_BASE_URL } from '~/config';
import { ClientConfig } from '~/generated/graphql';
export const getClientConfig = async (): Promise<ClientConfig> => { export const getClientConfig = async (): Promise<ClientConfig> => {
const response = await fetch(`${REACT_APP_SERVER_BASE_URL}/client-config`, { const response = await fetch(`${REACT_APP_SERVER_BASE_URL}/client-config`, {

View File

@ -1,4 +1,4 @@
import { ClientConfig } from '~/generated/graphql'; import { ClientConfig } from '@/client-config/types/ClientConfig';
import { createState } from 'twenty-ui/utilities'; import { createState } from 'twenty-ui/utilities';
export const domainConfigurationState = createState< export const domainConfigurationState = createState<

View File

@ -25,29 +25,14 @@ export const FIND_MANY_OBJECT_METADATA_ITEMS = gql`
isLabelSyncedWithName isLabelSyncedWithName
isSearchable isSearchable
duplicateCriteria duplicateCriteria
indexMetadatas(paging: { first: 100 }) { indexMetadataList {
edges { id
node { createdAt
id updatedAt
createdAt name
updatedAt indexWhereClause
name indexType
indexWhereClause isUnique
indexType
isUnique
indexFieldMetadatas(paging: { first: 100 }) {
edges {
node {
id
createdAt
updatedAt
order
fieldMetadataId
}
}
}
}
}
} }
fieldsList { fieldsList {
id id

View File

@ -11,6 +11,7 @@ export type ObjectMetadataItem = Omit<
| 'indexMetadatas' | 'indexMetadatas'
| 'labelIdentifierFieldMetadataId' | 'labelIdentifierFieldMetadataId'
| 'fieldsList' | 'fieldsList'
| 'indexMetadataList'
> & { > & {
__typename?: string; __typename?: string;
fields: FieldMetadataItem[]; fields: FieldMetadataItem[];

View File

@ -1,3 +1,4 @@
import { IndexMetadataItem } from '@/object-metadata/types/IndexMetadataItem';
import { objectMetadataItemSchema } from '@/object-metadata/validation-schemas/objectMetadataItemSchema'; import { objectMetadataItemSchema } from '@/object-metadata/validation-schemas/objectMetadataItemSchema';
import { ObjectMetadataItemsQuery } from '~/generated-metadata/graphql'; import { ObjectMetadataItemsQuery } from '~/generated-metadata/graphql';
import { ObjectMetadataItem } from '../types/ObjectMetadataItem'; import { ObjectMetadataItem } from '../types/ObjectMetadataItem';
@ -14,19 +15,21 @@ export const mapPaginatedObjectMetadataItemsToObjectMetadataItems = ({
object.node.labelIdentifierFieldMetadataId, object.node.labelIdentifierFieldMetadataId,
); );
const { fieldsList, ...objectWithoutFieldsList } = object.node; const { fieldsList, indexMetadataList, ...objectWithoutFieldsList } =
object.node;
return { return {
...objectWithoutFieldsList, ...objectWithoutFieldsList,
fields: fieldsList, fields: fieldsList,
labelIdentifierFieldMetadataId, labelIdentifierFieldMetadataId,
indexMetadatas: object.node.indexMetadatas?.edges.map((index) => ({ indexMetadatas: indexMetadataList.map(
...index.node, (index) =>
indexFieldMetadatas: index.node.indexFieldMetadatas?.edges.map( ({
(indexField) => indexField.node, ...index,
), indexFieldMetadatas: [],
})), }) satisfies IndexMetadataItem,
}; ),
} satisfies ObjectMetadataItem;
}) ?? []; }) ?? [];
return formattedObjects; return formattedObjects;

View File

@ -2,7 +2,6 @@ import { getOperationName } from '@apollo/client/utilities';
import { graphql, GraphQLQuery, http, HttpResponse } from 'msw'; import { graphql, GraphQLQuery, http, HttpResponse } from 'msw';
import { TRACK_ANALYTICS } from '@/analytics/graphql/queries/track'; 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 { FIND_MANY_OBJECT_METADATA_ITEMS } from '@/object-metadata/graphql/queries';
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser'; import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
import { REACT_APP_SERVER_BASE_URL } from '~/config'; 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 { mockedFavoriteFoldersData } from '~/testing/mock-data/favorite-folders';
import { mockedNotes } from '~/testing/mock-data/notes'; import { mockedNotes } from '~/testing/mock-data/notes';
import { getPeopleRecordConnectionMock } from '~/testing/mock-data/people'; import { getPeopleRecordConnectionMock } from '~/testing/mock-data/people';
import { mockedPublicWorkspaceDataBySubdomain } from '~/testing/mock-data/publicWorkspaceDataBySubdomain';
import { mockedRemoteTables } from '~/testing/mock-data/remote-tables'; import { mockedRemoteTables } from '~/testing/mock-data/remote-tables';
import { mockedUserData } from '~/testing/mock-data/users'; import { mockedUserData } from '~/testing/mock-data/users';
import { mockedViewsData } from '~/testing/mock-data/views'; import { mockedViewsData } from '~/testing/mock-data/views';
import { mockWorkspaceMembers } from '~/testing/mock-data/workspace-members'; 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_PUBLIC_WORKSPACE_DATA_BY_DOMAIN } from '@/auth/graphql/queries/getPublicWorkspaceDataByDomain';
import { GET_ROLES } from '@/settings/roles/graphql/queries/getRolesQuery'; 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`, () => { http.get(`${REACT_APP_SERVER_BASE_URL}/client-config`, () => {
return HttpResponse.json(mockedClientConfig); return HttpResponse.json(mockedClientConfig);
}), }),

View File

@ -1,8 +1,5 @@
import { import { ClientConfig } from '@/client-config/types/ClientConfig';
CaptchaDriverType, import { CaptchaDriverType, SupportDriver } from '~/generated/graphql';
ClientConfig,
SupportDriver,
} from '~/generated/graphql';
export const mockedClientConfig: ClientConfig = { export const mockedClientConfig: ClientConfig = {
aiModels: [], aiModels: [],

View File

@ -42,10 +42,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"labelPlural": "Workflow Runs", "labelPlural": "Workflow Runs",
"description": "A workflow run", "description": "A workflow run",
"icon": "IconHistoryToggle", "icon": "IconHistoryToggle",
"indexMetadatas": { "indexMetadataList": [],
"__typename": "ObjectIndexMetadatasConnection",
"edges": []
},
"fieldsList": [ "fieldsList": [
{ {
"__typename": "Field", "__typename": "Field",
@ -561,10 +558,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"labelPlural": "Note Targets", "labelPlural": "Note Targets",
"description": "A note target", "description": "A note target",
"icon": "IconCheckbox", "icon": "IconCheckbox",
"indexMetadatas": { "indexMetadataList": [],
"__typename": "ObjectIndexMetadatasConnection",
"edges": []
},
"fieldsList": [ "fieldsList": [
{ {
"__typename": "Field", "__typename": "Field",
@ -1033,40 +1027,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"labelPlural": "Survey results", "labelPlural": "Survey results",
"description": null, "description": null,
"icon": "IconRulerMeasure", "icon": "IconRulerMeasure",
"indexMetadatas": { "indexMetadataList": [],
"__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"
}
}
]
}
}
}
]
},
"fieldsList": [ "fieldsList": [
{ {
"__typename": "Field", "__typename": "Field",
@ -1650,10 +1611,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"labelPlural": "API Keys", "labelPlural": "API Keys",
"description": "An API key", "description": "An API key",
"icon": "IconRobot", "icon": "IconRobot",
"indexMetadatas": { "indexMetadataList": [],
"__typename": "ObjectIndexMetadatasConnection",
"edges": []
},
"fieldsList": [ "fieldsList": [
{ {
"__typename": "Field", "__typename": "Field",
@ -1835,10 +1793,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"labelPlural": "View Filter Groups", "labelPlural": "View Filter Groups",
"description": "(System) View Filter Groups", "description": "(System) View Filter Groups",
"icon": "IconFilterBolt", "icon": "IconFilterBolt",
"indexMetadatas": { "indexMetadataList": [],
"__typename": "ObjectIndexMetadatasConnection",
"edges": []
},
"fieldsList": [ "fieldsList": [
{ {
"__typename": "Field", "__typename": "Field",
@ -2092,10 +2047,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"labelPlural": "Timeline Activities", "labelPlural": "Timeline Activities",
"description": "Aggregated / filtered event to be displayed on the timeline", "description": "Aggregated / filtered event to be displayed on the timeline",
"icon": "IconTimelineEvent", "icon": "IconTimelineEvent",
"indexMetadatas": { "indexMetadataList": [],
"__typename": "ObjectIndexMetadatasConnection",
"edges": []
},
"fieldsList": [ "fieldsList": [
{ {
"__typename": "Field", "__typename": "Field",
@ -2940,10 +2892,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"labelPlural": "Message Threads", "labelPlural": "Message Threads",
"description": "A group of related messages (e.g. email thread, chat thread)", "description": "A group of related messages (e.g. email thread, chat thread)",
"icon": "IconMessage", "icon": "IconMessage",
"indexMetadatas": { "indexMetadataList": [],
"__typename": "ObjectIndexMetadatasConnection",
"edges": []
},
"fieldsList": [ "fieldsList": [
{ {
"__typename": "Field", "__typename": "Field",
@ -3110,10 +3059,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"labelPlural": "View Groups", "labelPlural": "View Groups",
"description": "(System) View Groups", "description": "(System) View Groups",
"icon": "IconTag", "icon": "IconTag",
"indexMetadatas": { "indexMetadataList": [],
"__typename": "ObjectIndexMetadatasConnection",
"edges": []
},
"fieldsList": [ "fieldsList": [
{ {
"__typename": "Field", "__typename": "Field",
@ -3366,40 +3312,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"labelPlural": "Rockets", "labelPlural": "Rockets",
"description": "A rocket", "description": "A rocket",
"icon": "IconRocket", "icon": "IconRocket",
"indexMetadatas": { "indexMetadataList": [],
"__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"
}
}
]
}
}
}
]
},
"fieldsList": [ "fieldsList": [
{ {
"__typename": "Field", "__typename": "Field",
@ -3839,10 +3752,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"labelPlural": "Message Participants", "labelPlural": "Message Participants",
"description": "Message Participants", "description": "Message Participants",
"icon": "IconUserCircle", "icon": "IconUserCircle",
"indexMetadatas": { "indexMetadataList": [],
"__typename": "ObjectIndexMetadatasConnection",
"edges": []
},
"fieldsList": [ "fieldsList": [
{ {
"__typename": "Field", "__typename": "Field",
@ -4203,10 +4113,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"labelPlural": "Views", "labelPlural": "Views",
"description": "(System) Views", "description": "(System) Views",
"icon": "IconLayoutCollage", "icon": "IconLayoutCollage",
"indexMetadatas": { "indexMetadataList": [],
"__typename": "ObjectIndexMetadatasConnection",
"edges": []
},
"fieldsList": [ "fieldsList": [
{ {
"__typename": "Field", "__typename": "Field",
@ -4952,10 +4859,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"labelPlural": "Favorites", "labelPlural": "Favorites",
"description": "A favorite that can be accessed from the left menu", "description": "A favorite that can be accessed from the left menu",
"icon": "IconHeart", "icon": "IconHeart",
"indexMetadatas": { "indexMetadataList": [],
"__typename": "ObjectIndexMetadatasConnection",
"edges": []
},
"fieldsList": [ "fieldsList": [
{ {
"__typename": "Field", "__typename": "Field",
@ -5795,10 +5699,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"labelPlural": "Favorite Folders", "labelPlural": "Favorite Folders",
"description": "A Folder of favorites", "description": "A Folder of favorites",
"icon": "IconFolder", "icon": "IconFolder",
"indexMetadatas": { "indexMetadataList": [],
"__typename": "ObjectIndexMetadatasConnection",
"edges": []
},
"fieldsList": [ "fieldsList": [
{ {
"__typename": "Field", "__typename": "Field",
@ -6007,10 +5908,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"labelPlural": "Attachments", "labelPlural": "Attachments",
"description": "An attachment", "description": "An attachment",
"icon": "IconFileImport", "icon": "IconFileImport",
"indexMetadatas": { "indexMetadataList": [],
"__typename": "ObjectIndexMetadatasConnection",
"edges": []
},
"fieldsList": [ "fieldsList": [
{ {
"__typename": "Field", "__typename": "Field",
@ -6642,10 +6540,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"labelPlural": "Message Channel Message Associations", "labelPlural": "Message Channel Message Associations",
"description": "Message Synced with a Message Channel", "description": "Message Synced with a Message Channel",
"icon": "IconMessage", "icon": "IconMessage",
"indexMetadatas": { "indexMetadataList": [],
"__typename": "ObjectIndexMetadatasConnection",
"edges": []
},
"fieldsList": [ "fieldsList": [
{ {
"__typename": "Field", "__typename": "Field",
@ -6942,10 +6837,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"labelPlural": "View Filters", "labelPlural": "View Filters",
"description": "(System) View Filters", "description": "(System) View Filters",
"icon": "IconFilterBolt", "icon": "IconFilterBolt",
"indexMetadatas": { "indexMetadataList": [],
"__typename": "ObjectIndexMetadatasConnection",
"edges": []
},
"fieldsList": [ "fieldsList": [
{ {
"__typename": "Field", "__typename": "Field",
@ -7261,40 +7153,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"labelPlural": "Tasks", "labelPlural": "Tasks",
"description": "A task", "description": "A task",
"icon": "IconCheckbox", "icon": "IconCheckbox",
"indexMetadatas": { "indexMetadataList": [],
"__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"
}
}
]
}
}
}
]
},
"fieldsList": [ "fieldsList": [
{ {
"__typename": "Field", "__typename": "Field",
@ -7852,10 +7711,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"labelPlural": "Task Targets", "labelPlural": "Task Targets",
"description": "A task target", "description": "A task target",
"icon": "IconCheckbox", "icon": "IconCheckbox",
"indexMetadatas": { "indexMetadataList": [],
"__typename": "ObjectIndexMetadatasConnection",
"edges": []
},
"fieldsList": [ "fieldsList": [
{ {
"__typename": "Field", "__typename": "Field",
@ -8324,10 +8180,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"labelPlural": "Calendar events", "labelPlural": "Calendar events",
"description": "Calendar events", "description": "Calendar events",
"icon": "IconCalendar", "icon": "IconCalendar",
"indexMetadatas": { "indexMetadataList": [],
"__typename": "ObjectIndexMetadatasConnection",
"edges": []
},
"fieldsList": [ "fieldsList": [
{ {
"__typename": "Field", "__typename": "Field",
@ -8798,10 +8651,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"labelPlural": "Blocklists", "labelPlural": "Blocklists",
"description": "Blocklist", "description": "Blocklist",
"icon": "IconForbid2", "icon": "IconForbid2",
"indexMetadatas": { "indexMetadataList": [],
"__typename": "ObjectIndexMetadatasConnection",
"edges": []
},
"fieldsList": [ "fieldsList": [
{ {
"__typename": "Field", "__typename": "Field",
@ -9002,69 +8852,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"labelPlural": "People", "labelPlural": "People",
"description": "A person", "description": "A person",
"icon": "IconUser", "icon": "IconUser",
"indexMetadatas": { "indexMetadataList": [],
"__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"
}
}
]
}
}
}
]
},
"fieldsList": [ "fieldsList": [
{ {
"__typename": "Field", "__typename": "Field",
@ -10013,80 +9801,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"labelPlural": "Opportunities", "labelPlural": "Opportunities",
"description": "An opportunity", "description": "An opportunity",
"icon": "IconTargetArrow", "icon": "IconTargetArrow",
"indexMetadatas": { "indexMetadataList": [],
"__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"
}
}
]
}
}
}
]
},
"fieldsList": [ "fieldsList": [
{ {
"__typename": "Field", "__typename": "Field",
@ -10735,40 +10450,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"labelPlural": "Pets", "labelPlural": "Pets",
"description": null, "description": null,
"icon": "IconCat", "icon": "IconCat",
"indexMetadatas": { "indexMetadataList": [],
"__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"
}
}
]
}
}
}
]
},
"fieldsList": [ "fieldsList": [
{ {
"__typename": "Field", "__typename": "Field",
@ -11688,10 +11370,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"labelPlural": "WorkflowAutomatedTriggers", "labelPlural": "WorkflowAutomatedTriggers",
"description": "A workflow automated trigger", "description": "A workflow automated trigger",
"icon": "IconSettingsAutomation", "icon": "IconSettingsAutomation",
"indexMetadatas": { "indexMetadataList": [],
"__typename": "ObjectIndexMetadatasConnection",
"edges": []
},
"fieldsList": [ "fieldsList": [
{ {
"__typename": "Field", "__typename": "Field",
@ -11924,69 +11603,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"labelPlural": "Companies", "labelPlural": "Companies",
"description": "A company", "description": "A company",
"icon": "IconBuildingSkyscraper", "icon": "IconBuildingSkyscraper",
"indexMetadatas": { "indexMetadataList": [],
"__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"
}
}
]
}
}
}
]
},
"fieldsList": [ "fieldsList": [
{ {
"__typename": "Field", "__typename": "Field",
@ -12860,10 +12477,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"labelPlural": "Connected Accounts", "labelPlural": "Connected Accounts",
"description": "A connected account", "description": "A connected account",
"icon": "IconAt", "icon": "IconAt",
"indexMetadatas": { "indexMetadataList": [],
"__typename": "ObjectIndexMetadatasConnection",
"edges": []
},
"fieldsList": [ "fieldsList": [
{ {
"__typename": "Field", "__typename": "Field",
@ -13296,40 +12910,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"labelPlural": "Notes", "labelPlural": "Notes",
"description": "A note", "description": "A note",
"icon": "IconNotes", "icon": "IconNotes",
"indexMetadatas": { "indexMetadataList": [],
"__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"
}
}
]
}
}
}
]
},
"fieldsList": [ "fieldsList": [
{ {
"__typename": "Field", "__typename": "Field",
@ -13773,10 +13354,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"labelPlural": "View Fields", "labelPlural": "View Fields",
"description": "(System) View Fields", "description": "(System) View Fields",
"icon": "IconTag", "icon": "IconTag",
"indexMetadatas": { "indexMetadataList": [],
"__typename": "ObjectIndexMetadatasConnection",
"edges": []
},
"fieldsList": [ "fieldsList": [
{ {
"__typename": "Field", "__typename": "Field",
@ -14135,10 +13713,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"labelPlural": "Workflow Versions", "labelPlural": "Workflow Versions",
"description": "A workflow version", "description": "A workflow version",
"icon": "IconVersions", "icon": "IconVersions",
"indexMetadatas": { "indexMetadataList": [],
"__typename": "ObjectIndexMetadatasConnection",
"edges": []
},
"fieldsList": [ "fieldsList": [
{ {
"__typename": "Field", "__typename": "Field",
@ -14585,10 +14160,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"labelPlural": "Calendar event participants", "labelPlural": "Calendar event participants",
"description": "Calendar event participants", "description": "Calendar event participants",
"icon": "IconCalendar", "icon": "IconCalendar",
"indexMetadatas": { "indexMetadataList": [],
"__typename": "ObjectIndexMetadatasConnection",
"edges": []
},
"fieldsList": [ "fieldsList": [
{ {
"__typename": "Field", "__typename": "Field",
@ -14970,10 +14542,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"labelPlural": "Workflows", "labelPlural": "Workflows",
"description": "A workflow", "description": "A workflow",
"icon": "IconSettingsAutomation", "icon": "IconSettingsAutomation",
"indexMetadatas": { "indexMetadataList": [],
"__typename": "ObjectIndexMetadatasConnection",
"edges": []
},
"fieldsList": [ "fieldsList": [
{ {
"__typename": "Field", "__typename": "Field",
@ -15460,40 +15029,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"labelPlural": "Workspace Members", "labelPlural": "Workspace Members",
"description": "A workspace member", "description": "A workspace member",
"icon": "IconUserCircle", "icon": "IconUserCircle",
"indexMetadatas": { "indexMetadataList": [],
"__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"
}
}
]
}
}
}
]
},
"fieldsList": [ "fieldsList": [
{ {
"__typename": "Field", "__typename": "Field",
@ -16329,10 +15865,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"labelPlural": "Webhooks", "labelPlural": "Webhooks",
"description": "A webhook", "description": "A webhook",
"icon": "IconRobot", "icon": "IconRobot",
"indexMetadatas": { "indexMetadataList": [],
"__typename": "ObjectIndexMetadatasConnection",
"edges": []
},
"fieldsList": [ "fieldsList": [
{ {
"__typename": "Field", "__typename": "Field",
@ -16537,10 +16070,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"labelPlural": "Message Folders", "labelPlural": "Message Folders",
"description": "Folder for Message Channel", "description": "Folder for Message Channel",
"icon": "IconFolder", "icon": "IconFolder",
"indexMetadatas": { "indexMetadataList": [],
"__typename": "ObjectIndexMetadatasConnection",
"edges": []
},
"fieldsList": [ "fieldsList": [
{ {
"__typename": "Field", "__typename": "Field",
@ -16751,10 +16281,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"labelPlural": "View Sorts", "labelPlural": "View Sorts",
"description": "(System) View Sorts", "description": "(System) View Sorts",
"icon": "IconArrowsSort", "icon": "IconArrowsSort",
"indexMetadatas": { "indexMetadataList": [],
"__typename": "ObjectIndexMetadatasConnection",
"edges": []
},
"fieldsList": [ "fieldsList": [
{ {
"__typename": "Field", "__typename": "Field",
@ -16965,10 +16492,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"labelPlural": "Calendar Channel Event Associations", "labelPlural": "Calendar Channel Event Associations",
"description": "Calendar Channel Event Associations", "description": "Calendar Channel Event Associations",
"icon": "IconCalendar", "icon": "IconCalendar",
"indexMetadatas": { "indexMetadataList": [],
"__typename": "ObjectIndexMetadatasConnection",
"edges": []
},
"fieldsList": [ "fieldsList": [
{ {
"__typename": "Field", "__typename": "Field",
@ -17229,10 +16753,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"labelPlural": "Calendar Channels", "labelPlural": "Calendar Channels",
"description": "Calendar Channels", "description": "Calendar Channels",
"icon": "IconCalendar", "icon": "IconCalendar",
"indexMetadatas": { "indexMetadataList": [],
"__typename": "ObjectIndexMetadatasConnection",
"edges": []
},
"fieldsList": [ "fieldsList": [
{ {
"__typename": "Field", "__typename": "Field",
@ -17803,10 +17324,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"labelPlural": "Message Channels", "labelPlural": "Message Channels",
"description": "Message Channels", "description": "Message Channels",
"icon": "IconMessage", "icon": "IconMessage",
"indexMetadatas": { "indexMetadataList": [],
"__typename": "ObjectIndexMetadatasConnection",
"edges": []
},
"fieldsList": [ "fieldsList": [
{ {
"__typename": "Field", "__typename": "Field",
@ -18503,10 +18021,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"labelPlural": "Messages", "labelPlural": "Messages",
"description": "A message sent or received through a messaging channel (email, chat, etc.)", "description": "A message sent or received through a messaging channel (email, chat, etc.)",
"icon": "IconMessage", "icon": "IconMessage",
"indexMetadatas": { "indexMetadataList": [],
"__typename": "ObjectIndexMetadatasConnection",
"edges": []
},
"fieldsList": [ "fieldsList": [
{ {
"__typename": "Field", "__typename": "Field",

View File

@ -10,17 +10,16 @@ export const generatedMockObjectMetadataItems: ObjectMetadataItem[] =
edge.node.labelIdentifierFieldMetadataId, edge.node.labelIdentifierFieldMetadataId,
); );
const { fieldsList, ...objectWithoutFieldsList } = edge.node; const { fieldsList, indexMetadataList, ...objectWithoutFieldsList } =
edge.node;
return { return {
...objectWithoutFieldsList, ...objectWithoutFieldsList,
fields: fieldsList, fields: fieldsList,
labelIdentifierFieldMetadataId, labelIdentifierFieldMetadataId,
indexMetadatas: edge.node.indexMetadatas.edges.map((index) => ({ indexMetadatas: indexMetadataList.map((index) => ({
...index.node, ...index,
indexFieldMetadatas: index.node.indexFieldMetadatas?.edges.map( indexFieldMetadatas: [],
(indexField) => indexField.node,
),
})), })),
}; };
}); });

View File

@ -140,7 +140,6 @@ export abstract class ActiveOrSuspendedWorkspacesMigrationCommandRunner<
const dataSource = const dataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace({ await this.twentyORMGlobalManager.getDataSourceForWorkspace({
workspaceId, workspaceId,
shouldFailIfMetadataNotFound: false,
}); });
await this.runOnWorkspace({ await this.runOnWorkspace({

View File

@ -0,0 +1,30 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class RemoveRelationMetadata1750673748111 implements MigrationInterface {
name = 'RemoveRelationMetadata1750673748111';
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
await queryRunner.query(
`DROP INDEX "core"."IDX_DATA_SOURCE_WORKSPACE_ID_CREATED_AT"`,
);
}
}

View File

@ -307,22 +307,34 @@ export const objectMetadataItemMock = {
export const objectMetadataMapItemMock = { export const objectMetadataMapItemMock = {
id: 'mockObjectId', id: 'mockObjectId',
icon: 'Icon123',
nameSingular: 'objectName', nameSingular: 'objectName',
namePlural: 'objectsName', namePlural: 'objectsName',
fields,
fieldsById: fields.reduce((acc, field) => { fieldsById: fields.reduce((acc, field) => {
// @ts-expect-error legacy noImplicitAny // @ts-expect-error legacy noImplicitAny
acc[field.id] = field; acc[field.id] = field;
return acc; return acc;
}, {}), }, {}),
fieldsByName: fields.reduce((acc, field) => { fieldIdByName: fields.reduce((acc, field) => {
// @ts-expect-error legacy noImplicitAny // @ts-expect-error legacy noImplicitAny
acc[field.name] = field; acc[field.name] = field;
return acc; 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 = { export const objectMetadataMapsMock = {
byId: { byId: {

View File

@ -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 { 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'; import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
export const mockPersonObjectMetadata = ( export const mockPersonObjectMetadataWithFieldMaps = (
duplicateCriteria: WorkspaceEntityDuplicateCriteria[], duplicateCriteria: WorkspaceEntityDuplicateCriteria[],
): ObjectMetadataItemWithFieldMaps => ({ ): ObjectMetadataItemWithFieldMaps => ({
id: '', id: '',
icon: 'Icon123',
standardId: '', standardId: '',
nameSingular: 'person', nameSingular: 'person',
namePlural: 'people', namePlural: 'people',
@ -24,12 +25,16 @@ export const mockPersonObjectMetadata = (
labelIdentifierFieldMetadataId: '', labelIdentifierFieldMetadataId: '',
imageIdentifierFieldMetadataId: '', imageIdentifierFieldMetadataId: '',
workspaceId: '', workspaceId: '',
fields: [],
indexMetadatas: [], indexMetadatas: [],
fieldsById: {}, fieldIdByName: {
fieldsByJoinColumnName: {}, name: 'name-id',
fieldsByName: { emails: 'emails-id',
name: { linkedinLink: 'linkedinLink-id',
jobTitle: 'jobTitle-id',
},
fieldIdByJoinColumnName: {},
fieldsById: {
'name-id': {
id: '', id: '',
objectMetadataId: '', objectMetadataId: '',
type: FieldMetadataType.FULL_NAME, type: FieldMetadataType.FULL_NAME,
@ -44,8 +49,11 @@ export const mockPersonObjectMetadata = (
isNullable: true, isNullable: true,
isUnique: false, isUnique: false,
workspaceId: '', workspaceId: '',
isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
}, },
emails: { 'emails-id': {
id: '', id: '',
objectMetadataId: '', objectMetadataId: '',
type: FieldMetadataType.EMAILS, type: FieldMetadataType.EMAILS,
@ -57,9 +65,13 @@ export const mockPersonObjectMetadata = (
}, },
description: 'Contacts Emails', description: 'Contacts Emails',
isCustom: false, isCustom: false,
isNullable: true,
workspaceId: '', workspaceId: '',
isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
}, },
linkedinLink: { 'linkedinLink-id': {
id: '', id: '',
objectMetadataId: '', objectMetadataId: '',
type: FieldMetadataType.LINKS, type: FieldMetadataType.LINKS,
@ -75,8 +87,11 @@ export const mockPersonObjectMetadata = (
isNullable: true, isNullable: true,
isUnique: false, isUnique: false,
workspaceId: '', workspaceId: '',
isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
}, },
jobTitle: { 'jobTitle-id': {
id: '', id: '',
objectMetadataId: '', objectMetadataId: '',
type: FieldMetadataType.TEXT, type: FieldMetadataType.TEXT,
@ -88,6 +103,9 @@ export const mockPersonObjectMetadata = (
isNullable: false, isNullable: false,
isUnique: false, isUnique: false,
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419', workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
}, },
}, },
}); });

View File

@ -15,6 +15,7 @@ export enum GraphqlQueryRunnerExceptionCode {
UNSUPPORTED_OPERATOR = 'UNSUPPORTED_OPERATOR', UNSUPPORTED_OPERATOR = 'UNSUPPORTED_OPERATOR',
ARGS_CONFLICT = 'ARGS_CONFLICT', ARGS_CONFLICT = 'ARGS_CONFLICT',
FIELD_NOT_FOUND = 'FIELD_NOT_FOUND', FIELD_NOT_FOUND = 'FIELD_NOT_FOUND',
MISSING_SYSTEM_FIELD = 'MISSING_SYSTEM_FIELD',
OBJECT_METADATA_NOT_FOUND = 'OBJECT_METADATA_NOT_FOUND', OBJECT_METADATA_NOT_FOUND = 'OBJECT_METADATA_NOT_FOUND',
RECORD_NOT_FOUND = 'RECORD_NOT_FOUND', RECORD_NOT_FOUND = 'RECORD_NOT_FOUND',
INVALID_ARGS_FIRST = 'INVALID_ARGS_FIRST', INVALID_ARGS_FIRST = 'INVALID_ARGS_FIRST',

View File

@ -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 { 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 { WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder';
import { GraphqlQueryFilterFieldParser } from './graphql-query-filter-field.parser'; import { GraphqlQueryFilterFieldParser } from './graphql-query-filter-field.parser';
export class GraphqlQueryFilterConditionParser { export class GraphqlQueryFilterConditionParser {
private fieldMetadataMapByName: FieldMetadataMap; private objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
private fieldMetadataMapByJoinColumnName: FieldMetadataMap;
private queryFilterFieldParser: GraphqlQueryFilterFieldParser; private queryFilterFieldParser: GraphqlQueryFilterFieldParser;
constructor( constructor(objectMetadataMapItem: ObjectMetadataItemWithFieldMaps) {
fieldMetadataMapByName: FieldMetadataMap, this.objectMetadataMapItem = objectMetadataMapItem;
fieldMetadataMapByJoinColumnName: FieldMetadataMap,
) {
this.fieldMetadataMapByName = fieldMetadataMapByName;
this.fieldMetadataMapByJoinColumnName = fieldMetadataMapByJoinColumnName;
this.queryFilterFieldParser = new GraphqlQueryFilterFieldParser( this.queryFilterFieldParser = new GraphqlQueryFilterFieldParser(
this.fieldMetadataMapByName, this.objectMetadataMapItem,
this.fieldMetadataMapByJoinColumnName,
); );
} }

View File

@ -10,21 +10,16 @@ import {
import { computeWhereConditionParts } from 'src/engine/api/graphql/graphql-query-runner/utils/compute-where-condition-parts'; 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 { 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 { 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'; import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory';
const ARRAY_OPERATORS = ['in', 'contains', 'notContains']; const ARRAY_OPERATORS = ['in', 'contains', 'notContains'];
export class GraphqlQueryFilterFieldParser { export class GraphqlQueryFilterFieldParser {
private fieldMetadataMapByName: FieldMetadataMap; private objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
private fieldMetadataMapByJoinColumnName: FieldMetadataMap;
constructor( constructor(objectMetadataMapItem: ObjectMetadataItemWithFieldMaps) {
fieldMetadataMapByName: FieldMetadataMap, this.objectMetadataMapItem = objectMetadataMapItem;
fieldMetadataMapByJoinColumnName: FieldMetadataMap,
) {
this.fieldMetadataMapByName = fieldMetadataMapByName;
this.fieldMetadataMapByJoinColumnName = fieldMetadataMapByJoinColumnName;
} }
public parse( public parse(
@ -35,9 +30,12 @@ export class GraphqlQueryFilterFieldParser {
filterValue: any, filterValue: any,
isFirst = false, isFirst = false,
): void { ): void {
const fieldMetadataId =
this.objectMetadataMapItem.fieldIdByName[`${key}`] ||
this.objectMetadataMapItem.fieldIdByJoinColumnName[`${key}`];
const fieldMetadata = const fieldMetadata =
this.fieldMetadataMapByName[`${key}`] || this.objectMetadataMapItem.fieldsById[fieldMetadataId];
this.fieldMetadataMapByJoinColumnName[`${key}`];
if (!fieldMetadata) { if (!fieldMetadata) {
throw new Error(`Field metadata not found for field: ${key}`); throw new Error(`Field metadata not found for field: ${key}`);

View File

@ -12,14 +12,14 @@ import {
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; } 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 { 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 { 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'; import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory';
export class GraphqlQueryOrderFieldParser { export class GraphqlQueryOrderFieldParser {
private fieldMetadataMapByName: FieldMetadataMap; private objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
constructor(fieldMetadataMapByName: FieldMetadataMap) { constructor(objectMetadataMapItem: ObjectMetadataItemWithFieldMaps) {
this.fieldMetadataMapByName = fieldMetadataMapByName; this.objectMetadataMapItem = objectMetadataMapItem;
} }
parse( parse(
@ -30,7 +30,9 @@ export class GraphqlQueryOrderFieldParser {
return orderBy.reduce( return orderBy.reduce(
(acc, item) => { (acc, item) => {
Object.entries(item).forEach(([key, value]) => { 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) { if (!fieldMetadata || value === undefined) {
throw new GraphqlQueryRunnerException( throw new GraphqlQueryRunnerException(

View File

@ -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 { GraphqlQuerySelectedFieldsResult } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser';
import { import {
AggregationField, AggregationField,
getAvailableAggregationsFromObjectFields, getAvailableAggregationsFromObjectFields,
} from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util'; } 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 { export class GraphqlQuerySelectedFieldsAggregateParser {
parse( parse(
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
graphqlSelectedFields: Partial<Record<string, any>>, graphqlSelectedFields: Partial<Record<string, any>>,
fieldMetadataMapByName: Record<string, FieldMetadataInterface>, objectMetadataMapItem: ObjectMetadataItemWithFieldMaps,
accumulator: GraphqlQuerySelectedFieldsResult, accumulator: GraphqlQuerySelectedFieldsResult,
): void { ): void {
const availableAggregations: Record<string, AggregationField> = const availableAggregations: Record<string, AggregationField> =
getAvailableAggregationsFromObjectFields( getAvailableAggregationsFromObjectFields(
Object.values(fieldMetadataMapByName), Object.values(objectMetadataMapItem.fieldsById),
); );
for (const selectedField of Object.keys(graphqlSelectedFields)) { for (const selectedField of Object.keys(graphqlSelectedFields)) {

View File

@ -32,11 +32,13 @@ export class GraphqlQuerySelectedFieldsRelationParser {
this.objectMetadataMaps, this.objectMetadataMaps,
); );
const targetFields = targetObjectMetadata.fieldsByName;
const fieldParser = new GraphqlQuerySelectedFieldsParser( const fieldParser = new GraphqlQuerySelectedFieldsParser(
this.objectMetadataMaps, this.objectMetadataMaps,
); );
const relationAccumulator = fieldParser.parse(fieldValue, targetFields); const relationAccumulator = fieldParser.parse(
fieldValue,
targetObjectMetadata,
);
accumulator.select[fieldKey] = { accumulator.select[fieldKey] = {
id: true, id: true,

View File

@ -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 { 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 { 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 { 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 { 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 { 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'; import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
@ -32,7 +33,7 @@ export class GraphqlQuerySelectedFieldsParser {
parse( parse(
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
graphqlSelectedFields: Partial<Record<string, any>>, graphqlSelectedFields: Partial<Record<string, any>>,
fieldMetadataMapByName: Record<string, FieldMetadataInterface>, objectMetadataMapItem: ObjectMetadataItemWithFieldMaps,
): GraphqlQuerySelectedFieldsResult { ): GraphqlQuerySelectedFieldsResult {
const accumulator: GraphqlQuerySelectedFieldsResult = { const accumulator: GraphqlQuerySelectedFieldsResult = {
select: {}, select: {},
@ -43,7 +44,7 @@ export class GraphqlQuerySelectedFieldsParser {
if (this.isRootConnection(graphqlSelectedFields)) { if (this.isRootConnection(graphqlSelectedFields)) {
this.parseConnectionField( this.parseConnectionField(
graphqlSelectedFields, graphqlSelectedFields,
fieldMetadataMapByName, objectMetadataMapItem,
accumulator, accumulator,
); );
@ -52,13 +53,13 @@ export class GraphqlQuerySelectedFieldsParser {
this.aggregateParser.parse( this.aggregateParser.parse(
graphqlSelectedFields, graphqlSelectedFields,
fieldMetadataMapByName, objectMetadataMapItem,
accumulator, accumulator,
); );
this.parseRecordField( this.parseRecordField(
graphqlSelectedFields, graphqlSelectedFields,
fieldMetadataMapByName, objectMetadataMapItem,
accumulator, accumulator,
); );
@ -68,13 +69,16 @@ export class GraphqlQuerySelectedFieldsParser {
private parseRecordField( private parseRecordField(
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
graphqlSelectedFields: Partial<Record<string, any>>, graphqlSelectedFields: Partial<Record<string, any>>,
fieldMetadataMapByName: Record<string, FieldMetadataInterface>, objectMetadataMapItem: ObjectMetadataItemWithFieldMaps,
accumulator: GraphqlQuerySelectedFieldsResult, accumulator: GraphqlQuerySelectedFieldsResult,
): void { ): void {
for (const [fieldKey, fieldValue] of Object.entries( for (const [fieldKey, fieldValue] of Object.entries(
graphqlSelectedFields, graphqlSelectedFields,
)) { )) {
const fieldMetadata = fieldMetadataMapByName[fieldKey]; const fieldMetadata =
objectMetadataMapItem.fieldsById[
objectMetadataMapItem.fieldIdByName[fieldKey]
];
if (!fieldMetadata) { if (!fieldMetadata) {
continue; continue;
@ -103,18 +107,18 @@ export class GraphqlQuerySelectedFieldsParser {
private parseConnectionField( private parseConnectionField(
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
graphqlSelectedFields: Partial<Record<string, any>>, graphqlSelectedFields: Partial<Record<string, any>>,
fieldMetadataMapByName: Record<string, FieldMetadataInterface>, objectMetadataMapItem: ObjectMetadataItemWithFieldMaps,
accumulator: GraphqlQuerySelectedFieldsResult, accumulator: GraphqlQuerySelectedFieldsResult,
): void { ): void {
this.aggregateParser.parse( this.aggregateParser.parse(
graphqlSelectedFields, graphqlSelectedFields,
fieldMetadataMapByName, objectMetadataMapItem,
accumulator, accumulator,
); );
const node = graphqlSelectedFields.edges.node; const node = graphqlSelectedFields.edges.node;
this.parseRecordField(node, fieldMetadataMapByName, accumulator); this.parseRecordField(node, objectMetadataMapItem, accumulator);
} }
private isRootConnection( private isRootConnection(

View File

@ -15,33 +15,29 @@ import {
GraphqlQuerySelectedFieldsParser, GraphqlQuerySelectedFieldsParser,
GraphqlQuerySelectedFieldsResult, GraphqlQuerySelectedFieldsResult,
} from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser'; } 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 { 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 { 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 { 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'; import { WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder';
export class GraphqlQueryParser { export class GraphqlQueryParser {
private fieldMetadataMapByName: FieldMetadataMap; private objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
private fieldMetadataMapByJoinColumnName: FieldMetadataMap;
private objectMetadataMaps: ObjectMetadataMaps; private objectMetadataMaps: ObjectMetadataMaps;
private filterConditionParser: GraphqlQueryFilterConditionParser; private filterConditionParser: GraphqlQueryFilterConditionParser;
private orderFieldParser: GraphqlQueryOrderFieldParser; private orderFieldParser: GraphqlQueryOrderFieldParser;
constructor( constructor(
fieldMetadataMapByName: FieldMetadataMap, objectMetadataMapItem: ObjectMetadataItemWithFieldMaps,
fieldMetadataMapByJoinColumnName: FieldMetadataMap,
objectMetadataMaps: ObjectMetadataMaps, objectMetadataMaps: ObjectMetadataMaps,
) { ) {
this.objectMetadataMapItem = objectMetadataMapItem;
this.objectMetadataMaps = objectMetadataMaps; this.objectMetadataMaps = objectMetadataMaps;
this.fieldMetadataMapByName = fieldMetadataMapByName;
this.fieldMetadataMapByJoinColumnName = fieldMetadataMapByJoinColumnName;
this.filterConditionParser = new GraphqlQueryFilterConditionParser( this.filterConditionParser = new GraphqlQueryFilterConditionParser(
this.fieldMetadataMapByName, this.objectMetadataMapItem,
this.fieldMetadataMapByJoinColumnName,
); );
this.orderFieldParser = new GraphqlQueryOrderFieldParser( this.orderFieldParser = new GraphqlQueryOrderFieldParser(
this.fieldMetadataMapByName, this.objectMetadataMapItem,
); );
} }
@ -120,12 +116,12 @@ export class GraphqlQueryParser {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
graphqlSelectedFields: Partial<Record<string, any>>, graphqlSelectedFields: Partial<Record<string, any>>,
): GraphqlQuerySelectedFieldsResult { ): GraphqlQuerySelectedFieldsResult {
const parentFields = getObjectMetadataMapItemByNameSingular( const objectMetadataMapItem = getObjectMetadataMapItemByNameSingular(
this.objectMetadataMaps, this.objectMetadataMaps,
parentObjectMetadata.nameSingular, parentObjectMetadata.nameSingular,
)?.fieldsByName; );
if (!parentFields) { if (!objectMetadataMapItem) {
throw new GraphqlQueryRunnerException( throw new GraphqlQueryRunnerException(
`Could not find object metadata for ${parentObjectMetadata.nameSingular}`, `Could not find object metadata for ${parentObjectMetadata.nameSingular}`,
GraphqlQueryRunnerExceptionCode.OBJECT_METADATA_NOT_FOUND, GraphqlQueryRunnerExceptionCode.OBJECT_METADATA_NOT_FOUND,
@ -136,6 +132,9 @@ export class GraphqlQueryParser {
this.objectMetadataMaps, this.objectMetadataMaps,
); );
return selectedFieldsParser.parse(graphqlSelectedFields, parentFields); return selectedFieldsParser.parse(
graphqlSelectedFields,
objectMetadataMapItem,
);
} }
} }

View File

@ -168,7 +168,8 @@ export class ObjectRecordsToGraphqlConnectionHelper {
const processedObjectRecord: Record<string, any> = {}; const processedObjectRecord: Record<string, any> = {};
for (const [key, value] of Object.entries(objectRecord)) { 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) { if (!fieldMetadata) {
processedObjectRecord[key] = value; processedObjectRecord[key] = value;

View File

@ -103,8 +103,10 @@ export class ProcessNestedRelationsV2Helper {
shouldBypassPermissionChecks: boolean; shouldBypassPermissionChecks: boolean;
roleId?: string; roleId?: string;
}): Promise<void> { }): Promise<void> {
const sourceFieldMetadataId =
parentObjectMetadataItem.fieldIdByName[sourceFieldName];
const sourceFieldMetadata = const sourceFieldMetadata =
parentObjectMetadataItem.fieldsByName[sourceFieldName]; parentObjectMetadataItem.fieldsById[sourceFieldMetadataId];
if ( if (
!isFieldMetadataInterfaceOfType( !isFieldMetadataInterfaceOfType(
@ -219,8 +221,11 @@ export class ProcessNestedRelationsV2Helper {
parentObjectMetadataItem: ObjectMetadataItemWithFieldMaps; parentObjectMetadataItem: ObjectMetadataItemWithFieldMaps;
sourceFieldName: string; sourceFieldName: string;
}) { }) {
const targetFieldMetadataId =
parentObjectMetadataItem.fieldIdByName[sourceFieldName];
const targetFieldMetadata = const targetFieldMetadata =
parentObjectMetadataItem.fieldsByName[sourceFieldName]; parentObjectMetadataItem.fieldsById[targetFieldMetadataId];
const targetObjectMetadata = getTargetObjectMetadataOrThrow( const targetObjectMetadata = getTargetObjectMetadataOrThrow(
targetFieldMetadata, targetFieldMetadata,
objectMetadataMaps, objectMetadataMaps,

View File

@ -93,7 +93,6 @@ export abstract class GraphqlQueryBaseResolverService<
const workspaceDataSource = const workspaceDataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace({ await this.twentyORMGlobalManager.getDataSourceForWorkspace({
workspaceId: workspace.id, workspaceId: workspace.id,
shouldFailIfMetadataNotFound: false,
}); });
const featureFlagsMap = workspaceDataSource.featureFlagMap; const featureFlagsMap = workspaceDataSource.featureFlagMap;
@ -132,8 +131,7 @@ export abstract class GraphqlQueryBaseResolverService<
); );
const graphqlQueryParser = new GraphqlQueryParser( const graphqlQueryParser = new GraphqlQueryParser(
objectMetadataItemWithFieldMaps.fieldsByName, objectMetadataItemWithFieldMaps,
objectMetadataItemWithFieldMaps.fieldsByJoinColumnName,
options.objectMetadataMaps, options.objectMetadataMaps,
); );

View File

@ -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 { 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 { 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 { 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 { 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'; 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 { 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 { 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 { 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 { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import { formatData } from 'src/engine/twenty-orm/utils/format-data.util'; import { formatData } from 'src/engine/twenty-orm/utils/format-data.util';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
@ -128,7 +133,7 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
fullPath: string; fullPath: string;
column: string; column: string;
}[] { }[] {
return objectMetadataItemWithFieldMaps.fields return Object.values(objectMetadataItemWithFieldMaps.fieldsById)
.filter((field) => field.isUnique || field.name === 'id') .filter((field) => field.isUnique || field.name === 'id')
.flatMap((field) => { .flatMap((field) => {
const compositeType = compositeTypeDefinitions.get(field.type); const compositeType = compositeTypeDefinitions.get(field.type);
@ -330,7 +335,10 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
records: structuredClone([record]), records: structuredClone([record]),
updatedFields: Object.keys(formattedPartialRecordToUpdate), updatedFields: Object.keys(formattedPartialRecordToUpdate),
authContext, authContext,
objectMetadataItem: objectMetadataItemWithFieldMaps, objectMetadataItem:
getObjectMetadataFromObjectMetadataItemWithFieldMaps(
objectMetadataItemWithFieldMaps,
),
}); });
} }
} }
@ -373,7 +381,9 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
this.apiEventEmitterService.emitCreateEvents({ this.apiEventEmitterService.emitCreateEvents({
records: structuredClone(formattedInsertedRecords), records: structuredClone(formattedInsertedRecords),
authContext, authContext,
objectMetadataItem: objectMetadataItemWithFieldMaps, objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps(
objectMetadataItemWithFieldMaps,
),
}); });
} }
@ -450,11 +460,19 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
) { ) {
let recordWithoutCreatedByUpdate = record; let recordWithoutCreatedByUpdate = record;
if ( const createdByFieldMetadataId =
'createdBy' in record && objectMetadataItemWithFieldMaps.fieldIdByName['createdBy'];
objectMetadataItemWithFieldMaps.fieldsByName['createdBy']?.isCustom === const createdByFieldMetadata =
false 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; const { createdBy: _createdBy, ...recordWithoutCreatedBy } = record;
recordWithoutCreatedByUpdate = recordWithoutCreatedBy; recordWithoutCreatedByUpdate = recordWithoutCreatedBy;

View File

@ -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 { 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 { 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 { 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 { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
@Injectable() @Injectable()
@ -56,7 +57,9 @@ export class GraphqlQueryCreateOneResolverService extends GraphqlQueryBaseResolv
this.apiEventEmitterService.emitCreateEvents({ this.apiEventEmitterService.emitCreateEvents({
records: structuredClone(upsertedRecords), records: structuredClone(upsertedRecords),
authContext, authContext,
objectMetadataItem: objectMetadataItemWithFieldMaps, objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps(
objectMetadataItemWithFieldMaps,
),
}); });
if (executionArgs.graphqlQuerySelectedFieldsResult.relations) { if (executionArgs.graphqlQuerySelectedFieldsResult.relations) {

View File

@ -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 { 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 { 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 { 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 { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
import { computeTableName } from 'src/engine/utils/compute-table-name.util'; import { computeTableName } from 'src/engine/utils/compute-table-name.util';
@ -58,7 +59,9 @@ export class GraphqlQueryDeleteManyResolverService extends GraphqlQueryBaseResol
this.apiEventEmitterService.emitDeletedEvents({ this.apiEventEmitterService.emitDeletedEvents({
records: structuredClone(formattedDeletedRecords), records: structuredClone(formattedDeletedRecords),
authContext, authContext,
objectMetadataItem: objectMetadataItemWithFieldMaps, objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps(
objectMetadataItemWithFieldMaps,
),
}); });
if (executionArgs.graphqlQuerySelectedFieldsResult.relations) { if (executionArgs.graphqlQuerySelectedFieldsResult.relations) {

View File

@ -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 { 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 { 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 { 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() @Injectable()
export class GraphqlQueryDeleteOneResolverService extends GraphqlQueryBaseResolverService< export class GraphqlQueryDeleteOneResolverService extends GraphqlQueryBaseResolverService<
@ -60,7 +61,9 @@ export class GraphqlQueryDeleteOneResolverService extends GraphqlQueryBaseResolv
this.apiEventEmitterService.emitDeletedEvents({ this.apiEventEmitterService.emitDeletedEvents({
records: structuredClone(formattedDeletedRecords), records: structuredClone(formattedDeletedRecords),
authContext, authContext,
objectMetadataItem: objectMetadataItemWithFieldMaps, objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps(
objectMetadataItemWithFieldMaps,
),
}); });
if (executionArgs.graphqlQuerySelectedFieldsResult.relations) { if (executionArgs.graphqlQuerySelectedFieldsResult.relations) {

View File

@ -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 { 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 { 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 { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
import { computeTableName } from 'src/engine/utils/compute-table-name.util'; import { computeTableName } from 'src/engine/utils/compute-table-name.util';
@ -56,7 +57,9 @@ export class GraphqlQueryDestroyManyResolverService extends GraphqlQueryBaseReso
this.apiEventEmitterService.emitDestroyEvents({ this.apiEventEmitterService.emitDestroyEvents({
records: structuredClone(deletedRecords), records: structuredClone(deletedRecords),
authContext, authContext,
objectMetadataItem: objectMetadataItemWithFieldMaps, objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps(
objectMetadataItemWithFieldMaps,
),
}); });
if (executionArgs.graphqlQuerySelectedFieldsResult.relations) { if (executionArgs.graphqlQuerySelectedFieldsResult.relations) {

View File

@ -15,6 +15,7 @@ import {
GraphqlQueryRunnerExceptionCode, GraphqlQueryRunnerExceptionCode,
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; } 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 { 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 { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
@Injectable() @Injectable()
@ -56,7 +57,9 @@ export class GraphqlQueryDestroyOneResolverService extends GraphqlQueryBaseResol
this.apiEventEmitterService.emitDestroyEvents({ this.apiEventEmitterService.emitDestroyEvents({
records: structuredClone(deletedRecords), records: structuredClone(deletedRecords),
authContext, authContext,
objectMetadataItem: objectMetadataItemWithFieldMaps, objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps(
objectMetadataItemWithFieldMaps,
),
}); });
if (executionArgs.graphqlQuerySelectedFieldsResult.relations) { if (executionArgs.graphqlQuerySelectedFieldsResult.relations) {

View File

@ -21,10 +21,10 @@ import {
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; } 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 { 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 { 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 { 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 { formatData } from 'src/engine/twenty-orm/utils/format-data.util';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
import { buildDuplicateConditions } from 'src/engine/api/utils/build-duplicate-conditions.utils';
@Injectable() @Injectable()
export class GraphqlQueryFindDuplicatesResolverService extends GraphqlQueryBaseResolverService< export class GraphqlQueryFindDuplicatesResolverService extends GraphqlQueryBaseResolverService<
@ -56,8 +56,7 @@ export class GraphqlQueryFindDuplicatesResolverService extends GraphqlQueryBaseR
} }
const graphqlQueryParser = new GraphqlQueryParser( const graphqlQueryParser = new GraphqlQueryParser(
objectMetadataItemWithFieldsMaps?.fieldsByName, objectMetadataItemWithFieldMaps,
objectMetadataItemWithFieldsMaps?.fieldsByJoinColumnName,
objectMetadataMaps, objectMetadataMaps,
); );

View File

@ -80,7 +80,7 @@ export class GraphqlQueryFindManyResolverService extends GraphqlQueryBaseResolve
const cursorArgFilter = computeCursorArgFilter( const cursorArgFilter = computeCursorArgFilter(
cursor, cursor,
orderByWithIdCondition, orderByWithIdCondition,
objectMetadataItemWithFieldMaps.fieldsByName, objectMetadataItemWithFieldMaps,
isForwardPagination, isForwardPagination,
); );

View File

@ -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 { 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 { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
import { computeTableName } from 'src/engine/utils/compute-table-name.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() @Injectable()
export class GraphqlQueryRestoreManyResolverService extends GraphqlQueryBaseResolverService< export class GraphqlQueryRestoreManyResolverService extends GraphqlQueryBaseResolverService<
@ -58,7 +59,9 @@ export class GraphqlQueryRestoreManyResolverService extends GraphqlQueryBaseReso
this.apiEventEmitterService.emitRestoreEvents({ this.apiEventEmitterService.emitRestoreEvents({
records: structuredClone(formattedRestoredRecords), records: structuredClone(formattedRestoredRecords),
authContext, authContext,
objectMetadataItem: objectMetadataItemWithFieldMaps, objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps(
objectMetadataItemWithFieldMaps,
),
}); });
if (executionArgs.graphqlQuerySelectedFieldsResult.relations) { if (executionArgs.graphqlQuerySelectedFieldsResult.relations) {

View File

@ -17,6 +17,7 @@ import {
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; 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 { 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 { 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 { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
@Injectable() @Injectable()
@ -60,7 +61,9 @@ export class GraphqlQueryRestoreOneResolverService extends GraphqlQueryBaseResol
this.apiEventEmitterService.emitRestoreEvents({ this.apiEventEmitterService.emitRestoreEvents({
records: structuredClone(formattedRestoredRecords), records: structuredClone(formattedRestoredRecords),
authContext, authContext,
objectMetadataItem: objectMetadataItemWithFieldMaps, objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps(
objectMetadataItemWithFieldMaps,
),
}); });
if (executionArgs.graphqlQuerySelectedFieldsResult.relations) { if (executionArgs.graphqlQuerySelectedFieldsResult.relations) {

View File

@ -21,6 +21,7 @@ import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/obj
import { formatData } from 'src/engine/twenty-orm/utils/format-data.util'; import { formatData } from 'src/engine/twenty-orm/utils/format-data.util';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
import { computeTableName } from 'src/engine/utils/compute-table-name.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() @Injectable()
export class GraphqlQueryUpdateManyResolverService extends GraphqlQueryBaseResolverService< export class GraphqlQueryUpdateManyResolverService extends GraphqlQueryBaseResolverService<
@ -94,7 +95,9 @@ export class GraphqlQueryUpdateManyResolverService extends GraphqlQueryBaseResol
records: structuredClone(formattedUpdatedRecords), records: structuredClone(formattedUpdatedRecords),
updatedFields: Object.keys(executionArgs.args.data), updatedFields: Object.keys(executionArgs.args.data),
authContext, authContext,
objectMetadataItem: objectMetadataItemWithFieldMaps, objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps(
objectMetadataItemWithFieldMaps,
),
}); });
if (executionArgs.graphqlQuerySelectedFieldsResult.relations) { if (executionArgs.graphqlQuerySelectedFieldsResult.relations) {

View File

@ -18,6 +18,7 @@ import {
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; 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 { 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 { 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 { formatData } from 'src/engine/twenty-orm/utils/format-data.util';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
@ -89,7 +90,9 @@ export class GraphqlQueryUpdateOneResolverService extends GraphqlQueryBaseResolv
records: structuredClone(formattedUpdatedRecords), records: structuredClone(formattedUpdatedRecords),
updatedFields: Object.keys(executionArgs.args.data), updatedFields: Object.keys(executionArgs.args.data),
authContext, authContext,
objectMetadataItem: objectMetadataItemWithFieldMaps, objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps(
objectMetadataItemWithFieldMaps,
),
}); });
if (executionArgs.graphqlQuerySelectedFieldsResult.relations) { if (executionArgs.graphqlQuerySelectedFieldsResult.relations) {

View File

@ -23,40 +23,29 @@ describe('QueryRunnerArgsFactory', () => {
objectMetadataItemWithFieldMaps: { objectMetadataItemWithFieldMaps: {
isCustom: true, isCustom: true,
nameSingular: 'testNumber', nameSingular: 'testNumber',
fields: [ fieldsById: {
{ 'position-id': {
type: FieldMetadataType.POSITION, type: FieldMetadataType.POSITION,
isCustom: true, isCustom: true,
name: 'position', name: 'position',
}, },
{ 'testNumber-id': {
type: FieldMetadataType.NUMBER, type: FieldMetadataType.NUMBER,
isCustom: true, isCustom: true,
name: 'testNumber', name: 'testNumber',
}, },
{ 'otherField-id': {
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: {
type: FieldMetadataType.TEXT, type: FieldMetadataType.TEXT,
isCustom: true, isCustom: true,
name: 'otherField', name: 'otherField',
}, },
} as unknown as FieldMetadataMap, } as unknown as FieldMetadataMap,
fieldIdByName: {
position: 'position-id',
testNumber: 'testNumber-id',
otherField: 'otherField-id',
},
fieldIdByJoinColumnName: {},
}, },
} as unknown as WorkspaceQueryRunnerOptions; } as unknown as WorkspaceQueryRunnerOptions;

View File

@ -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 { 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 { 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 { 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 { 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'; 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 { FileService } from 'src/engine/core-modules/file/services/file.service';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { isFieldMetadataInterfaceOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; 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 // 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 // And this factory that is also executing logic on object relations
@ -126,7 +126,9 @@ export class QueryResultGettersFactory {
const relationFields = Object.keys(record) const relationFields = Object.keys(record)
.map( .map(
(recordFieldName) => (recordFieldName) =>
objectMetadataMapItem.fieldsByName[recordFieldName], objectMetadataMapItem.fieldsById[
objectMetadataMapItem.fieldIdByName[recordFieldName]
],
) )
.filter(isDefined) .filter(isDefined)
.filter((fieldMetadata) => .filter((fieldMetadata) =>
@ -214,7 +216,7 @@ export class QueryResultGettersFactory {
async create( async create(
result: QueryResultFieldValue, result: QueryResultFieldValue,
objectMetadataItem: ObjectMetadataInterface, objectMetadataItem: ObjectMetadataItemWithFieldMaps,
workspaceId: string, workspaceId: string,
objectMetadataMaps: ObjectMetadataMaps, objectMetadataMaps: ObjectMetadataMaps,
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any

View File

@ -19,12 +19,12 @@ import {
UpdateManyResolverArgs, UpdateManyResolverArgs,
UpdateOneResolverArgs, UpdateOneResolverArgs,
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; } 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 { 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 { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service';
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate'; import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; 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 = { type ArgPositionBackfillInput = {
argIndex?: number; argIndex?: number;
@ -44,14 +44,14 @@ export class QueryRunnerArgsFactory {
resolverArgsType: ResolverArgsType, resolverArgsType: ResolverArgsType,
) { ) {
const fieldMetadataMapByNameByName = const fieldMetadataMapByNameByName =
options.objectMetadataItemWithFieldMaps.fieldsByName; options.objectMetadataItemWithFieldMaps.fieldsById;
const shouldBackfillPosition = const shouldBackfillPosition = Object.values(
options.objectMetadataItemWithFieldMaps.fields.some( options.objectMetadataItemWithFieldMaps.fieldsById,
(field) => ).some(
field.type === FieldMetadataType.POSITION && (field) =>
field.name === 'position', field.type === FieldMetadataType.POSITION && field.name === 'position',
); );
switch (resolverArgsType) { switch (resolverArgsType) {
case ResolverArgsType.CreateOne: case ResolverArgsType.CreateOne:
@ -60,7 +60,6 @@ export class QueryRunnerArgsFactory {
data: await this.overrideDataByFieldMetadata( data: await this.overrideDataByFieldMetadata(
(args as CreateOneResolverArgs).data, (args as CreateOneResolverArgs).data,
options, options,
fieldMetadataMapByNameByName,
{ {
argIndex: 0, argIndex: 0,
shouldBackfillPosition, shouldBackfillPosition,
@ -72,15 +71,10 @@ export class QueryRunnerArgsFactory {
...args, ...args,
data: await Promise.all( data: await Promise.all(
(args as CreateManyResolverArgs).data?.map((arg, index) => (args as CreateManyResolverArgs).data?.map((arg, index) =>
this.overrideDataByFieldMetadata( this.overrideDataByFieldMetadata(arg, options, {
arg, argIndex: index,
options, shouldBackfillPosition,
fieldMetadataMapByNameByName, }),
{
argIndex: index,
shouldBackfillPosition,
},
),
) ?? [], ) ?? [],
), ),
} satisfies CreateManyResolverArgs; } satisfies CreateManyResolverArgs;
@ -91,7 +85,6 @@ export class QueryRunnerArgsFactory {
data: await this.overrideDataByFieldMetadata( data: await this.overrideDataByFieldMetadata(
(args as UpdateOneResolverArgs).data, (args as UpdateOneResolverArgs).data,
options, options,
fieldMetadataMapByNameByName,
{ {
argIndex: 0, argIndex: 0,
shouldBackfillPosition: false, shouldBackfillPosition: false,
@ -103,12 +96,11 @@ export class QueryRunnerArgsFactory {
...args, ...args,
filter: this.overrideFilterByFieldMetadata( filter: this.overrideFilterByFieldMetadata(
(args as UpdateManyResolverArgs).filter, (args as UpdateManyResolverArgs).filter,
fieldMetadataMapByNameByName, options.objectMetadataItemWithFieldMaps,
), ),
data: await this.overrideDataByFieldMetadata( data: await this.overrideDataByFieldMetadata(
(args as UpdateManyResolverArgs).data, (args as UpdateManyResolverArgs).data,
options, options,
fieldMetadataMapByNameByName,
{ {
argIndex: 0, argIndex: 0,
shouldBackfillPosition: false, shouldBackfillPosition: false,
@ -120,7 +112,7 @@ export class QueryRunnerArgsFactory {
...args, ...args,
filter: this.overrideFilterByFieldMetadata( filter: this.overrideFilterByFieldMetadata(
(args as FindOneResolverArgs).filter, (args as FindOneResolverArgs).filter,
fieldMetadataMapByNameByName, options.objectMetadataItemWithFieldMaps,
), ),
}; };
case ResolverArgsType.FindMany: case ResolverArgsType.FindMany:
@ -128,7 +120,7 @@ export class QueryRunnerArgsFactory {
...args, ...args,
filter: this.overrideFilterByFieldMetadata( filter: this.overrideFilterByFieldMetadata(
(args as FindManyResolverArgs).filter, (args as FindManyResolverArgs).filter,
fieldMetadataMapByNameByName, options.objectMetadataItemWithFieldMaps,
), ),
}; };
@ -146,15 +138,10 @@ export class QueryRunnerArgsFactory {
)) as string[], )) as string[],
data: await Promise.all( data: await Promise.all(
(args as FindDuplicatesResolverArgs).data?.map((arg, index) => (args as FindDuplicatesResolverArgs).data?.map((arg, index) =>
this.overrideDataByFieldMetadata( this.overrideDataByFieldMetadata(arg, options, {
arg, argIndex: index,
options, shouldBackfillPosition,
fieldMetadataMapByNameByName, }),
{
argIndex: index,
shouldBackfillPosition,
},
),
) ?? [], ) ?? [],
), ),
} satisfies FindDuplicatesResolverArgs; } satisfies FindDuplicatesResolverArgs;
@ -166,7 +153,6 @@ export class QueryRunnerArgsFactory {
private async overrideDataByFieldMetadata( private async overrideDataByFieldMetadata(
data: Partial<ObjectRecord> | undefined, data: Partial<ObjectRecord> | undefined,
options: WorkspaceQueryRunnerOptions, options: WorkspaceQueryRunnerOptions,
fieldMetadataMapByNameByName: Record<string, FieldMetadataInterface>,
argPositionBackfillInput: ArgPositionBackfillInput, argPositionBackfillInput: ArgPositionBackfillInput,
): Promise<Partial<ObjectRecord>> { ): Promise<Partial<ObjectRecord>> {
if (!isDefined(data)) { if (!isDefined(data)) {
@ -184,7 +170,10 @@ export class QueryRunnerArgsFactory {
data, data,
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
).map(async ([key, value]): Promise<[string, 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) { if (!fieldMetadata) {
return [key, value]; return [key, value];
@ -257,7 +246,7 @@ export class QueryRunnerArgsFactory {
private overrideFilterByFieldMetadata( private overrideFilterByFieldMetadata(
filter: ObjectRecordFilter | undefined, filter: ObjectRecordFilter | undefined,
fieldMetadataMapByName: Record<string, FieldMetadataInterface>, objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
) { ) {
if (!filter) { if (!filter) {
return; return;
@ -278,7 +267,7 @@ export class QueryRunnerArgsFactory {
acc[key] = this.transformFilterValueByType( acc[key] = this.transformFilterValueByType(
key, key,
value, value,
fieldMetadataMapByName, objectMetadataItemWithFieldMaps,
); );
} }
@ -293,9 +282,11 @@ export class QueryRunnerArgsFactory {
key: string, key: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any, value: any,
fieldMetadataMapByName: FieldMetadataMap, objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
) { ) {
const fieldMetadata = fieldMetadataMapByName[key]; const fieldMetadataId = objectMetadataItemWithFieldMaps.fieldIdByName[key];
const fieldMetadata =
objectMetadataItemWithFieldMaps.fieldsById[fieldMetadataId];
if (!fieldMetadata) { if (!fieldMetadata) {
return value; return value;

View File

@ -28,6 +28,7 @@ export const graphqlQueryRunnerExceptionHandler = (
case GraphqlQueryRunnerExceptionCode.RELATION_SETTINGS_NOT_FOUND: case GraphqlQueryRunnerExceptionCode.RELATION_SETTINGS_NOT_FOUND:
case GraphqlQueryRunnerExceptionCode.RELATION_TARGET_OBJECT_METADATA_NOT_FOUND: case GraphqlQueryRunnerExceptionCode.RELATION_TARGET_OBJECT_METADATA_NOT_FOUND:
case GraphqlQueryRunnerExceptionCode.INVALID_POST_HOOK_PAYLOAD: case GraphqlQueryRunnerExceptionCode.INVALID_POST_HOOK_PAYLOAD:
case GraphqlQueryRunnerExceptionCode.MISSING_SYSTEM_FIELD:
throw error; throw error;
default: { default: {
const _exhaustiveCheck: never = error.code; const _exhaustiveCheck: never = error.code;

View File

@ -1,5 +1,5 @@
import { QueryFailedError } from 'typeorm';
import { isDefined } from 'twenty-shared/utils'; 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'; import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
@ -14,14 +14,14 @@ export const handleDuplicateKeyError = (
if (indexNameMatch) { if (indexNameMatch) {
const indexName = indexNameMatch[1]; const indexName = indexNameMatch[1];
const deletedAtFieldMetadata = const deletedAtFieldMetadataId =
context.objectMetadataItemWithFieldMaps.fieldsByName['deletedAt']; context.objectMetadataItemWithFieldMaps.fieldIdByName['deletedAt'];
const affectedColumns = const affectedColumns =
context.objectMetadataItemWithFieldMaps.indexMetadatas context.objectMetadataItemWithFieldMaps.indexMetadatas
.find((index) => index.name === indexName) .find((index) => index.name === indexName)
?.indexFieldMetadatas?.filter( ?.indexFieldMetadatas?.filter(
(field) => field.fieldMetadataId !== deletedAtFieldMetadata?.id, (field) => field.fieldMetadataId !== deletedAtFieldMetadataId,
) )
.map((indexField) => { .map((indexField) => {
const fieldMetadata = const fieldMetadata =

View File

@ -12,7 +12,7 @@ export class WorkspaceResolverBuilderService {
constructor() {} constructor() {}
shouldBuildResolver( shouldBuildResolver(
objectMetadata: ObjectMetadataInterface, objectMetadata: Pick<ObjectMetadataInterface, 'duplicateCriteria'>,
methodName: WorkspaceResolverBuilderMethodNames, methodName: WorkspaceResolverBuilderMethodNames,
) { ) {
switch (methodName) { switch (methodName) {

View File

@ -3,26 +3,18 @@ import { Injectable } from '@nestjs/common';
import { makeExecutableSchema } from '@graphql-tools/schema'; import { makeExecutableSchema } from '@graphql-tools/schema';
import { GraphQLSchema, printSchema } from 'graphql'; import { GraphQLSchema, printSchema } from 'graphql';
import { gql } from 'graphql-tag'; import { gql } from 'graphql-tag';
import { isDefined } from 'twenty-shared/utils';
import { ScalarsExplorerService } from 'src/engine/api/graphql/services/scalars-explorer.service'; import { ScalarsExplorerService } from 'src/engine/api/graphql/services/scalars-explorer.service';
import { workspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/factories/factories'; 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 { 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 { 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 { 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 { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { import {
WorkspaceMetadataCacheException, WorkspaceMetadataCacheException,
WorkspaceMetadataCacheExceptionCode, WorkspaceMetadataCacheExceptionCode,
} from 'src/engine/metadata-modules/workspace-metadata-cache/exceptions/workspace-metadata-cache.exception'; } 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 { 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'; import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
@Injectable() @Injectable()
@ -34,8 +26,6 @@ export class WorkspaceSchemaFactory {
private readonly workspaceResolverFactory: WorkspaceResolverFactory, private readonly workspaceResolverFactory: WorkspaceResolverFactory,
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService, private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService, private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService,
private readonly featureFlagService: FeatureFlagService,
private readonly twentyConfigService: TwentyConfigService,
) {} ) {}
async createGraphQLSchema(authContext: AuthContext): Promise<GraphQLSchema> { async createGraphQLSchema(authContext: AuthContext): Promise<GraphQLSchema> {
@ -52,38 +42,12 @@ export class WorkspaceSchemaFactory {
return new GraphQLSchema({}); return new GraphQLSchema({});
} }
let currentCacheVersion = const { objectMetadataMaps, metadataVersion } =
await this.workspaceCacheStorageService.getMetadataVersion( await this.workspaceMetadataCacheService.getExistingOrRecomputeMetadataMaps(
authContext.workspace.id, {
);
let objectMetadataMaps: ObjectMetadataMaps | undefined;
if (currentCacheVersion === undefined) {
const recomputed =
await this.workspaceMetadataCacheService.recomputeMetadataCache({
workspaceId: authContext.workspace.id, 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) { if (!objectMetadataMaps) {
throw new WorkspaceMetadataCacheException( 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( const objectMetadataCollection = Object.values(objectMetadataMaps.byId).map(
(objectMetadataItem) => ({ (objectMetadataItem) => ({
...objectMetadataItem, ...objectMetadataItem,
fields: objectMetadataItem.fields, fields: Object.values(objectMetadataItem.fieldsById),
indexes: objectMetadataItem.indexMetadatas, indexes: objectMetadataItem.indexMetadatas,
}), }),
); );
@ -110,12 +67,12 @@ export class WorkspaceSchemaFactory {
// Get typeDefs from cache // Get typeDefs from cache
let typeDefs = await this.workspaceCacheStorageService.getGraphQLTypeDefs( let typeDefs = await this.workspaceCacheStorageService.getGraphQLTypeDefs(
authContext.workspace.id, authContext.workspace.id,
currentCacheVersion, metadataVersion,
); );
let usedScalarNames = let usedScalarNames =
await this.workspaceCacheStorageService.getGraphQLUsedScalarNames( await this.workspaceCacheStorageService.getGraphQLUsedScalarNames(
authContext.workspace.id, authContext.workspace.id,
currentCacheVersion, metadataVersion,
); );
// If typeDefs are not cached, generate them // If typeDefs are not cached, generate them
@ -133,12 +90,12 @@ export class WorkspaceSchemaFactory {
await this.workspaceCacheStorageService.setGraphQLTypeDefs( await this.workspaceCacheStorageService.setGraphQLTypeDefs(
authContext.workspace.id, authContext.workspace.id,
currentCacheVersion, metadataVersion,
typeDefs, typeDefs,
); );
await this.workspaceCacheStorageService.setGraphQLUsedScalarNames( await this.workspaceCacheStorageService.setGraphQLUsedScalarNames(
authContext.workspace.id, authContext.workspace.id,
currentCacheVersion, metadataVersion,
usedScalarNames, usedScalarNames,
); );
} }

View File

@ -5,6 +5,8 @@ import { isDefined } from 'twenty-shared/utils';
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler'; 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() @Injectable()
export class RestApiCreateManyHandler extends RestApiBaseHandler { export class RestApiCreateManyHandler extends RestApiBaseHandler {
async handle(request: Request) { async handle(request: Request) {
@ -57,7 +59,9 @@ export class RestApiCreateManyHandler extends RestApiBaseHandler {
this.apiEventEmitterService.emitCreateEvents({ this.apiEventEmitterService.emitCreateEvents({
records: createdRecords, records: createdRecords,
authContext: this.getAuthContextFromRequest(request), authContext: this.getAuthContextFromRequest(request),
objectMetadataItem: objectMetadata.objectMetadataMapItem, objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps(
objectMetadata.objectMetadataMapItem,
),
}); });
const records = await this.getRecord({ const records = await this.getRecord({

View File

@ -5,6 +5,8 @@ import { isDefined } from 'twenty-shared/utils';
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler'; 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() @Injectable()
export class RestApiCreateOneHandler extends RestApiBaseHandler { export class RestApiCreateOneHandler extends RestApiBaseHandler {
async handle(request: Request) { async handle(request: Request) {
@ -40,7 +42,9 @@ export class RestApiCreateOneHandler extends RestApiBaseHandler {
this.apiEventEmitterService.emitCreateEvents({ this.apiEventEmitterService.emitCreateEvents({
records: [createdRecord], records: [createdRecord],
authContext: this.getAuthContextFromRequest(request), authContext: this.getAuthContextFromRequest(request),
objectMetadataItem: objectMetadata.objectMetadataMapItem, objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps(
objectMetadata.objectMetadataMapItem,
),
}); });
const records = await this.getRecord({ const records = await this.getRecord({

View File

@ -5,6 +5,7 @@ import { Request } from 'express';
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler'; 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 { 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() @Injectable()
export class RestApiDeleteOneHandler extends RestApiBaseHandler { export class RestApiDeleteOneHandler extends RestApiBaseHandler {
@ -26,7 +27,9 @@ export class RestApiDeleteOneHandler extends RestApiBaseHandler {
this.apiEventEmitterService.emitDestroyEvents({ this.apiEventEmitterService.emitDestroyEvents({
records: [recordToDelete], records: [recordToDelete],
authContext: this.getAuthContextFromRequest(request), authContext: this.getAuthContextFromRequest(request),
objectMetadataItem: objectMetadata.objectMetadataMapItem, objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps(
objectMetadata.objectMetadataMapItem,
),
}); });
return this.formatResult({ return this.formatResult({

View File

@ -4,14 +4,14 @@ import { Request } from 'express';
import isEmpty from 'lodash.isempty'; import isEmpty from 'lodash.isempty';
import { In } from 'typeorm'; import { In } from 'typeorm';
import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { import {
FormatResult, FormatResult,
RestApiBaseHandler, RestApiBaseHandler,
} from 'src/engine/api/rest/core/interfaces/rest-api-base.handler'; } 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 { buildDuplicateConditions } from 'src/engine/api/utils/build-duplicate-conditions.utils';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
@Injectable() @Injectable()
export class RestApiFindDuplicatesHandler extends RestApiBaseHandler { export class RestApiFindDuplicatesHandler extends RestApiBaseHandler {

View File

@ -6,6 +6,7 @@ import { isDefined } from 'twenty-shared/utils';
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler'; 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 { 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() @Injectable()
export class RestApiUpdateOneHandler extends RestApiBaseHandler { export class RestApiUpdateOneHandler extends RestApiBaseHandler {
@ -38,7 +39,9 @@ export class RestApiUpdateOneHandler extends RestApiBaseHandler {
records: [updatedRecord], records: [updatedRecord],
updatedFields: Object.keys(request.body), updatedFields: Object.keys(request.body),
authContext: this.getAuthContextFromRequest(request), authContext: this.getAuthContextFromRequest(request),
objectMetadataItem: objectMetadata.objectMetadataMapItem, objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps(
objectMetadata.objectMetadataMapItem,
),
}); });
const records = await this.getRecord({ const records = await this.getRecord({

View File

@ -160,32 +160,34 @@ export abstract class RestApiBaseHandler {
const relations: string[] = []; const relations: string[] = [];
objectMetadata.objectMetadataMapItem.fields.forEach((field) => { Object.values(objectMetadata.objectMetadataMapItem.fieldsById).forEach(
if (field.type === FieldMetadataType.RELATION) { (field) => {
if ( if (field.type === FieldMetadataType.RELATION) {
depth === MAX_DEPTH && if (
isDefined(field.relationTargetObjectMetadataId) depth === MAX_DEPTH &&
) { isDefined(field.relationTargetObjectMetadataId)
const relationTargetObjectMetadata = ) {
objectMetadata.objectMetadataMaps.byId[ const relationTargetObjectMetadata =
field.relationTargetObjectMetadataId objectMetadata.objectMetadataMaps.byId[
]; field.relationTargetObjectMetadataId
const depth2Relations = this.getRelations({ ];
objectMetadata: { const depth2Relations = this.getRelations({
objectMetadataMaps: objectMetadata.objectMetadataMaps, objectMetadata: {
objectMetadataMapItem: relationTargetObjectMetadata, objectMetadataMaps: objectMetadata.objectMetadataMaps,
}, objectMetadataMapItem: relationTargetObjectMetadata,
depth: 1, },
}); depth: 1,
});
depth2Relations.forEach((depth2Relation) => { depth2Relations.forEach((depth2Relation) => {
relations.push(`${field.name}.${depth2Relation}`); relations.push(`${field.name}.${depth2Relation}`);
}); });
} else { } else {
relations.push(`${field.name}`); relations.push(`${field.name}`);
}
} }
} },
}); );
return relations; return relations;
} }
@ -305,9 +307,7 @@ export abstract class RestApiBaseHandler {
objectMetadataMaps: ObjectMetadataMaps; objectMetadataMaps: ObjectMetadataMaps;
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps; objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
}; };
objectMetadataItemWithFieldsMaps: objectMetadataItemWithFieldsMaps: ObjectMetadataItemWithFieldMaps;
| ObjectMetadataItemWithFieldMaps
| undefined;
extraFilters?: Partial<ObjectRecordFilter>; extraFilters?: Partial<ObjectRecordFilter>;
}) { }) {
const objectMetadataNameSingular = const objectMetadataNameSingular =
@ -321,17 +321,10 @@ export abstract class RestApiBaseHandler {
objectMetadata, objectMetadata,
); );
const fieldMetadataMapByName =
objectMetadataItemWithFieldsMaps?.fieldsByName || {};
const fieldMetadataMapByJoinColumnName =
objectMetadataItemWithFieldsMaps?.fieldsByJoinColumnName || {};
const isForwardPagination = !inputs.endingBefore; const isForwardPagination = !inputs.endingBefore;
const graphqlQueryParser = new GraphqlQueryParser( const graphqlQueryParser = new GraphqlQueryParser(
fieldMetadataMapByName, objectMetadataItemWithFieldsMaps,
fieldMetadataMapByJoinColumnName,
objectMetadata.objectMetadataMaps, objectMetadata.objectMetadataMaps,
); );
@ -442,7 +435,7 @@ export abstract class RestApiBaseHandler {
const cursorArgFilter = computeCursorArgFilter( const cursorArgFilter = computeCursorArgFilter(
this.parseCursor(cursor), this.parseCursor(cursor),
inputs.orderBy || [], inputs.orderBy || [],
objectMetadata.objectMetadataMapItem.fieldsByName, objectMetadata.objectMetadataMapItem,
isForwardPagination, isForwardPagination,
); );

View File

@ -26,7 +26,7 @@ export class CreateManyQueryFactory {
mutation Create${objectNamePlural}($data: [${objectNameSingular}CreateInput!]) { mutation Create${objectNamePlural}($data: [${objectNameSingular}CreateInput!]) {
create${objectNamePlural}(data: $data) { create${objectNamePlural}(data: $data) {
id id
${objectMetadata.objectMetadataMapItem.fields ${Object.values(objectMetadata.objectMetadataMapItem.fieldsById)
.map((field) => .map((field) =>
mapFieldMetadataToGraphqlQuery( mapFieldMetadataToGraphqlQuery(
objectMetadata.objectMetadataMaps, objectMetadata.objectMetadataMaps,

View File

@ -31,7 +31,7 @@ export class FindDuplicatesQueryFactory {
} }
edges{ edges{
node { node {
${objectMetadata.objectMetadataMapItem.fields ${Object.values(objectMetadata.objectMetadataMapItem.fieldsById)
.map((field) => .map((field) =>
mapFieldMetadataToGraphqlQuery( mapFieldMetadataToGraphqlQuery(
objectMetadata.objectMetadataMaps, objectMetadata.objectMetadataMaps,

View File

@ -17,21 +17,23 @@ describe('checkFields', () => {
objectMetadataId: 'object-metadata-id', objectMetadataId: 'object-metadata-id',
isNullable: fieldNumberMock.isNullable, isNullable: fieldNumberMock.isNullable,
defaultValue: fieldNumberMock.defaultValue, defaultValue: fieldNumberMock.defaultValue,
isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
}; };
const fieldsById: FieldMetadataMap = { const fieldsById: FieldMetadataMap = {
'field-number-id': completeFieldNumberMock, 'field-number-id': completeFieldNumberMock,
}; };
const fieldsByName: FieldMetadataMap = {
[completeFieldNumberMock.name]: completeFieldNumberMock,
};
const mockObjectMetadataWithFieldMaps = { const mockObjectMetadataWithFieldMaps = {
...objectMetadataItemMock, ...objectMetadataItemMock,
fieldsById, fieldsById,
fieldsByName, fieldIdByName: {
fieldsByJoinColumnName: {}, [completeFieldNumberMock.name]: completeFieldNumberMock.id,
},
fieldIdByJoinColumnName: {},
indexMetadatas: [],
}; };
it('should check field types', () => { it('should check field types', () => {

View File

@ -18,21 +18,23 @@ describe('getFieldType', () => {
objectMetadataId: 'object-metadata-id', objectMetadataId: 'object-metadata-id',
isNullable: fieldNumberMock.isNullable, isNullable: fieldNumberMock.isNullable,
defaultValue: fieldNumberMock.defaultValue, defaultValue: fieldNumberMock.defaultValue,
isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
}; };
const fieldsById: FieldMetadataMap = { const fieldsById: FieldMetadataMap = {
'field-number-id': completeFieldNumberMock, 'field-number-id': completeFieldNumberMock,
}; };
const fieldsByName: FieldMetadataMap = {
[completeFieldNumberMock.name]: completeFieldNumberMock,
};
const mockObjectMetadataWithFieldMaps = { const mockObjectMetadataWithFieldMaps = {
...objectMetadataItemMock, ...objectMetadataItemMock,
fieldsById, fieldsById,
fieldsByName, fieldIdByName: {
fieldsByJoinColumnName: {}, [completeFieldNumberMock.name]: completeFieldNumberMock.id,
},
fieldIdByJoinColumnName: {},
indexMetadatas: [],
}; };
it('should get field type', () => { it('should get field type', () => {

View File

@ -24,6 +24,9 @@ describe('mapFieldMetadataToGraphqlQuery', () => {
objectMetadataId: 'object-metadata-id', objectMetadataId: 'object-metadata-id',
isNullable: fieldNumberMock.isNullable, isNullable: fieldNumberMock.isNullable,
defaultValue: fieldNumberMock.defaultValue, defaultValue: fieldNumberMock.defaultValue,
isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
}; };
const typedFieldTextMock: FieldMetadataInterface = { const typedFieldTextMock: FieldMetadataInterface = {
@ -34,6 +37,9 @@ describe('mapFieldMetadataToGraphqlQuery', () => {
objectMetadataId: 'object-metadata-id', objectMetadataId: 'object-metadata-id',
isNullable: fieldTextMock.isNullable, isNullable: fieldTextMock.isNullable,
defaultValue: fieldTextMock.defaultValue, defaultValue: fieldTextMock.defaultValue,
isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
}; };
const typedFieldCurrencyMock: FieldMetadataInterface = { const typedFieldCurrencyMock: FieldMetadataInterface = {
@ -44,6 +50,9 @@ describe('mapFieldMetadataToGraphqlQuery', () => {
objectMetadataId: 'object-metadata-id', objectMetadataId: 'object-metadata-id',
isNullable: fieldCurrencyMock.isNullable, isNullable: fieldCurrencyMock.isNullable,
defaultValue: fieldCurrencyMock.defaultValue, defaultValue: fieldCurrencyMock.defaultValue,
isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
}; };
const fieldsById: FieldMetadataMap = { const fieldsById: FieldMetadataMap = {
@ -52,17 +61,16 @@ describe('mapFieldMetadataToGraphqlQuery', () => {
'field-currency-id': typedFieldCurrencyMock, 'field-currency-id': typedFieldCurrencyMock,
}; };
const fieldsByName: FieldMetadataMap = {
[typedFieldNumberMock.name]: typedFieldNumberMock,
[typedFieldTextMock.name]: typedFieldTextMock,
[typedFieldCurrencyMock.name]: typedFieldCurrencyMock,
};
const typedObjectMetadataItem: ObjectMetadataItemWithFieldMaps = { const typedObjectMetadataItem: ObjectMetadataItemWithFieldMaps = {
...objectMetadataItemMock, ...objectMetadataItemMock,
fieldsById, fieldsById,
fieldsByName, fieldIdByName: {
fieldsByJoinColumnName: {}, [typedFieldNumberMock.name]: typedFieldNumberMock.id,
[typedFieldTextMock.name]: typedFieldTextMock.id,
[typedFieldCurrencyMock.name]: typedFieldCurrencyMock.id,
},
fieldIdByJoinColumnName: {},
indexMetadatas: [],
}; };
const objectMetadataMapsMock: ObjectMetadataMaps = { const objectMetadataMapsMock: ObjectMetadataMaps = {
@ -110,6 +118,10 @@ describe('mapFieldMetadataToGraphqlQuery', () => {
name: 'toObjectMetadataName', name: 'toObjectMetadataName',
label: 'Test Field', label: 'Test Field',
objectMetadataId: 'object-metadata-id', objectMetadataId: 'object-metadata-id',
isNullable: true,
isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
}; };
if (fieldMetadataType === FieldMetadataType.RELATION) { if (fieldMetadataType === FieldMetadataType.RELATION) {

View File

@ -9,7 +9,7 @@ export const checkFields = (
objectMetadataItem: ObjectMetadataItemWithFieldMaps, objectMetadataItem: ObjectMetadataItemWithFieldMaps,
fieldNames: string[], fieldNames: string[],
): void => { ): void => {
const fieldMetadataNames = objectMetadataItem.fields const fieldMetadataNames = Object.values(objectMetadataItem.fieldsById)
.map((field) => { .map((field) => {
if (isCompositeFieldMetadataType(field.type)) { if (isCompositeFieldMetadataType(field.type)) {
const compositeType = compositeTypeDefinitions.get(field.type); const compositeType = compositeTypeDefinitions.get(field.type);

View File

@ -11,7 +11,7 @@ export const checkArrayFields = (
objectMetadataItem: ObjectMetadataItemWithFieldMaps, objectMetadataItem: ObjectMetadataItemWithFieldMaps,
fields: Array<Partial<ObjectRecord>>, fields: Array<Partial<ObjectRecord>>,
): void => { ): void => {
const fieldMetadataNames = objectMetadataItem.fields const fieldMetadataNames = Object.values(objectMetadataItem.fieldsById)
.map((field) => { .map((field) => {
if (isCompositeFieldMetadataType(field.type)) { if (isCompositeFieldMetadataType(field.type)) {
const compositeType = compositeTypeDefinitions.get(field.type); const compositeType = compositeTypeDefinitions.get(field.type);

View File

@ -19,21 +19,23 @@ describe('checkFilterEnumValues', () => {
isNullable: fieldSelectMock.isNullable, isNullable: fieldSelectMock.isNullable,
defaultValue: fieldSelectMock.defaultValue, defaultValue: fieldSelectMock.defaultValue,
options: fieldSelectMock.options, options: fieldSelectMock.options,
isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
}; };
const fieldsById: FieldMetadataMap = { const fieldsById: FieldMetadataMap = {
'field-select-id': completeFieldSelectMock, 'field-select-id': completeFieldSelectMock,
}; };
const fieldsByName: FieldMetadataMap = {
[completeFieldSelectMock.name]: completeFieldSelectMock,
};
const mockObjectMetadataWithFieldMaps = { const mockObjectMetadataWithFieldMaps = {
...objectMetadataItemMock, ...objectMetadataItemMock,
fieldsById, fieldsById,
fieldsByName, fieldIdByName: {
fieldsByJoinColumnName: {}, [completeFieldSelectMock.name]: completeFieldSelectMock.id,
},
fieldIdByJoinColumnName: {},
indexMetadatas: [],
}; };
it('should check properly', () => { it('should check properly', () => {

View File

@ -7,6 +7,7 @@ import {
} from 'src/engine/api/__mocks__/object-metadata-item.mock'; } 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 { 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 { 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', () => { describe('parseFilter', () => {
const completeFieldNumberMock: FieldMetadataInterface = { const completeFieldNumberMock: FieldMetadataInterface = {
@ -17,6 +18,9 @@ describe('parseFilter', () => {
objectMetadataId: 'object-metadata-id', objectMetadataId: 'object-metadata-id',
isNullable: fieldNumberMock.isNullable, isNullable: fieldNumberMock.isNullable,
defaultValue: fieldNumberMock.defaultValue, defaultValue: fieldNumberMock.defaultValue,
isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
}; };
const completeFieldTextMock: FieldMetadataInterface = { const completeFieldTextMock: FieldMetadataInterface = {
@ -27,6 +31,9 @@ describe('parseFilter', () => {
objectMetadataId: 'object-metadata-id', objectMetadataId: 'object-metadata-id',
isNullable: fieldTextMock.isNullable, isNullable: fieldTextMock.isNullable,
defaultValue: fieldTextMock.defaultValue, defaultValue: fieldTextMock.defaultValue,
isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
}; };
const fieldsById: FieldMetadataMap = { const fieldsById: FieldMetadataMap = {
@ -34,16 +41,15 @@ describe('parseFilter', () => {
'field-text-id': completeFieldTextMock, 'field-text-id': completeFieldTextMock,
}; };
const fieldsByName: FieldMetadataMap = { const mockObjectMetadataWithFieldMaps: ObjectMetadataItemWithFieldMaps = {
[completeFieldNumberMock.name]: completeFieldNumberMock,
[completeFieldTextMock.name]: completeFieldTextMock,
};
const mockObjectMetadataWithFieldMaps = {
...objectMetadataItemMock, ...objectMetadataItemMock,
fieldsById, fieldsById,
fieldsByName, fieldIdByName: {
fieldsByJoinColumnName: {}, [completeFieldNumberMock.name]: completeFieldNumberMock.id,
[completeFieldTextMock.name]: completeFieldTextMock.id,
},
fieldIdByJoinColumnName: {},
indexMetadatas: [],
}; };
it('should parse string filter test 1', () => { it('should parse string filter test 1', () => {

View File

@ -18,7 +18,8 @@ export const checkFilterEnumValues = (
) { ) {
return; return;
} }
const field = objectMetadataItem.fieldsByName[fieldName]; const fieldMetadataId = objectMetadataItem.fieldIdByName[fieldName];
const field = objectMetadataItem.fieldsById[fieldMetadataId];
const values = /^\[.*\]$/.test(value) const values = /^\[.*\]$/.test(value)
? value.slice(1, -1).split(',') ? value.slice(1, -1).split(',')

View File

@ -6,5 +6,8 @@ export const getFieldType = (
objectMetadataItem: ObjectMetadataItemWithFieldMaps, objectMetadataItem: ObjectMetadataItemWithFieldMaps,
fieldName: string, fieldName: string,
): FieldMetadataType | undefined => { ): FieldMetadataType | undefined => {
return objectMetadataItem.fieldsByName[fieldName]?.type; const fieldMetadataId = objectMetadataItem.fieldIdByName[fieldName];
const field = objectMetadataItem.fieldsById[fieldMetadataId];
return field?.type;
}; };

View File

@ -10,6 +10,7 @@ import {
} from 'src/engine/api/__mocks__/object-metadata-item.mock'; } from 'src/engine/api/__mocks__/object-metadata-item.mock';
import { FilterInputFactory } from 'src/engine/api/rest/input-factories/filter-input.factory'; import { FilterInputFactory } from 'src/engine/api/rest/input-factories/filter-input.factory';
import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; 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', () => { describe('FilterInputFactory', () => {
const completeFieldNumberMock: FieldMetadataInterface = { const completeFieldNumberMock: FieldMetadataInterface = {
@ -20,6 +21,9 @@ describe('FilterInputFactory', () => {
objectMetadataId: 'object-metadata-id', objectMetadataId: 'object-metadata-id',
isNullable: fieldNumberMock.isNullable, isNullable: fieldNumberMock.isNullable,
defaultValue: fieldNumberMock.defaultValue, defaultValue: fieldNumberMock.defaultValue,
isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
}; };
const completeFieldTextMock: FieldMetadataInterface = { const completeFieldTextMock: FieldMetadataInterface = {
@ -30,6 +34,9 @@ describe('FilterInputFactory', () => {
objectMetadataId: 'object-metadata-id', objectMetadataId: 'object-metadata-id',
isNullable: fieldTextMock.isNullable, isNullable: fieldTextMock.isNullable,
defaultValue: fieldTextMock.defaultValue, defaultValue: fieldTextMock.defaultValue,
isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
}; };
const completeFieldCurrencyMock: FieldMetadataInterface = { const completeFieldCurrencyMock: FieldMetadataInterface = {
@ -40,6 +47,9 @@ describe('FilterInputFactory', () => {
objectMetadataId: 'object-metadata-id', objectMetadataId: 'object-metadata-id',
isNullable: fieldCurrencyMock.isNullable, isNullable: fieldCurrencyMock.isNullable,
defaultValue: fieldCurrencyMock.defaultValue, defaultValue: fieldCurrencyMock.defaultValue,
isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
}; };
const fieldsById: FieldMetadataMap = { const fieldsById: FieldMetadataMap = {
@ -48,16 +58,15 @@ describe('FilterInputFactory', () => {
'field-currency-id': completeFieldCurrencyMock, 'field-currency-id': completeFieldCurrencyMock,
}; };
const fieldsByName: FieldMetadataMap = { const objectMetadataMapItem: ObjectMetadataItemWithFieldMaps = {
[completeFieldNumberMock.name]: completeFieldNumberMock,
[completeFieldTextMock.name]: completeFieldTextMock,
[completeFieldCurrencyMock.name]: completeFieldCurrencyMock,
};
const objectMetadataMapItem = {
...objectMetadataMapItemMock, ...objectMetadataMapItemMock,
fieldsById, fieldsById,
fieldsByName, fieldIdByName: {
[completeFieldNumberMock.name]: completeFieldNumberMock.id,
[completeFieldTextMock.name]: completeFieldTextMock.id,
[completeFieldCurrencyMock.name]: completeFieldCurrencyMock.id,
},
fieldIdByJoinColumnName: {},
}; };
const objectMetadataMaps = { const objectMetadataMaps = {

View File

@ -1,11 +1,11 @@
import { mockPersonObjectMetadata } from 'src/engine/api/graphql/graphql-query-runner/__mocks__/mockPersonObjectMetadata'; import { mockPersonObjectMetadataWithFieldMaps } from 'src/engine/api/graphql/graphql-query-runner/__mocks__/mockPersonObjectMetadata';
import { buildDuplicateConditions } from 'src/engine/api/utils/build-duplicate-conditions.utils';
import { mockPersonRecords } from 'src/engine/api/graphql/graphql-query-runner/__mocks__/mockPersonRecords'; 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', () => { describe('buildDuplicateConditions', () => {
it('should build conditions based on duplicate criteria from composite field', () => { it('should build conditions based on duplicate criteria from composite field', () => {
const duplicateConditons = buildDuplicateConditions( const duplicateConditons = buildDuplicateConditions(
mockPersonObjectMetadata([['emailsPrimaryEmail']]), mockPersonObjectMetadataWithFieldMaps([['emailsPrimaryEmail']]),
mockPersonRecords, mockPersonRecords,
'recordId', 'recordId',
); );
@ -13,8 +13,10 @@ describe('buildDuplicateConditions', () => {
expect(duplicateConditons).toEqual({ expect(duplicateConditons).toEqual({
or: [ or: [
{ {
emailsPrimaryEmail: { emails: {
eq: 'test@test.fr', primaryEmail: {
eq: 'test@test.fr',
},
}, },
}, },
], ],
@ -26,7 +28,7 @@ describe('buildDuplicateConditions', () => {
it('should build conditions based on duplicate criteria from basic field', () => { it('should build conditions based on duplicate criteria from basic field', () => {
const duplicateConditons = buildDuplicateConditions( const duplicateConditons = buildDuplicateConditions(
mockPersonObjectMetadata([['jobTitle']]), mockPersonObjectMetadataWithFieldMaps([['jobTitle']]),
mockPersonRecords, mockPersonRecords,
'recordId', 'recordId',
); );
@ -47,7 +49,7 @@ describe('buildDuplicateConditions', () => {
it('should not build conditions based on duplicate criteria if record value is null or too small', () => { it('should not build conditions based on duplicate criteria if record value is null or too small', () => {
const duplicateConditons = buildDuplicateConditions( const duplicateConditons = buildDuplicateConditions(
mockPersonObjectMetadata([['linkedinLinkPrimaryLinkUrl']]), mockPersonObjectMetadataWithFieldMaps([['linkedinLinkPrimaryLinkUrl']]),
mockPersonRecords, mockPersonRecords,
'recordId', 'recordId',
); );
@ -57,7 +59,7 @@ describe('buildDuplicateConditions', () => {
it('should build conditions based on duplicate criteria and without recordId filter', () => { it('should build conditions based on duplicate criteria and without recordId filter', () => {
const duplicateConditons = buildDuplicateConditions( const duplicateConditons = buildDuplicateConditions(
mockPersonObjectMetadata([['jobTitle']]), mockPersonObjectMetadataWithFieldMaps([['jobTitle']]),
mockPersonRecords, mockPersonRecords,
); );

View File

@ -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 { 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 { 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', () => { describe('computeCursorArgFilter', () => {
const mockFieldMetadataMap = { const objectMetadataItemWithFieldMaps = {
name: { id: 'object-id',
type: FieldMetadataType.TEXT, workspaceId: 'workspace-id',
id: 'name-id', nameSingular: 'person',
name: 'name', namePlural: 'people',
label: 'Name', isCustom: false,
objectMetadataId: 'object-id', 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: { fieldsById: {
type: FieldMetadataType.NUMBER, 'name-id': {
id: 'age-id', type: FieldMetadataType.TEXT,
name: 'age', id: 'name-id',
label: 'Age', name: 'name',
objectMetadataId: 'object-id', 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: { } satisfies ObjectMetadataItemWithFieldMaps;
type: FieldMetadataType.FULL_NAME,
id: 'fullname-id',
name: 'fullName',
label: 'Full Name',
objectMetadataId: 'object-id',
},
};
describe('basic cursor filtering', () => { describe('basic cursor filtering', () => {
it('should return empty array when cursor is empty', () => { it('should return empty array when cursor is empty', () => {
const result = computeCursorArgFilter({}, [], mockFieldMetadataMap, true); const result = computeCursorArgFilter(
{},
[],
objectMetadataItemWithFieldMaps,
true,
);
expect(result).toEqual([]); expect(result).toEqual([]);
}); });
@ -44,7 +85,7 @@ describe('computeCursorArgFilter', () => {
const result = computeCursorArgFilter( const result = computeCursorArgFilter(
cursor, cursor,
orderBy, orderBy,
mockFieldMetadataMap, objectMetadataItemWithFieldMaps,
true, true,
); );
@ -58,7 +99,7 @@ describe('computeCursorArgFilter', () => {
const result = computeCursorArgFilter( const result = computeCursorArgFilter(
cursor, cursor,
orderBy, orderBy,
mockFieldMetadataMap, objectMetadataItemWithFieldMaps,
false, false,
); );
@ -77,7 +118,7 @@ describe('computeCursorArgFilter', () => {
const result = computeCursorArgFilter( const result = computeCursorArgFilter(
cursor, cursor,
orderBy, orderBy,
mockFieldMetadataMap, objectMetadataItemWithFieldMaps,
true, true,
); );
@ -105,7 +146,7 @@ describe('computeCursorArgFilter', () => {
const result = computeCursorArgFilter( const result = computeCursorArgFilter(
cursor, cursor,
orderBy, orderBy,
mockFieldMetadataMap, objectMetadataItemWithFieldMaps,
true, true,
); );
@ -151,7 +192,7 @@ describe('computeCursorArgFilter', () => {
const result = computeCursorArgFilter( const result = computeCursorArgFilter(
cursor, cursor,
orderBy, orderBy,
mockFieldMetadataMap, objectMetadataItemWithFieldMaps,
true, true,
); );
@ -180,7 +221,7 @@ describe('computeCursorArgFilter', () => {
const result = computeCursorArgFilter( const result = computeCursorArgFilter(
cursor, cursor,
orderBy, orderBy,
mockFieldMetadataMap, objectMetadataItemWithFieldMaps,
false, false,
); );
@ -218,7 +259,12 @@ describe('computeCursorArgFilter', () => {
const orderBy = [{ invalidField: OrderByDirection.AscNullsLast }]; const orderBy = [{ invalidField: OrderByDirection.AscNullsLast }];
expect(() => expect(() =>
computeCursorArgFilter(cursor, orderBy, mockFieldMetadataMap, true), computeCursorArgFilter(
cursor,
orderBy,
objectMetadataItemWithFieldMaps,
true,
),
).toThrow(GraphqlQueryRunnerException); ).toThrow(GraphqlQueryRunnerException);
}); });
@ -227,7 +273,12 @@ describe('computeCursorArgFilter', () => {
const orderBy = [{ age: OrderByDirection.AscNullsLast }]; const orderBy = [{ age: OrderByDirection.AscNullsLast }];
expect(() => expect(() =>
computeCursorArgFilter(cursor, orderBy, mockFieldMetadataMap, true), computeCursorArgFilter(
cursor,
orderBy,
objectMetadataItemWithFieldMaps,
true,
),
).toThrow(GraphqlQueryRunnerException); ).toThrow(GraphqlQueryRunnerException);
}); });
}); });

View File

@ -11,19 +11,19 @@ import {
GraphqlQueryRunnerException, GraphqlQueryRunnerException,
GraphqlQueryRunnerExceptionCode, GraphqlQueryRunnerExceptionCode,
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; } 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 { computeOperator } from 'src/engine/api/utils/compute-operator.utils';
import { isAscendingOrder } from 'src/engine/api/utils/is-ascending-order.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 { 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 { 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 { buildCursorCompositeFieldWhereCondition } from 'src/engine/api/utils/build-cursor-composite-field-where-condition.utils';
type BuildCursorWhereConditionParams = { type BuildCursorWhereConditionParams = {
cursorKey: keyof ObjectRecord; cursorKey: keyof ObjectRecord;
cursorValue: cursorValue:
| ObjectRecordCursorLeafScalarValue | ObjectRecordCursorLeafScalarValue
| ObjectRecordCursorLeafCompositeValue; | ObjectRecordCursorLeafCompositeValue;
fieldMetadataMapByName: FieldMetadataMap; objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
orderBy: ObjectRecordOrderBy; orderBy: ObjectRecordOrderBy;
isForwardPagination: boolean; isForwardPagination: boolean;
isEqualityCondition?: boolean; isEqualityCondition?: boolean;
@ -32,12 +32,15 @@ type BuildCursorWhereConditionParams = {
export const buildCursorWhereCondition = ({ export const buildCursorWhereCondition = ({
cursorKey, cursorKey,
cursorValue, cursorValue,
fieldMetadataMapByName, objectMetadataItemWithFieldMaps,
orderBy, orderBy,
isForwardPagination, isForwardPagination,
isEqualityCondition = false, isEqualityCondition = false,
}: BuildCursorWhereConditionParams): Record<string, unknown> => { }: BuildCursorWhereConditionParams): Record<string, unknown> => {
const fieldMetadata = fieldMetadataMapByName[cursorKey]; const fieldMetadataId =
objectMetadataItemWithFieldMaps.fieldIdByName[cursorKey];
const fieldMetadata =
objectMetadataItemWithFieldMaps.fieldsById[fieldMetadataId];
if (!fieldMetadata) { if (!fieldMetadata) {
throw new GraphqlQueryRunnerException( throw new GraphqlQueryRunnerException(

View File

@ -9,13 +9,13 @@ import {
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; } 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 { 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 { 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 = ( export const computeCursorArgFilter = (
cursor: ObjectRecordCursor, cursor: ObjectRecordCursor,
orderBy: ObjectRecordOrderBy, orderBy: ObjectRecordOrderBy,
fieldMetadataMapByName: FieldMetadataMap, objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
isForwardPagination = true, isForwardPagination = true,
): ObjectRecordFilter[] => { ): ObjectRecordFilter[] => {
const cursorEntries = Object.entries(cursor) const cursorEntries = Object.entries(cursor)
@ -42,7 +42,7 @@ export const computeCursorArgFilter = (
buildCursorWhereCondition({ buildCursorWhereCondition({
cursorKey, cursorKey,
cursorValue, cursorValue,
fieldMetadataMapByName, objectMetadataItemWithFieldMaps,
orderBy, orderBy,
isForwardPagination: true, isForwardPagination: true,
isEqualityCondition: true, isEqualityCondition: true,
@ -51,7 +51,7 @@ export const computeCursorArgFilter = (
buildCursorWhereCondition({ buildCursorWhereCondition({
cursorKey, cursorKey,
cursorValue, cursorValue,
fieldMetadataMapByName, objectMetadataItemWithFieldMaps,
orderBy, orderBy,
isForwardPagination, isForwardPagination,
}), }),

View File

@ -23,25 +23,6 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa
labelIdentifierFieldMetadataId: 'nameFieldMetadataId', labelIdentifierFieldMetadataId: 'nameFieldMetadataId',
imageIdentifierFieldMetadataId: '', imageIdentifierFieldMetadataId: '',
workspaceId: '', workspaceId: '',
fields: [
{
id: 'nameFieldMetadataId',
objectMetadataId: '',
type: FieldMetadataType.FULL_NAME,
icon: 'test-field-icon',
name: 'name',
label: 'Name',
defaultValue: {
lastName: "''",
firstName: "''",
},
description: 'Contacts name',
isCustom: false,
isNullable: true,
isUnique: false,
workspaceId: '',
},
],
indexMetadatas: [], indexMetadatas: [],
fieldsById: { fieldsById: {
nameFieldMetadataId: { nameFieldMetadataId: {
@ -60,28 +41,15 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa
isNullable: true, isNullable: true,
isUnique: false, isUnique: false,
workspaceId: '', workspaceId: '',
isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
}, },
}, },
fieldsByName: { fieldIdByName: {
name: { name: 'nameFieldMetadataId',
id: 'nameFieldMetadataId',
objectMetadataId: '',
type: FieldMetadataType.FULL_NAME,
icon: 'test-field-icon',
name: 'name',
label: 'Name',
defaultValue: {
lastName: "''",
firstName: "''",
},
description: 'Contacts name',
isCustom: false,
isNullable: true,
isUnique: false,
workspaceId: '',
},
}, },
fieldsByJoinColumnName: {}, fieldIdByJoinColumnName: {},
}, },
{ {
id: '', id: '',
@ -102,34 +70,6 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa
labelIdentifierFieldMetadataId: 'nameFieldMetadataId', labelIdentifierFieldMetadataId: 'nameFieldMetadataId',
imageIdentifierFieldMetadataId: '', imageIdentifierFieldMetadataId: '',
workspaceId: '', 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: [], indexMetadatas: [],
fieldsById: { fieldsById: {
nameFieldMetadataId: { nameFieldMetadataId: {
@ -144,6 +84,9 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa
isNullable: true, isNullable: true,
isUnique: false, isUnique: false,
workspaceId: '', workspaceId: '',
isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
}, },
domainNameFieldMetadataId: { domainNameFieldMetadataId: {
id: 'domainNameFieldMetadataId', id: 'domainNameFieldMetadataId',
@ -157,40 +100,16 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa
isNullable: true, isNullable: true,
isUnique: false, isUnique: false,
workspaceId: '', workspaceId: '',
isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
}, },
}, },
fieldsByName: { fieldIdByName: {
name: { name: 'nameFieldMetadataId',
id: 'nameFieldMetadataId', domainName: 'domainNameFieldMetadataId',
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: '',
},
}, },
fieldsByJoinColumnName: {}, fieldIdByJoinColumnName: {},
}, },
{ {
id: '', id: '',
@ -211,34 +130,6 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa
labelIdentifierFieldMetadataId: 'nameFieldMetadataId', labelIdentifierFieldMetadataId: 'nameFieldMetadataId',
imageIdentifierFieldMetadataId: 'imageIdentifierFieldMetadataId', imageIdentifierFieldMetadataId: 'imageIdentifierFieldMetadataId',
workspaceId: '', 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: [], indexMetadatas: [],
fieldsById: { fieldsById: {
nameFieldMetadataId: { nameFieldMetadataId: {
@ -253,6 +144,9 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa
isNullable: true, isNullable: true,
isUnique: false, isUnique: false,
workspaceId: '', workspaceId: '',
isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
}, },
imageIdentifierFieldMetadataId: { imageIdentifierFieldMetadataId: {
id: 'imageIdentifierFieldMetadataId', id: 'imageIdentifierFieldMetadataId',
@ -266,40 +160,16 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa
isNullable: true, isNullable: true,
isUnique: false, isUnique: false,
workspaceId: '', workspaceId: '',
isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
}, },
}, },
fieldsByName: { fieldIdByName: {
name: { name: 'nameFieldMetadataId',
id: 'nameFieldMetadataId', imageIdentifierFieldName: 'imageIdentifierFieldMetadataId',
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: '',
},
}, },
fieldsByJoinColumnName: {}, fieldIdByJoinColumnName: {},
}, },
{ {
id: '', id: '',
@ -320,10 +190,9 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa
labelIdentifierFieldMetadataId: '', labelIdentifierFieldMetadataId: '',
imageIdentifierFieldMetadataId: '', imageIdentifierFieldMetadataId: '',
workspaceId: '', workspaceId: '',
fields: [],
indexMetadatas: [], indexMetadatas: [],
fieldsById: {}, fieldsById: {},
fieldsByName: {}, fieldIdByName: {},
fieldsByJoinColumnName: {}, fieldIdByJoinColumnName: {},
}, },
]; ];

View File

@ -21,13 +21,13 @@ import {
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service'; 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 { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; 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 { User } from 'src/engine/core-modules/user/user.entity';
import { userValidator } from 'src/engine/core-modules/user/user.validate'; import { userValidator } from 'src/engine/core-modules/user/user.validate';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate'; import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; 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 { 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() @Injectable()
export class AccessTokenService { export class AccessTokenService {
@ -78,9 +78,6 @@ export class AccessTokenService {
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>( await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspaceId, workspaceId,
'workspaceMember', 'workspaceMember',
{
shouldFailIfMetadataNotFound: false,
},
); );
const workspaceMember = await workspaceMemberRepository.findOne({ const workspaceMember = await workspaceMemberRepository.findOne({

View File

@ -13,6 +13,6 @@ export class ObjectRecordBaseEvent<T = object> {
recordId: string; recordId: string;
userId?: string; userId?: string;
workspaceMemberId?: string; workspaceMemberId?: string;
objectMetadata: ObjectMetadataInterface; objectMetadata: Omit<ObjectMetadataInterface, 'indexMetadatas'>;
properties: Properties<T>; properties: Properties<T>;
} }

View File

@ -4,6 +4,7 @@ import { objectRecordChangedValues } from 'src/engine/core-modules/event-emitter
const mockObjectMetadata: ObjectMetadataInterface = { const mockObjectMetadata: ObjectMetadataInterface = {
id: '1', id: '1',
icon: 'Icon123',
nameSingular: 'Object', nameSingular: 'Object',
namePlural: 'Objects', namePlural: 'Objects',
labelSingular: 'Object', labelSingular: 'Object',

View File

@ -74,10 +74,7 @@ export class FeatureFlagService {
); );
await this.workspaceFeatureFlagsMapCacheService.recomputeFeatureFlagsMapCache( await this.workspaceFeatureFlagsMapCacheService.recomputeFeatureFlagsMapCache(
{ { workspaceId },
workspaceId,
ignoreLock: true,
},
); );
} }
} }
@ -132,10 +129,7 @@ export class FeatureFlagService {
const result = await this.featureFlagRepository.save(featureFlagToSave); const result = await this.featureFlagRepository.save(featureFlagToSave);
await this.workspaceFeatureFlagsMapCacheService.recomputeFeatureFlagsMapCache( await this.workspaceFeatureFlagsMapCacheService.recomputeFeatureFlagsMapCache(
{ { workspaceId },
workspaceId,
ignoreLock: true,
},
); );
return result; return result;

View File

@ -3,8 +3,6 @@ import { Injectable } from '@nestjs/common';
import { FieldMetadataType } from 'twenty-shared/types'; import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils'; 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 { 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 { transformPhonesValue } from 'src/engine/core-modules/record-transformer/utils/transform-phones-value.util';
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
@ -29,19 +27,11 @@ export class RecordInputTransformerService {
return recordInput; return recordInput;
} }
const fieldMetadataByFieldName = objectMetadataMapItem.fields.reduce(
(acc, field) => {
acc[field.name] = field;
return acc;
},
{} as Record<string, FieldMetadataInterface>,
);
let transformedEntries = {}; let transformedEntries = {};
for (const [key, value] of Object.entries(recordInput)) { for (const [key, value] of Object.entries(recordInput)) {
const fieldMetadata = fieldMetadataByFieldName[key]; const fieldMetadataId = objectMetadataMapItem.fieldIdByName[key];
const fieldMetadata = objectMetadataMapItem.fieldsById[fieldMetadataId];
if (!fieldMetadata) { if (!fieldMetadata) {
transformedEntries = { ...transformedEntries, [key]: value }; transformedEntries = { ...transformedEntries, [key]: value };

View File

@ -163,9 +163,13 @@ export class SearchService {
const queryBuilder = entityManager.createQueryBuilder(); const queryBuilder = entityManager.createQueryBuilder();
const queryParser = new GraphqlQueryParser( const queryParser = new GraphqlQueryParser(
objectMetadataItem.fieldsByName, objectMetadataItem,
objectMetadataItem.fieldsByJoinColumnName, generateObjectMetadataMaps([
generateObjectMetadataMaps([objectMetadataItem]), {
...objectMetadataItem,
fields: Object.values(objectMetadataItem.fieldsById),
},
]),
); );
queryParser.applyFilterToBuilder( queryParser.applyFilterToBuilder(

View File

@ -2,9 +2,12 @@ import DataLoader from 'dataloader';
import { import {
FieldMetadataLoaderPayload, FieldMetadataLoaderPayload,
IndexMetadataLoaderPayload,
RelationLoaderPayload, RelationLoaderPayload,
} from 'src/engine/dataloaders/dataloader.service'; } 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 { 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'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
export interface IDataloaders { export interface IDataloaders {
@ -20,6 +23,11 @@ export interface IDataloaders {
fieldMetadataLoader: DataLoader< fieldMetadataLoader: DataLoader<
FieldMetadataLoaderPayload, FieldMetadataLoaderPayload,
FieldMetadataEntity[] FieldMetadataDTO[]
>;
indexMetadataLoader: DataLoader<
IndexMetadataLoaderPayload,
IndexMetadataDTO[]
>; >;
} }

View File

@ -2,9 +2,10 @@ import { Module } from '@nestjs/common';
import { DataloaderService } from 'src/engine/dataloaders/dataloader.service'; import { DataloaderService } from 'src/engine/dataloaders/dataloader.service';
import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module'; 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({ @Module({
imports: [FieldMetadataModule], imports: [FieldMetadataModule, WorkspaceMetadataCacheModule],
providers: [DataloaderService], providers: [DataloaderService],
exports: [DataloaderService], exports: [DataloaderService],
}) })

View File

@ -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 { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
import { IDataloaders } from 'src/engine/dataloaders/dataloader.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 { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/field-metadata.service'; 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 { 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 { 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 = { export type RelationMetadataLoaderPayload = {
workspaceId: string; workspaceId: string;
@ -36,20 +39,28 @@ export type FieldMetadataLoaderPayload = {
objectMetadata: Pick<ObjectMetadataInterface, 'id'>; objectMetadata: Pick<ObjectMetadataInterface, 'id'>;
}; };
export type IndexMetadataLoaderPayload = {
workspaceId: string;
objectMetadata: Pick<ObjectMetadataInterface, 'id'>;
};
@Injectable() @Injectable()
export class DataloaderService { export class DataloaderService {
constructor( constructor(
private readonly fieldMetadataRelationService: FieldMetadataRelationService, private readonly fieldMetadataRelationService: FieldMetadataRelationService,
private readonly fieldMetadataService: FieldMetadataService, private readonly fieldMetadataService: FieldMetadataService,
private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService,
) {} ) {}
createLoaders(): IDataloaders { createLoaders(): IDataloaders {
const relationLoader = this.createRelationLoader(); const relationLoader = this.createRelationLoader();
const fieldMetadataLoader = this.createFieldMetadataLoader(); const fieldMetadataLoader = this.createFieldMetadataLoader();
const indexMetadataLoader = this.createIndexMetadataLoader();
return { return {
relationLoader, relationLoader,
fieldMetadataLoader, fieldMetadataLoader,
indexMetadataLoader,
}; };
} }
@ -78,20 +89,67 @@ export class DataloaderService {
}); });
} }
private createFieldMetadataLoader() { private createIndexMetadataLoader() {
return new DataLoader<FieldMetadataLoaderPayload, FieldMetadataEntity[]>( return new DataLoader<IndexMetadataLoaderPayload, IndexMetadataDTO[]>(
async (dataLoaderParams: FieldMetadataLoaderPayload[]) => { async (dataLoaderParams: IndexMetadataLoaderPayload[]) => {
const workspaceId = dataLoaderParams[0].workspaceId; const workspaceId = dataLoaderParams[0].workspaceId;
const objectMetadataItems = dataLoaderParams.map( const objectMetadataIds = dataLoaderParams.map(
(dataLoaderParam) => dataLoaderParam.objectMetadata, (dataLoaderParam) => dataLoaderParam.objectMetadata.id,
); );
const fieldMetadataCollection = const { objectMetadataMaps } =
await this.fieldMetadataService.getFieldMetadataItemsByBatch( await this.workspaceMetadataCacheService.getExistingOrRecomputeMetadataMaps(
objectMetadataItems.map((item) => item.id), { workspaceId },
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<FieldMetadataLoaderPayload, FieldMetadataDTO[]>(
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; return fieldMetadataCollection;
}, },
); );

View File

@ -1,11 +1,12 @@
import { import {
Column, Column,
CreateDateColumn, CreateDateColumn,
DataSourceOptions,
Entity, Entity,
Index,
OneToMany,
PrimaryGeneratedColumn, PrimaryGeneratedColumn,
UpdateDateColumn, UpdateDateColumn,
DataSourceOptions,
OneToMany,
} from 'typeorm'; } from 'typeorm';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; 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']; export type DataSourceType = DataSourceOptions['type'];
@Entity('dataSource') @Entity('dataSource')
@Index('IDX_DATA_SOURCE_WORKSPACE_ID_CREATED_AT', ['workspaceId', 'createdAt'])
export class DataSourceEntity { export class DataSourceEntity {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
id: string; id: string;

View File

@ -17,7 +17,6 @@ import {
import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface'; 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 { 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 { 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 { 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'; 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< export class FieldMetadataEntity<
T extends FieldMetadataType = FieldMetadataType, T extends FieldMetadataType = FieldMetadataType,
> implements FieldMetadataInterface<T> > {
{
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
id: string; id: string;

View File

@ -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 { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module'; import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.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 { 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 { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.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, ActorModule,
ViewModule, ViewModule,
PermissionsModule, PermissionsModule,
WorkspaceMetadataCacheModule,
], ],
services: [ services: [
IsFieldMetadataDefaultValue, IsFieldMetadataDefaultValue,

View File

@ -10,6 +10,7 @@ import { isDefined } from 'twenty-shared/utils';
import { DataSource, FindOneOptions, In, Repository } from 'typeorm'; import { DataSource, FindOneOptions, In, Repository } from 'typeorm';
import { v4 as uuidV4, v4 } from 'uuid'; 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 { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { settings } from 'src/engine/constants/settings'; 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 { 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 { 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 { 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 { 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 { 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 { 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 { validateFieldNameAvailabilityOrThrow } from 'src/engine/metadata-modules/utils/validate-field-name-availability.utils';
import { validateMetadataNameOrThrow } from 'src/engine/metadata-modules/utils/validate-metadata-name.utils'; import { validateMetadataNameOrThrow } from 'src/engine/metadata-modules/utils/validate-metadata-name.utils';
@ -50,6 +51,7 @@ import {
computeMetadataNameFromLabel, computeMetadataNameFromLabel,
validateNameAndLabelAreSyncOrThrow, validateNameAndLabelAreSyncOrThrow,
} from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util'; } 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 { 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 { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
import { import {
@ -78,8 +80,8 @@ type ValidateFieldMetadataArgs<T extends UpdateFieldInput | CreateFieldInput> =
{ {
fieldMetadataType: FieldMetadataType; fieldMetadataType: FieldMetadataType;
fieldMetadataInput: T; fieldMetadataInput: T;
objectMetadata: ObjectMetadataEntity; objectMetadata: ObjectMetadataItemWithFieldMaps;
existingFieldMetadata?: FieldMetadataEntity; existingFieldMetadata?: FieldMetadataInterface;
}; };
@Injectable() @Injectable()
@ -89,8 +91,6 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
private readonly coreDataSource: DataSource, private readonly coreDataSource: DataSource,
@InjectRepository(FieldMetadataEntity, 'core') @InjectRepository(FieldMetadataEntity, 'core')
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>, private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
@InjectRepository(ObjectMetadataEntity, 'core')
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
private readonly workspaceMigrationFactory: WorkspaceMigrationFactory, private readonly workspaceMigrationFactory: WorkspaceMigrationFactory,
private readonly workspaceMigrationService: WorkspaceMigrationService, private readonly workspaceMigrationService: WorkspaceMigrationService,
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService, private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
@ -100,6 +100,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
private readonly fieldMetadataValidationService: FieldMetadataValidationService, private readonly fieldMetadataValidationService: FieldMetadataValidationService,
private readonly fieldMetadataRelatedRecordsService: FieldMetadataRelatedRecordsService, private readonly fieldMetadataRelatedRecordsService: FieldMetadataRelatedRecordsService,
private readonly viewService: ViewService, private readonly viewService: ViewService,
private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService,
) { ) {
super(fieldMetadataRepository); super(fieldMetadataRepository);
} }
@ -123,54 +124,49 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
id: string, id: string,
fieldMetadataInput: UpdateFieldInput, fieldMetadataInput: UpdateFieldInput,
): Promise<FieldMetadataEntity> { ): Promise<FieldMetadataEntity> {
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(); const queryRunner = this.coreDataSource.createQueryRunner();
await queryRunner.connect(); await queryRunner.connect();
await queryRunner.startTransaction(); await queryRunner.startTransaction();
try { try {
const fieldMetadataRepository = if (
queryRunner.manager.getRepository<FieldMetadataEntity>( !isDefined(
FieldMetadataEntity, objectMetadataItemWithFieldMaps.labelIdentifierFieldMetadataId,
); )
) {
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)) {
throw new FieldMetadataException( throw new FieldMetadataException(
'Label identifier field metadata id does not exist', 'Label identifier field metadata id does not exist',
FieldMetadataExceptionCode.LABEL_IDENTIFIER_FIELD_METADATA_ID_NOT_FOUND, FieldMetadataExceptionCode.LABEL_IDENTIFIER_FIELD_METADATA_ID_NOT_FOUND,
); );
} }
assertMutationNotOnRemoteObject(objectMetadata); assertMutationNotOnRemoteObject(objectMetadataItemWithFieldMaps);
assertDoesNotNullifyDefaultValueForNonNullableField({ assertDoesNotNullifyDefaultValueForNonNullableField({
isNullable: existingFieldMetadata.isNullable, isNullable: existingFieldMetadata.isNullable,
@ -180,19 +176,9 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
if (fieldMetadataInput.isActive === false) { if (fieldMetadataInput.isActive === false) {
checkCanDeactivateFieldOrThrow({ checkCanDeactivateFieldOrThrow({
labelIdentifierFieldMetadataId: labelIdentifierFieldMetadataId:
objectMetadata.labelIdentifierFieldMetadataId, objectMetadataItemWithFieldMaps.labelIdentifierFieldMetadataId,
existingFieldMetadata, existingFieldMetadata,
}); });
const viewsRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
fieldMetadataInput.workspaceId,
'view',
);
await viewsRepository.delete({
kanbanFieldMetadataId: id,
});
} }
const updatableFieldInput = const updatableFieldInput =
@ -221,7 +207,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
fieldMetadataType: existingFieldMetadata.type, fieldMetadataType: existingFieldMetadata.type,
existingFieldMetadata, existingFieldMetadata,
fieldMetadataInput: fieldMetadataForUpdate, fieldMetadataInput: fieldMetadataForUpdate,
objectMetadata, objectMetadata: objectMetadataItemWithFieldMaps,
}); });
const isLabelSyncedWithName = const isLabelSyncedWithName =
@ -236,9 +222,9 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
} }
// We're running field update under a transaction, so we can rollback if migration fails // We're running field update under a transaction, so we can rollback if migration fails
await fieldMetadataRepository.update(id, fieldMetadataForUpdate); await this.fieldMetadataRepository.update(id, fieldMetadataForUpdate);
const [updatedFieldMetadata] = await fieldMetadataRepository.find({ const [updatedFieldMetadata] = await this.fieldMetadataRepository.find({
where: { id }, where: { id },
}); });
@ -249,6 +235,18 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
); );
} }
if (fieldMetadataInput.isActive === false) {
const viewsRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
fieldMetadataInput.workspaceId,
'view',
);
await viewsRepository.delete({
kanbanFieldMetadataId: id,
});
}
if ( if (
updatedFieldMetadata.isActive && updatedFieldMetadata.isActive &&
isSelectOrMultiSelectFieldMetadata(updatedFieldMetadata) && isSelectOrMultiSelectFieldMetadata(updatedFieldMetadata) &&
@ -272,10 +270,10 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
) { ) {
await this.workspaceMigrationService.createCustomMigration( await this.workspaceMigrationService.createCustomMigration(
generateMigrationName(`update-${updatedFieldMetadata.name}`), generateMigrationName(`update-${updatedFieldMetadata.name}`),
existingFieldMetadata.workspaceId, fieldMetadataInput.workspaceId,
[ [
{ {
name: computeObjectTargetTable(objectMetadata), name: computeObjectTargetTable(objectMetadataItemWithFieldMaps),
action: WorkspaceMigrationTableActionType.ALTER, action: WorkspaceMigrationTableActionType.ALTER,
columns: this.workspaceMigrationFactory.createColumnActions( columns: this.workspaceMigrationFactory.createColumnActions(
WorkspaceMigrationColumnActionType.ALTER, WorkspaceMigrationColumnActionType.ALTER,
@ -299,6 +297,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
throw error; throw error;
} finally { } finally {
await queryRunner.release(); await queryRunner.release();
await this.workspaceMetadataVersionService.incrementMetadataVersion( await this.workspaceMetadataVersionService.incrementMetadataVersion(
fieldMetadataInput.workspaceId, fieldMetadataInput.workspaceId,
); );
@ -470,28 +469,6 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
} }
} }
public async findOneOrFail(
id: string,
options?: FindOneOptions<FieldMetadataEntity>,
) {
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( public async findOneWithinWorkspace(
workspaceId: string, workspaceId: string,
options: FindOneOptions<FieldMetadataEntity>, options: FindOneOptions<FieldMetadataEntity>,
@ -509,7 +486,10 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
private buildUpdatableStandardFieldInput( private buildUpdatableStandardFieldInput(
fieldMetadataInput: UpdateFieldInput, fieldMetadataInput: UpdateFieldInput,
existingFieldMetadata: FieldMetadataEntity, existingFieldMetadata: Pick<
FieldMetadataInterface,
'type' | 'isNullable' | 'defaultValue' | 'options'
>,
) { ) {
const updatableStandardFieldInput: UpdateFieldInput & { const updatableStandardFieldInput: UpdateFieldInput & {
standardOverrides?: FieldStandardOverridesDTO; standardOverrides?: FieldStandardOverridesDTO;
@ -754,7 +734,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
private async validateAndCreateFieldMetadataItems( private async validateAndCreateFieldMetadataItems(
fieldMetadataInput: CreateFieldInput, fieldMetadataInput: CreateFieldInput,
objectMetadata: ObjectMetadataEntity, objectMetadata: ObjectMetadataItemWithFieldMaps,
fieldMetadataRepository: Repository<FieldMetadataEntity>, fieldMetadataRepository: Repository<FieldMetadataEntity>,
): Promise<FieldMetadataEntity[]> { ): Promise<FieldMetadataEntity[]> {
if (!fieldMetadataInput.isRemoteCreation) { if (!fieldMetadataInput.isRemoteCreation) {
@ -841,7 +821,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
isRemoteCreation, isRemoteCreation,
}: { }: {
createdFieldMetadataItems: FieldMetadataEntity[]; createdFieldMetadataItems: FieldMetadataEntity[];
objectMetadataMap: Record<string, ObjectMetadataEntity>; objectMetadataMap: Record<string, ObjectMetadataItemWithFieldMaps>;
isRemoteCreation: boolean; isRemoteCreation: boolean;
}): Promise<WorkspaceMigrationTableAction[]> { }): Promise<WorkspaceMigrationTableAction[]> {
if (isRemoteCreation) { if (isRemoteCreation) {
@ -886,6 +866,11 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
return []; return [];
} }
const { objectMetadataMaps } =
await this.workspaceMetadataCacheService.getExistingOrRecomputeMetadataMaps(
{ workspaceId: fieldMetadataInputs[0].workspaceId },
);
const workspaceId = fieldMetadataInputs[0].workspaceId; const workspaceId = fieldMetadataInputs[0].workspaceId;
const queryRunner = this.coreDataSource.createQueryRunner(); const queryRunner = this.coreDataSource.createQueryRunner();
@ -902,23 +887,11 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
this.groupFieldInputsByObjectId(fieldMetadataInputs); this.groupFieldInputsByObjectId(fieldMetadataInputs);
const objectMetadataIds = Object.keys(inputsByObjectId); const objectMetadataIds = Object.keys(inputsByObjectId);
const objectMetadatas = await this.objectMetadataRepository.find({
where: {
workspaceId,
},
relations: ['fields'],
});
const objectMetadataMap = objectMetadatas.reduce(
(acc, obj) => ({ ...acc, [obj.id]: obj }),
{} as Record<string, ObjectMetadataEntity>,
);
const createdFieldMetadatas: FieldMetadataEntity[] = []; const createdFieldMetadatas: FieldMetadataEntity[] = [];
const migrationActions: WorkspaceMigrationTableAction[] = []; const migrationActions: WorkspaceMigrationTableAction[] = [];
for (const objectMetadataId of objectMetadataIds) { for (const objectMetadataId of objectMetadataIds) {
const objectMetadata = objectMetadataMap[objectMetadataId]; const objectMetadata = objectMetadataMaps.byId[objectMetadataId];
if (!isDefined(objectMetadata)) { if (!isDefined(objectMetadata)) {
throw new FieldMetadataException( throw new FieldMetadataException(
@ -941,7 +914,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
const fieldMigrationActions = await this.createMigrationActions({ const fieldMigrationActions = await this.createMigrationActions({
createdFieldMetadataItems, createdFieldMetadataItems,
objectMetadataMap, objectMetadataMap: objectMetadataMaps.byId,
isRemoteCreation: fieldMetadataInput.isRemoteCreation ?? false, isRemoteCreation: fieldMetadataInput.isRemoteCreation ?? false,
}); });

View File

@ -19,7 +19,7 @@ export interface FieldMetadataInterface<
workspaceId?: string; workspaceId?: string;
description?: string; description?: string;
icon?: string; icon?: string;
isNullable?: boolean; isNullable: boolean;
isUnique?: boolean; isUnique?: boolean;
relationTargetFieldMetadataId?: string; relationTargetFieldMetadataId?: string;
relationTargetFieldMetadata?: FieldMetadataInterface; relationTargetFieldMetadata?: FieldMetadataInterface;
@ -30,4 +30,7 @@ export interface FieldMetadataInterface<
isActive?: boolean; isActive?: boolean;
generatedType?: 'STORED' | 'VIRTUAL'; generatedType?: 'STORED' | 'VIRTUAL';
asExpression?: string; asExpression?: string;
isLabelSyncedWithName: boolean;
createdAt: Date;
updatedAt: Date;
} }

View File

@ -13,7 +13,7 @@ export interface ObjectMetadataInterface {
labelSingular: string; labelSingular: string;
labelPlural: string; labelPlural: string;
description?: string; description?: string;
icon?: string; icon: string;
targetTableName: string; targetTableName: string;
fields: FieldMetadataInterface[]; fields: FieldMetadataInterface[];
indexMetadatas: IndexMetadataInterface[]; indexMetadatas: IndexMetadataInterface[];

View File

@ -8,7 +8,7 @@ import {
FieldMetadataExceptionCode, FieldMetadataExceptionCode,
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { removeFieldMapsFromObjectMetadata } from 'src/engine/metadata-modules/utils/remove-field-maps-from-object-metadata.util'; import { getObjectMetadataFromObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
@Injectable() @Injectable()
@ -77,13 +77,15 @@ export class FieldMetadataRelationService {
} }
return { return {
sourceObjectMetadata: removeFieldMapsFromObjectMetadata( sourceObjectMetadata:
sourceObjectMetadata, getObjectMetadataFromObjectMetadataItemWithFieldMaps(
) as ObjectMetadataEntity, sourceObjectMetadata,
) as ObjectMetadataEntity,
sourceFieldMetadata: sourceFieldMetadata as FieldMetadataEntity, sourceFieldMetadata: sourceFieldMetadata as FieldMetadataEntity,
targetObjectMetadata: removeFieldMapsFromObjectMetadata( targetObjectMetadata:
targetObjectMetadata, getObjectMetadataFromObjectMetadataItemWithFieldMaps(
) as ObjectMetadataEntity, targetObjectMetadata,
) as ObjectMetadataEntity,
targetFieldMetadata: targetFieldMetadata as FieldMetadataEntity, targetFieldMetadata: targetFieldMetadata as FieldMetadataEntity,
}; };
}); });

View File

@ -6,6 +6,7 @@ import { assertUnreachable, isDefined } from 'twenty-shared/utils';
import { z } from 'zod'; import { z } from 'zod';
import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface'; import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input'; import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
import { import {
@ -13,7 +14,6 @@ import {
FieldMetadataDefaultOption, FieldMetadataDefaultOption,
} from 'src/engine/metadata-modules/field-metadata/dtos/options.input'; } from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
import { UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input'; import { UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { import {
FieldMetadataException, FieldMetadataException,
FieldMetadataExceptionCode, FieldMetadataExceptionCode,
@ -31,7 +31,10 @@ type Validator<T> = { validator: (str: T) => boolean; message: string };
type FieldMetadataUpdateCreateInput = CreateFieldInput | UpdateFieldInput; type FieldMetadataUpdateCreateInput = CreateFieldInput | UpdateFieldInput;
type ValidateEnumFieldMetadataArgs = { type ValidateEnumFieldMetadataArgs = {
existingFieldMetadata?: FieldMetadataEntity; existingFieldMetadata?: Pick<
FieldMetadataInterface,
'type' | 'isNullable' | 'defaultValue' | 'options'
>;
fieldMetadataInput: FieldMetadataUpdateCreateInput; fieldMetadataInput: FieldMetadataUpdateCreateInput;
fieldMetadataType: EnumFieldMetadataUnionType; fieldMetadataType: EnumFieldMetadataUnionType;
}; };

View File

@ -1,17 +1,15 @@
import { FieldMetadataType } from 'twenty-shared/types'; 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'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
export type SelectOrMultiSelectFieldMetadataEntity = FieldMetadataEntity< export type SelectOrMultiSelectFieldMetadataEntity = FieldMetadataEntity<
FieldMetadataType.SELECT | FieldMetadataType.MULTI_SELECT FieldMetadataType.SELECT | FieldMetadataType.MULTI_SELECT
>; >;
export const isSelectOrMultiSelectFieldMetadata = ( export const isSelectOrMultiSelectFieldMetadata = (
fieldMetadata: unknown, fieldMetadata: FieldMetadataInterface,
): fieldMetadata is SelectOrMultiSelectFieldMetadataEntity => { ): fieldMetadata is SelectOrMultiSelectFieldMetadataEntity => {
if (!(fieldMetadata instanceof FieldMetadataEntity)) {
return false;
}
return [FieldMetadataType.SELECT, FieldMetadataType.MULTI_SELECT].includes( return [FieldMetadataType.SELECT, FieldMetadataType.MULTI_SELECT].includes(
fieldMetadata.type, fieldMetadata.type,
); );

View File

@ -80,5 +80,5 @@ export class IndexMetadataEntity {
default: IndexType.BTREE, default: IndexType.BTREE,
nullable: false, nullable: false,
}) })
indexType?: IndexType; indexType: IndexType;
} }

View File

@ -106,7 +106,10 @@ export class IndexMetadataService {
async recomputeIndexMetadataForObject( async recomputeIndexMetadataForObject(
workspaceId: string, workspaceId: string,
updatedObjectMetadata: ObjectMetadataEntity, updatedObjectMetadata: Pick<
ObjectMetadataEntity,
'nameSingular' | 'isCustom' | 'id'
>,
) { ) {
const indexesToRecompute = await this.indexMetadataRepository.find({ const indexesToRecompute = await this.indexMetadataRepository.find({
where: { where: {
@ -232,7 +235,10 @@ export class IndexMetadataService {
async createIndexRecomputeMigrations( async createIndexRecomputeMigrations(
workspaceId: string, workspaceId: string,
objectMetadata: ObjectMetadataEntity, objectMetadata: Pick<
ObjectMetadataEntity,
'nameSingular' | 'isCustom' | 'id'
>,
recomputedIndexes: { recomputedIndexes: {
indexMetadata: IndexMetadataEntity; indexMetadata: IndexMetadataEntity;
previousName: string; previousName: string;

View File

@ -8,4 +8,6 @@ export interface IndexFieldMetadataInterface {
fieldMetadata: FieldMetadataInterface; fieldMetadata: FieldMetadataInterface;
indexMetadata: IndexMetadataInterface; indexMetadata: IndexMetadataInterface;
order: number; order: number;
createdAt: Date;
updatedAt: Date;
} }

View File

@ -1,7 +1,14 @@
import { IndexFieldMetadataInterface } from 'src/engine/metadata-modules/index-metadata/interfaces/index-field-metadata.interface'; 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 { export interface IndexMetadataInterface {
id: string;
name: string; name: string;
isUnique: boolean; isUnique: boolean;
indexFieldMetadatas: IndexFieldMetadataInterface[]; indexFieldMetadatas: IndexFieldMetadataInterface[];
createdAt: Date;
updatedAt: Date;
indexWhereClause: string | null;
indexType: IndexType;
} }

View File

@ -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 { 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 { 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 { 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 { 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 { 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'; 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, PermissionsModule,
WorkspacePermissionsCacheModule, WorkspacePermissionsCacheModule,
WorkspaceCacheStorageModule, WorkspaceCacheStorageModule,
WorkspaceMetadataCacheModule,
], ],
services: [ services: [
ObjectMetadataService, ObjectMetadataService,

View File

@ -17,6 +17,7 @@ import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorat
import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard'; import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto'; 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 { 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 { ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto';
import { import {
@ -150,4 +151,26 @@ export class ObjectMetadataResolver {
return []; return [];
} }
} }
@ResolveField(() => [IndexMetadataDTO], { nullable: false })
async indexMetadataList(
@AuthWorkspace() workspace: Workspace,
@Parent() objectMetadata: ObjectMetadataDTO,
@Context() context: { loaders: IDataloaders },
): Promise<IndexMetadataDTO[]> {
try {
const indexMetadataItems = await context.loaders.indexMetadataLoader.load(
{
objectMetadata,
workspaceId: workspace.id,
},
);
return indexMetadataItems;
} catch (error) {
objectMetadataGraphqlApiExceptionHandler(error);
return [];
}
}
} }

View File

@ -4,11 +4,9 @@ import { InjectRepository } from '@nestjs/typeorm';
import { i18n } from '@lingui/core'; import { i18n } from '@lingui/core';
import { Query, QueryOptions } from '@ptc-org/nestjs-query-core'; import { Query, QueryOptions } from '@ptc-org/nestjs-query-core';
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; 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 { capitalize, isDefined } from 'twenty-shared/utils';
import { FindManyOptions, FindOneOptions, In, Not, Repository } from 'typeorm'; import { FindManyOptions, FindOneOptions, In, Repository } from 'typeorm';
import { ObjectMetadataStandardIdToIdMap } from 'src/engine/metadata-modules/object-metadata/interfaces/object-metadata-standard-id-to-id-map';
import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId'; import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; 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'; } 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 { 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 { 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 { 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 { 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 { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
@ -58,6 +59,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
private readonly remoteTableRelationsService: RemoteTableRelationsService, private readonly remoteTableRelationsService: RemoteTableRelationsService,
private readonly dataSourceService: DataSourceService, private readonly dataSourceService: DataSourceService,
private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService,
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService, private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService, private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
private readonly searchVectorService: SearchVectorService, private readonly searchVectorService: SearchVectorService,
@ -89,6 +91,13 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
override async createOne( override async createOne(
objectMetadataInput: CreateObjectInput, objectMetadataInput: CreateObjectInput,
): Promise<ObjectMetadataEntity> { ): Promise<ObjectMetadataEntity> {
const { objectMetadataMaps } =
await this.workspaceMetadataCacheService.getExistingOrRecomputeMetadataMaps(
{
workspaceId: objectMetadataInput.workspaceId,
},
);
const lastDataSourceMetadata = const lastDataSourceMetadata =
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail( await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
objectMetadataInput.workspaceId, objectMetadataInput.workspaceId,
@ -131,13 +140,28 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
); );
} }
await this.validatesNoOtherObjectWithSameNameExistsOrThrows({ validatesNoOtherObjectWithSameNameExistsOrThrows({
objectMetadataNamePlural: objectMetadataInput.namePlural, objectMetadataNamePlural: objectMetadataInput.namePlural,
objectMetadataNameSingular: objectMetadataInput.nameSingular, objectMetadataNameSingular: objectMetadataInput.nameSingular,
workspaceId: objectMetadataInput.workspaceId, objectMetadataMaps,
}); });
const createdObjectMetadata = await super.createOne({ const baseCustomFields = buildDefaultFieldsForCustomObject(
objectMetadataInput.workspaceId,
);
const labelIdentifierFieldMetadataId = baseCustomFields.find(
(field) => 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, ...objectMetadataInput,
dataSourceId: lastDataSourceMetadata.id, dataSourceId: lastDataSourceMetadata.id,
targetTableName: 'DEPRECATED', targetTableName: 'DEPRECATED',
@ -146,24 +170,8 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
isSystem: false, isSystem: false,
isRemote: objectMetadataInput.isRemote, isRemote: objectMetadataInput.isRemote,
isSearchable: !objectMetadataInput.isRemote, isSearchable: !objectMetadataInput.isRemote,
fields: objectMetadataInput.isRemote fields: objectMetadataInput.isRemote ? [] : baseCustomFields,
? [] labelIdentifierFieldMetadataId,
: buildDefaultFieldsForCustomObject(objectMetadataInput.workspaceId),
});
const labelIdentifierFieldMetadata = createdObjectMetadata.fields.find(
(field) => 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,
}); });
if (objectMetadataInput.isRemote) { if (objectMetadataInput.isRemote) {
@ -174,6 +182,13 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
objectMetadataInput.primaryKeyColumnType, objectMetadataInput.primaryKeyColumnType,
); );
} else { } else {
const createdRelatedObjectMetadataCollection =
await this.objectMetadataFieldRelationService.createRelationsAndForeignKeysMetadata(
objectMetadataInput.workspaceId,
createdObjectMetadata,
objectMetadataMaps,
);
await this.objectMetadataMigrationService.createTableMigration( await this.objectMetadataMigrationService.createTableMigration(
createdObjectMetadata, createdObjectMetadata,
); );
@ -183,12 +198,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
createdObjectMetadata.fields, createdObjectMetadata.fields,
); );
const createdRelatedObjectMetadataCollection =
await this.objectMetadataFieldRelationService.createRelationsAndForeignKeysMetadata(
objectMetadataInput.workspaceId,
createdObjectMetadata,
);
await this.objectMetadataMigrationService.createRelationMigrations( await this.objectMetadataMigrationService.createRelationMigrations(
createdObjectMetadata, createdObjectMetadata,
createdRelatedObjectMetadataCollection, createdRelatedObjectMetadataCollection,
@ -214,7 +223,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache({ await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache({
workspaceId: objectMetadataInput.workspaceId, workspaceId: objectMetadataInput.workspaceId,
ignoreLock: true,
}); });
return createdObjectMetadata; return createdObjectMetadata;
@ -224,6 +232,11 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
input: UpdateOneObjectInput, input: UpdateOneObjectInput,
workspaceId: string, workspaceId: string,
): Promise<ObjectMetadataEntity> { ): Promise<ObjectMetadataEntity> {
const { objectMetadataMaps } =
await this.workspaceMetadataCacheService.getExistingOrRecomputeMetadataMaps(
{ workspaceId },
);
const inputId = input.id; const inputId = input.id;
const inputPayload = { const inputPayload = {
@ -238,9 +251,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
validateObjectMetadataInputNamesOrThrow(inputPayload); validateObjectMetadataInputNamesOrThrow(inputPayload);
const existingObjectMetadata = await this.objectMetadataRepository.findOne({ const existingObjectMetadata = objectMetadataMaps.byId[inputId];
where: { id: inputId, workspaceId: workspaceId },
});
if (!existingObjectMetadata) { if (!existingObjectMetadata) {
throw new ObjectMetadataException( throw new ObjectMetadataException(
@ -254,14 +265,14 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
...inputPayload, ...inputPayload,
}; };
await this.validatesNoOtherObjectWithSameNameExistsOrThrows({ validatesNoOtherObjectWithSameNameExistsOrThrows({
objectMetadataNameSingular: objectMetadataNameSingular:
existingObjectMetadataCombinedWithUpdateInput.nameSingular, existingObjectMetadataCombinedWithUpdateInput.nameSingular,
objectMetadataNamePlural: objectMetadataNamePlural:
existingObjectMetadataCombinedWithUpdateInput.namePlural, existingObjectMetadataCombinedWithUpdateInput.namePlural,
workspaceId: workspaceId,
existingObjectMetadataId: existingObjectMetadataId:
existingObjectMetadataCombinedWithUpdateInput.id, existingObjectMetadataCombinedWithUpdateInput.id,
objectMetadataMaps,
}); });
if (existingObjectMetadataCombinedWithUpdateInput.isLabelSyncedWithName) { if (existingObjectMetadataCombinedWithUpdateInput.isLabelSyncedWithName) {
@ -395,7 +406,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache({ await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache({
workspaceId, workspaceId,
ignoreLock: true,
}); });
return objectMetadata; return objectMetadata;
@ -436,44 +446,26 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
}); });
} }
public async findMany(options?: FindManyOptions<ObjectMetadataEntity>) {
return this.objectMetadataRepository.find({
relations: ['fields'],
...options,
where: {
...options?.where,
},
});
}
public async deleteObjectsMetadata(workspaceId: string) { public async deleteObjectsMetadata(workspaceId: string) {
await this.objectMetadataRepository.delete({ workspaceId }); await this.objectMetadataRepository.delete({ workspaceId });
} }
public async getObjectMetadataStandardIdToIdMap(workspaceId: string) {
const objectMetadata = await this.findManyWithinWorkspace(workspaceId);
const objectMetadataStandardIdToIdMap =
objectMetadata.reduce<ObjectMetadataStandardIdToIdMap>((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( private async handleObjectNameAndLabelUpdates(
existingObjectMetadata: ObjectMetadataEntity, existingObjectMetadata: Pick<
objectMetadataForUpdate: ObjectMetadataEntity, ObjectMetadataItemWithFieldMaps,
'nameSingular' | 'isCustom' | 'id' | 'labelPlural' | 'icon' | 'fieldsById'
>,
objectMetadataForUpdate: Pick<
ObjectMetadataItemWithFieldMaps,
| 'nameSingular'
| 'isCustom'
| 'workspaceId'
| 'id'
| 'labelSingular'
| 'labelPlural'
| 'icon'
| 'fieldsById'
>,
inputPayload: UpdateObjectPayload, inputPayload: UpdateObjectPayload,
) { ) {
const newTargetTableName = computeObjectTargetTable( const newTargetTableName = computeObjectTargetTable(
@ -533,45 +525,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
} }
} }
private validatesNoOtherObjectWithSameNameExistsOrThrows = async ({
objectMetadataNameSingular,
objectMetadataNamePlural,
workspaceId,
existingObjectMetadataId,
}: {
objectMetadataNameSingular: string;
objectMetadataNamePlural: string;
workspaceId: string;
existingObjectMetadataId?: string;
}): Promise<void> => {
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( async resolveOverridableString(
objectMetadata: ObjectMetadataDTO, objectMetadata: ObjectMetadataDTO,
labelKey: 'labelPlural' | 'labelSingular' | 'description' | 'icon', labelKey: 'labelPlural' | 'labelSingular' | 'description' | 'icon',
@ -581,17 +534,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
return objectMetadata[labelKey]; return objectMetadata[labelKey];
} }
if (!locale || locale === SOURCE_LOCALE) {
if (
objectMetadata.standardOverrides &&
isDefined(objectMetadata.standardOverrides[labelKey])
) {
return objectMetadata.standardOverrides[labelKey] as string;
}
return objectMetadata[labelKey];
}
const translationValue = const translationValue =
// @ts-expect-error legacy noImplicitAny // @ts-expect-error legacy noImplicitAny
objectMetadata.standardOverrides?.translations?.[locale]?.[labelKey]; objectMetadata.standardOverrides?.translations?.[locale]?.[labelKey];

View File

@ -14,6 +14,8 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat
import { buildDescriptionForRelationFieldMetadataOnFromField } from 'src/engine/metadata-modules/object-metadata/utils/build-description-for-relation-field-on-from-field.util'; import { buildDescriptionForRelationFieldMetadataOnFromField } from 'src/engine/metadata-modules/object-metadata/utils/build-description-for-relation-field-on-from-field.util';
import { buildDescriptionForRelationFieldMetadataOnToField } from 'src/engine/metadata-modules/object-metadata/utils/build-description-for-relation-field-on-to-field.util'; import { buildDescriptionForRelationFieldMetadataOnToField } from 'src/engine/metadata-modules/object-metadata/utils/build-description-for-relation-field-on-to-field.util';
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-on-delete-action.type'; 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 { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { import {
CUSTOM_OBJECT_STANDARD_FIELD_IDS, CUSTOM_OBJECT_STANDARD_FIELD_IDS,
STANDARD_OBJECT_FIELD_IDS, STANDARD_OBJECT_FIELD_IDS,
@ -41,33 +43,51 @@ export class ObjectMetadataFieldRelationService {
public async createRelationsAndForeignKeysMetadata( public async createRelationsAndForeignKeysMetadata(
workspaceId: string, workspaceId: string,
sourceObjectMetadata: ObjectMetadataEntity, sourceObjectMetadata: Pick<
ObjectMetadataItemWithFieldMaps,
'id' | 'nameSingular' | 'labelSingular'
>,
objectMetadataMaps: ObjectMetadataMaps,
) { ) {
const relatedObjectMetadataCollection = await Promise.all( const relatedObjectMetadataCollection = await Promise.all(
DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS.map( DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS.map(
async (relationObjectMetadataStandardId) => async (relationObjectMetadataStandardId) =>
this.createRelationAndForeignKeyMetadata( this.createRelationAndForeignKeyMetadata({
workspaceId, workspaceId,
sourceObjectMetadata, sourceObjectMetadata,
relationObjectMetadataStandardId, relationObjectMetadataStandardId,
), objectMetadataMaps,
}),
), ),
); );
return relatedObjectMetadataCollection; return relatedObjectMetadataCollection;
} }
private async createRelationAndForeignKeyMetadata( private async createRelationAndForeignKeyMetadata({
workspaceId: string, workspaceId,
sourceObjectMetadata: ObjectMetadataEntity, sourceObjectMetadata,
relationObjectMetadataStandardId: string, relationObjectMetadataStandardId,
) { objectMetadataMaps,
const targetObjectMetadata = }: {
await this.objectMetadataRepository.findOneByOrFail({ workspaceId: string;
standardId: relationObjectMetadataStandardId, sourceObjectMetadata: Pick<
workspaceId: workspaceId, ObjectMetadataItemWithFieldMaps,
isCustom: false, '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( await this.createFieldMetadataRelation(
workspaceId, workspaceId,
@ -80,8 +100,11 @@ export class ObjectMetadataFieldRelationService {
private async createFieldMetadataRelation( private async createFieldMetadataRelation(
workspaceId: string, workspaceId: string,
sourceObjectMetadata: ObjectMetadataEntity, sourceObjectMetadata: Pick<
targetObjectMetadata: ObjectMetadataEntity, ObjectMetadataItemWithFieldMaps,
'id' | 'nameSingular' | 'labelSingular'
>,
targetObjectMetadata: ObjectMetadataItemWithFieldMaps,
): Promise<FieldMetadataEntity<FieldMetadataType.RELATION>[]> { ): Promise<FieldMetadataEntity<FieldMetadataType.RELATION>[]> {
const sourceFieldMetadata = this.createSourceFieldMetadata( const sourceFieldMetadata = this.createSourceFieldMetadata(
workspaceId, workspaceId,
@ -119,7 +142,10 @@ export class ObjectMetadataFieldRelationService {
public async updateRelationsAndForeignKeysMetadata( public async updateRelationsAndForeignKeysMetadata(
workspaceId: string, workspaceId: string,
updatedObjectMetadata: ObjectMetadataEntity, updatedObjectMetadata: Pick<
ObjectMetadataEntity,
'nameSingular' | 'isCustom' | 'id' | 'labelSingular'
>,
): Promise< ): Promise<
{ {
targetObjectMetadata: ObjectMetadataEntity; targetObjectMetadata: ObjectMetadataEntity;
@ -141,7 +167,10 @@ export class ObjectMetadataFieldRelationService {
private async updateRelationAndForeignKeyMetadata( private async updateRelationAndForeignKeyMetadata(
workspaceId: string, workspaceId: string,
sourceObjectMetadata: ObjectMetadataEntity, sourceObjectMetadata: Pick<
ObjectMetadataEntity,
'nameSingular' | 'id' | 'isCustom' | 'labelSingular'
>,
targetObjectMetadataStandardId: string, targetObjectMetadataStandardId: string,
) { ) {
const targetObjectMetadata = const targetObjectMetadata =
@ -226,8 +255,14 @@ export class ObjectMetadataFieldRelationService {
private createSourceFieldMetadata( private createSourceFieldMetadata(
workspaceId: string, workspaceId: string,
sourceObjectMetadata: ObjectMetadataEntity, sourceObjectMetadata: Pick<
targetObjectMetadata: ObjectMetadataEntity, ObjectMetadataItemWithFieldMaps,
'labelSingular' | 'id'
>,
targetObjectMetadata: Pick<
ObjectMetadataItemWithFieldMaps,
'namePlural' | 'labelSingular'
>,
): Partial<FieldMetadataEntity<FieldMetadataType.RELATION>> { ): Partial<FieldMetadataEntity<FieldMetadataType.RELATION>> {
const relationObjectMetadataNamePlural = targetObjectMetadata.namePlural; const relationObjectMetadataNamePlural = targetObjectMetadata.namePlural;
@ -261,8 +296,8 @@ export class ObjectMetadataFieldRelationService {
} }
private updateSourceFieldMetadata( private updateSourceFieldMetadata(
sourceObjectMetadata: ObjectMetadataEntity, sourceObjectMetadata: Pick<ObjectMetadataEntity, 'labelSingular'>,
targetObjectMetadata: ObjectMetadataEntity, targetObjectMetadata: Pick<ObjectMetadataEntity, 'namePlural'>,
) { ) {
const relationObjectMetadataNamePlural = targetObjectMetadata.namePlural; const relationObjectMetadataNamePlural = targetObjectMetadata.namePlural;
@ -280,8 +315,14 @@ export class ObjectMetadataFieldRelationService {
private createTargetFieldMetadata( private createTargetFieldMetadata(
workspaceId: string, workspaceId: string,
sourceObjectMetadata: ObjectMetadataEntity, sourceObjectMetadata: Pick<
targetObjectMetadata: ObjectMetadataEntity, ObjectMetadataItemWithFieldMaps,
'labelSingular' | 'id' | 'nameSingular'
>,
targetObjectMetadata: Pick<
ObjectMetadataItemWithFieldMaps,
'namePlural' | 'labelSingular' | 'id' | 'nameSingular'
>,
): Partial<FieldMetadataEntity<FieldMetadataType.RELATION>> { ): Partial<FieldMetadataEntity<FieldMetadataType.RELATION>> {
const customStandardFieldId = const customStandardFieldId =
// @ts-expect-error legacy noImplicitAny // @ts-expect-error legacy noImplicitAny
@ -319,8 +360,14 @@ export class ObjectMetadataFieldRelationService {
} }
private updateTargetFieldMetadata( private updateTargetFieldMetadata(
sourceObjectMetadata: ObjectMetadataEntity, sourceObjectMetadata: Pick<
targetObjectMetadata: ObjectMetadataEntity, ObjectMetadataEntity,
'nameSingular' | 'labelSingular'
>,
targetObjectMetadata: Pick<
ObjectMetadataEntity,
'nameSingular' | 'namePlural'
>,
) { ) {
const customStandardFieldId = const customStandardFieldId =
// @ts-expect-error legacy noImplicitAny // @ts-expect-error legacy noImplicitAny

Some files were not shown because too many files have changed in this diff Show More