Compare commits

...
Author SHA1 Message Date
sonarly-bot 56ab123055 fix(server): add missing indexes for custom timeline relations
https://sonarly.com/issue/38659?type=bug

Record pages timed out because timelineActivity lookups on custom relation columns scanned a very large table without an index. The custom-object creation path created relation fields but failed to persist their index metadata.

Fix: Implemented the custom-object relation-index propagation fix in the same code path identified in RCA:

1) `buildDefaultRelationFlatFieldMetadatasForCustomObject` now keeps relation index metadata returned by `generateMorphOrRelationFlatFieldMetadataPair` (in addition to source/target flat field metadata).
2) `fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCreate` now includes those relation index metadatas in `flatIndexMetadataToCreate` alongside the default searchVector index.

This ensures default custom-object relations (including timelineActivity target<CustomObject>Id morph/relation join columns) persist their BTREE index metadata and get created during object creation, preventing unindexed scans.

Authored by Sonarly by autonomous analysis (run 44012).
2026-05-19 13:37:06 +00:00
4 changed files with 25 additions and 4 deletions
@@ -149,6 +149,7 @@ export const useGraphQLErrorHandlerHook = <
[ErrorCode.UNAUTHENTICATED]: MetricsKeys.GraphqlOperation401,
[ErrorCode.FORBIDDEN]: MetricsKeys.GraphqlOperation403,
[ErrorCode.NOT_FOUND]: MetricsKeys.GraphqlOperation404,
[ErrorCode.TIMEOUT]: MetricsKeys.GraphqlOperation500,
[ErrorCode.INTERNAL_SERVER_ERROR]:
MetricsKeys.GraphqlOperation500,
};
@@ -101,6 +101,7 @@ export const fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCre
const {
standardSourceFlatFieldMetadatas,
standardTargetFlatFieldMetadatas,
standardRelationFlatIndexMetadatas,
} = buildDefaultRelationFlatFieldMetadatasForCustomObject({
existingFlatObjectMetadataMaps,
sourceFlatObjectMetadata: universalFlatObjectMetadataToCreate,
@@ -120,9 +121,10 @@ export const fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCre
return {
flatObjectMetadataToCreate: universalFlatObjectMetadataToCreate,
flatIndexMetadataToCreate: Object.values(
defaultIndexesForCustomObject.indexes,
),
flatIndexMetadataToCreate: [
...Object.values(defaultIndexesForCustomObject.indexes),
...standardRelationFlatIndexMetadatas,
],
relationTargetFlatFieldMetadataToCreate: standardTargetFlatFieldMetadatas,
flatFieldMetadataToCreateOnObject: objectFlatFieldMetadatas,
};
@@ -19,6 +19,7 @@ import {
} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
import { STANDARD_OBJECT_ICONS } from 'src/engine/workspace-manager/workspace-migration/constant/standard-object-icons';
import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type';
import { type UniversalFlatIndexMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-index-metadata.type';
import { type UniversalFlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-metadata.type';
const morphIdByRelationObjectNameSingular = {
@@ -41,11 +42,13 @@ export type BuildDefaultRelationFieldsForCustomObjectArgs = {
type SourceAndTargetFlatFieldMetadatasRecord = {
standardSourceFlatFieldMetadatas: UniversalFlatFieldMetadata[];
standardTargetFlatFieldMetadatas: UniversalFlatFieldMetadata[];
standardRelationFlatIndexMetadatas: UniversalFlatIndexMetadata[];
};
const EMPTY_SOURCE_AND_TARGET_FLAT_FIELD_METADATAS_RECORD: SourceAndTargetFlatFieldMetadatasRecord =
{
standardSourceFlatFieldMetadatas: [],
standardTargetFlatFieldMetadatas: [],
standardRelationFlatIndexMetadatas: [],
};
export const buildDefaultRelationFlatFieldMetadatasForCustomObject = ({
@@ -107,7 +110,7 @@ export const buildDefaultRelationFlatFieldMetadatasForCustomObject = ({
const morphId =
morphIdByRelationObjectNameSingular[objectMetadataNameSingular];
const { flatFieldMetadatas } =
const { flatFieldMetadatas, indexMetadatas } =
generateMorphOrRelationFlatFieldMetadataPair({
sourceFlatObjectMetadata,
targetFlatObjectMetadata,
@@ -144,6 +147,10 @@ export const buildDefaultRelationFlatFieldMetadatasForCustomObject = ({
...sourceAndTargetFlatFieldMetadatasRecord.standardTargetFlatFieldMetadatas,
flatFieldMetadatas[1],
],
standardRelationFlatIndexMetadatas: [
...sourceAndTargetFlatFieldMetadatasRecord.standardRelationFlatIndexMetadatas,
...indexMetadatas,
],
};
},
EMPTY_SOURCE_AND_TARGET_FLAT_FIELD_METADATAS_RECORD,
@@ -18,6 +18,10 @@ import {
TimeoutError,
ValidationError,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import {
TwentyORMException,
TwentyORMExceptionCode,
} from 'src/engine/twenty-orm/exceptions/twenty-orm.exception';
import { type CustomException } from 'src/utils/custom-exception';
const graphQLPredefinedExceptions = {
@@ -80,6 +84,13 @@ export const shouldCaptureException = (
return false;
}
if (
exception instanceof TwentyORMException &&
exception.code === TwentyORMExceptionCode.QUERY_READ_TIMEOUT
) {
return false;
}
if (statusCode && statusCode < 500) {
return false;
}