[Apps] Fix - app-synced object should be searchable (#19206)

## Summary

- **Make app-synced objects searchable**: `isSearchable` was hardcoded
to `false` and the `searchVector` field was missing the `GENERATED
ALWAYS AS (...)` expression, causing all records to have a `NULL` search
vector and be excluded from search results. Fixed by defaulting
`isSearchable` to `true` (configurable via the object manifest),
computing the `asExpression` from the label identifier field, and
allowing the update-field-action-handler to handle the `null` → defined
`asExpression` transition.
- **Make `isSearchable` updatable on an object**: The property had
`toCompare: false` in the entity properties configuration, so updates
via the API were silently ignored and never persisted. Fixed by setting
`toCompare: true`.
This commit is contained in:
Marie
2026-04-02 19:14:37 +02:00
committed by GitHub
parent 43baf7b91c
commit 2d6c8be7df
34 changed files with 639 additions and 51 deletions
@@ -14,6 +14,7 @@ import {
createStandardFieldFlatMetadata,
} from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-field-flat-metadata.util';
import { createStandardRelationFieldFlatMetadata } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-relation-field-flat-metadata.util';
import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/utils/get-ts-vector-column-expression.util';
export const buildFavoriteStandardFlatFieldMetadatas = ({
now,
@@ -185,6 +186,12 @@ export const buildFavoriteStandardFlatFieldMetadatas = ({
icon: 'IconUser',
isSystem: true,
isNullable: true,
settings: {
generatedType: 'STORED',
asExpression: getTsVectorColumnExpressionFromFields([
{ name: 'id', type: FieldMetadataType.UUID },
]),
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
@@ -13,6 +13,7 @@ import {
createStandardFieldFlatMetadata,
} from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-field-flat-metadata.util';
import { createStandardRelationFieldFlatMetadata } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-relation-field-flat-metadata.util';
import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/utils/get-ts-vector-column-expression.util';
export const buildMessageThreadStandardFlatFieldMetadatas = ({
now,
@@ -180,6 +181,12 @@ export const buildMessageThreadStandardFlatFieldMetadatas = ({
icon: 'IconUser',
isSystem: true,
isNullable: true,
settings: {
generatedType: 'STORED',
asExpression: getTsVectorColumnExpressionFromFields([
{ name: 'id', type: FieldMetadataType.UUID },
]),
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
@@ -15,6 +15,7 @@ import {
createStandardFieldFlatMetadata,
} from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-field-flat-metadata.util';
import { createStandardRelationFieldFlatMetadata } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-relation-field-flat-metadata.util';
import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/utils/get-ts-vector-column-expression.util';
export const buildNoteTargetStandardFlatFieldMetadatas = ({
now,
@@ -189,6 +190,12 @@ export const buildNoteTargetStandardFlatFieldMetadatas = ({
icon: 'IconUser',
isSystem: true,
isNullable: true,
settings: {
generatedType: 'STORED',
asExpression: getTsVectorColumnExpressionFromFields([
{ name: 'id', type: FieldMetadataType.UUID },
]),
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
@@ -15,6 +15,7 @@ import {
createStandardFieldFlatMetadata,
} from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-field-flat-metadata.util';
import { createStandardRelationFieldFlatMetadata } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-relation-field-flat-metadata.util';
import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/utils/get-ts-vector-column-expression.util';
export const buildTaskTargetStandardFlatFieldMetadatas = ({
now,
@@ -189,6 +190,12 @@ export const buildTaskTargetStandardFlatFieldMetadatas = ({
icon: 'IconUser',
isSystem: true,
isNullable: true,
settings: {
generatedType: 'STORED',
asExpression: getTsVectorColumnExpressionFromFields([
{ name: 'id', type: FieldMetadataType.UUID },
]),
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
@@ -14,6 +14,7 @@ import {
createStandardFieldFlatMetadata,
} from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-field-flat-metadata.util';
import { createStandardRelationFieldFlatMetadata } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-relation-field-flat-metadata.util';
import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/utils/get-ts-vector-column-expression.util';
export const buildWorkflowAutomatedTriggerStandardFlatFieldMetadatas = ({
now,
@@ -184,6 +185,12 @@ export const buildWorkflowAutomatedTriggerStandardFlatFieldMetadatas = ({
icon: 'IconUser',
isSystem: true,
isNullable: true,
settings: {
generatedType: 'STORED',
asExpression: getTsVectorColumnExpressionFromFields([
{ name: 'id', type: FieldMetadataType.UUID },
]),
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
@@ -2,15 +2,15 @@ import {
FieldMetadataType,
compositeTypeDefinitions,
} from 'twenty-shared/types';
import type { SearchableFieldType } from 'twenty-shared/utils';
import {
computeColumnName,
computeCompositeColumnName,
} from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { escapeIdentifier } from 'src/engine/workspace-manager/workspace-migration/utils/remove-sql-injection.util';
import { type SearchableFieldType } from 'src/engine/workspace-manager/utils/is-searchable-field.util';
import { isSearchableSubfield } from 'src/engine/workspace-manager/utils/is-searchable-subfield.util';
import { escapeIdentifier } from 'src/engine/workspace-manager/workspace-migration/utils/remove-sql-injection.util';
export type FieldTypeAndNameMetadata = {
name: string;
@@ -126,6 +126,9 @@ const getColumnExpression = (
case FieldMetadataType.PHONES:
return `COALESCE(${quotedColumnName}, '')`;
case FieldMetadataType.UUID:
return `COALESCE(${quotedColumnName}::text, '')`;
default:
return `COALESCE(public.unaccent_immutable(${quotedColumnName}), '')`;
}
@@ -1,19 +0,0 @@
import { FieldMetadataType } from 'twenty-shared/types';
const SEARCHABLE_FIELD_TYPES = [
FieldMetadataType.TEXT,
FieldMetadataType.FULL_NAME,
FieldMetadataType.EMAILS,
FieldMetadataType.ADDRESS,
FieldMetadataType.LINKS,
FieldMetadataType.PHONES,
FieldMetadataType.RICH_TEXT,
FieldMetadataType.UUID,
] as const;
export type SearchableFieldType = (typeof SEARCHABLE_FIELD_TYPES)[number];
export const isSearchableFieldType = (
type: FieldMetadataType,
): type is SearchableFieldType => {
return SEARCHABLE_FIELD_TYPES.includes(type as SearchableFieldType);
};
@@ -35,6 +35,7 @@ type Assertions = [
| 'labelSingular'
| 'labelPlural'
| 'labelIdentifierFieldMetadataUniversalIdentifier'
| 'isSearchable'
>
>,
];
@@ -266,9 +266,8 @@ export class UpdateFieldActionHandlerService extends WorkspaceMigrationRunnerAct
if (
isDefined(toSettings?.asExpression) &&
isDefined(fromSettings?.asExpression) &&
(toSettings.asExpression !== fromSettings.asExpression ||
toSettings.generatedType !== fromSettings.generatedType)
(toSettings.asExpression !== fromSettings?.asExpression ||
toSettings.generatedType !== fromSettings?.generatedType)
) {
await this.workspaceSchemaManagerService.columnManager.dropColumns({
queryRunner,