diff --git a/packages/create-twenty-app/package.json b/packages/create-twenty-app/package.json index 5c94b49114b..2580c0fac6d 100644 --- a/packages/create-twenty-app/package.json +++ b/packages/create-twenty-app/package.json @@ -1,6 +1,6 @@ { "name": "create-twenty-app", - "version": "0.8.0-canary.8", + "version": "0.8.0-canary.9", "description": "Command-line interface to create Twenty application", "main": "dist/cli.cjs", "bin": "dist/cli.cjs", diff --git a/packages/twenty-client-sdk/package.json b/packages/twenty-client-sdk/package.json index 4c6dfac1596..76969157c7f 100644 --- a/packages/twenty-client-sdk/package.json +++ b/packages/twenty-client-sdk/package.json @@ -1,6 +1,6 @@ { "name": "twenty-client-sdk", - "version": "0.8.0-canary.8", + "version": "0.8.0-canary.9", "sideEffects": false, "license": "AGPL-3.0", "scripts": { diff --git a/packages/twenty-front-component-renderer/package.json b/packages/twenty-front-component-renderer/package.json index 73602134c33..5e7380af17a 100644 --- a/packages/twenty-front-component-renderer/package.json +++ b/packages/twenty-front-component-renderer/package.json @@ -1,6 +1,5 @@ { "name": "twenty-front-component-renderer", - "version": "0.8.0-canary.5", "private": true, "main": "dist/index.cjs", "module": "dist/index.mjs", diff --git a/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm.tsx b/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm.tsx index 1267cda67e4..a05bca90752 100644 --- a/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm.tsx @@ -1,18 +1,21 @@ +import { isDDLLockedState } from '@/client-config/states/isDDLLockedState'; import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem'; import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem'; import { getActiveFieldMetadataItems } from '@/object-metadata/utils/getActiveFieldMetadataItems'; import { objectMetadataItemSchema } from '@/object-metadata/validation-schemas/objectMetadataItemSchema'; -import { isDDLLockedState } from '@/client-config/states/isDDLLockedState'; import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly'; import { Select } from '@/ui/input/components/Select'; -import { styled } from '@linaria/react'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { zodResolver } from '@hookform/resolvers/zod'; +import { styled } from '@linaria/react'; import { t } from '@lingui/core/macro'; import { useMemo } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { useNavigate } from 'react-router-dom'; -import { isLabelIdentifierFieldMetadataTypes } from 'twenty-shared/utils'; +import { + isLabelIdentifierFieldMetadataTypes, + isSearchableFieldType, +} from 'twenty-shared/utils'; import { IconCircleOff, IconPlus, useIcons } from 'twenty-ui/display'; import { type SelectOption } from 'twenty-ui/input'; import { themeCssVariables } from 'twenty-ui/theme-constants'; @@ -76,7 +79,8 @@ export const SettingsDataModelObjectIdentifiersForm = ({ getActiveFieldMetadataItems(objectMetadataItem) .filter( ({ id, type }) => - isLabelIdentifierFieldMetadataTypes(type) || + (isLabelIdentifierFieldMetadataTypes(type) && + isSearchableFieldType(type)) || objectMetadataItem.labelIdentifierFieldMetadataId === id, ) .map>((fieldMetadataItem) => ({ diff --git a/packages/twenty-sdk/package.json b/packages/twenty-sdk/package.json index 8807ba7c0c1..dcde754d651 100644 --- a/packages/twenty-sdk/package.json +++ b/packages/twenty-sdk/package.json @@ -1,6 +1,6 @@ { "name": "twenty-sdk", - "version": "0.8.0-canary.8", + "version": "0.8.0-canary.9", "main": "dist/index.cjs", "module": "dist/index.mjs", "types": "dist/sdk/index.d.ts", diff --git a/packages/twenty-sdk/src/cli/utilities/error/serialize-error.ts b/packages/twenty-sdk/src/cli/utilities/error/serialize-error.ts index a15714ead82..aebbe172979 100644 --- a/packages/twenty-sdk/src/cli/utilities/error/serialize-error.ts +++ b/packages/twenty-sdk/src/cli/utilities/error/serialize-error.ts @@ -1,5 +1,19 @@ import axios from 'axios'; +const safeStringify = (value: unknown): string => { + try { + const stringified = JSON.stringify(value, null, 2); + + if (stringified === '{}' || stringified === undefined) { + return String(value); + } + + return stringified; + } catch { + return String(value); + } +}; + export const serializeError = (error: unknown): string => { if (typeof error === 'string') { return error; @@ -14,19 +28,29 @@ export const serializeError = (error: unknown): string => { parts.push(`HTTP ${status}${statusText ? ` ${statusText}` : ''}`); } - const graphqlErrors = error.response?.data?.errors; + const responseData = error.response?.data; + const graphqlErrors = responseData?.errors; if (Array.isArray(graphqlErrors) && graphqlErrors.length > 0) { const messages = graphqlErrors - .map( - (graphqlError: { message?: string }) => - graphqlError.message ?? 'Unknown GraphQL error', - ) + .map((graphqlError: { message?: unknown }) => { + if (typeof graphqlError.message === 'string') { + return graphqlError.message; + } + + return safeStringify(graphqlError); + }) .join('; '); parts.push(messages); - } else if (error.response?.data?.message) { - parts.push(error.response.data.message); + } else if (responseData?.message) { + parts.push( + typeof responseData.message === 'string' + ? responseData.message + : safeStringify(responseData.message), + ); + } else if (responseData) { + parts.push(safeStringify(responseData)); } else if (error.message) { parts.push(error.message); } @@ -42,11 +66,5 @@ export const serializeError = (error: unknown): string => { return error.message || error.toString(); } - const stringified = JSON.stringify(error, null, 2); - - if (stringified === '{}' || stringified === undefined) { - return String(error); - } - - return stringified; + return safeStringify(error); }; diff --git a/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/from-object-manifest-to-universal-flat-object-metadata.util.ts b/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/from-object-manifest-to-universal-flat-object-metadata.util.ts index 0ba3264f23f..815ac8c813f 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/from-object-manifest-to-universal-flat-object-metadata.util.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/from-object-manifest-to-universal-flat-object-metadata.util.ts @@ -29,7 +29,7 @@ export const fromObjectManifestToUniversalFlatObjectMetadata = ({ isSystem: false, isUIReadOnly: false, isAuditLogged: true, - isSearchable: false, + isSearchable: objectManifest.isSearchable ?? true, duplicateCriteria: null, shortcut: null, isLabelSyncedWithName: false, diff --git a/packages/twenty-server/src/engine/core-modules/application/application-manifest/utils/__tests__/compute-search-vector-universal-settings-from-object-manifest.util.spec.ts b/packages/twenty-server/src/engine/core-modules/application/application-manifest/utils/__tests__/compute-search-vector-universal-settings-from-object-manifest.util.spec.ts new file mode 100644 index 00000000000..66bcb2ad11c --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/application/application-manifest/utils/__tests__/compute-search-vector-universal-settings-from-object-manifest.util.spec.ts @@ -0,0 +1,149 @@ +import { type ObjectManifest } from 'twenty-shared/application'; +import { FieldMetadataType } from 'twenty-shared/types'; + +import { computeSearchVectorUniversalSettingsFromObjectManifest } from 'src/engine/core-modules/application/application-manifest/utils/compute-search-vector-universal-settings-from-object-manifest.util'; + +const buildObjectManifest = ( + overrides: Partial & { + fields: ObjectManifest['fields']; + labelIdentifierFieldMetadataUniversalIdentifier: string; + }, +): ObjectManifest => ({ + universalIdentifier: 'obj-uuid-1', + nameSingular: 'testObject', + namePlural: 'testObjects', + labelSingular: 'Test Object', + labelPlural: 'Test Objects', + ...overrides, +}); + +describe('computeSearchVectorUniversalSettingsFromObjectManifest', () => { + it('should return asExpression and generatedType for a TEXT label identifier field', () => { + const result = computeSearchVectorUniversalSettingsFromObjectManifest({ + objectManifest: buildObjectManifest({ + labelIdentifierFieldMetadataUniversalIdentifier: 'field-uuid-name', + fields: [ + { + universalIdentifier: 'field-uuid-name', + name: 'name', + label: 'Name', + type: FieldMetadataType.TEXT, + }, + ], + }), + }); + + expect(result?.generatedType).toBe('STORED'); + expect(result?.asExpression).toContain("to_tsvector('simple'"); + expect(result?.asExpression).toContain('"name"'); + }); + + it('should return asExpression for a FULL_NAME label identifier field', () => { + const result = computeSearchVectorUniversalSettingsFromObjectManifest({ + objectManifest: buildObjectManifest({ + labelIdentifierFieldMetadataUniversalIdentifier: 'field-uuid-name', + fields: [ + { + universalIdentifier: 'field-uuid-name', + name: 'name', + label: 'Name', + type: FieldMetadataType.FULL_NAME, + }, + ], + }), + }); + + expect(result?.generatedType).toBe('STORED'); + expect(result?.asExpression).toContain("to_tsvector('simple'"); + expect(result?.asExpression).toContain('"nameFirstName"'); + expect(result?.asExpression).toContain('"nameLastName"'); + }); + + it('should return asExpression for an EMAILS label identifier field', () => { + const result = computeSearchVectorUniversalSettingsFromObjectManifest({ + objectManifest: buildObjectManifest({ + labelIdentifierFieldMetadataUniversalIdentifier: 'field-uuid-email', + fields: [ + { + universalIdentifier: 'field-uuid-email', + name: 'email', + label: 'Email', + type: FieldMetadataType.EMAILS, + }, + ], + }), + }); + + expect(result?.generatedType).toBe('STORED'); + expect(result?.asExpression).toContain("to_tsvector('simple'"); + expect(result?.asExpression).toContain('"emailPrimaryEmail"'); + }); + + it('should return asExpression for a UUID label identifier field', () => { + const result = computeSearchVectorUniversalSettingsFromObjectManifest({ + objectManifest: buildObjectManifest({ + labelIdentifierFieldMetadataUniversalIdentifier: 'field-uuid-id', + fields: [ + { + universalIdentifier: 'field-uuid-id', + name: 'externalId', + label: 'External ID', + type: FieldMetadataType.UUID, + }, + ], + }), + }); + + expect(result?.generatedType).toBe('STORED'); + expect(result?.asExpression).toContain("to_tsvector('simple'"); + expect(result?.asExpression).toContain('"externalId"'); + }); + + it('should return null when label identifier field is not found in fields', () => { + const result = computeSearchVectorUniversalSettingsFromObjectManifest({ + objectManifest: buildObjectManifest({ + labelIdentifierFieldMetadataUniversalIdentifier: + 'non-existent-field-uuid', + fields: [ + { + universalIdentifier: 'field-uuid-name', + name: 'name', + label: 'Name', + type: FieldMetadataType.TEXT, + }, + ], + }), + }); + + expect(result).toBeNull(); + }); + + it('should return null when label identifier field has a non-searchable type', () => { + const result = computeSearchVectorUniversalSettingsFromObjectManifest({ + objectManifest: buildObjectManifest({ + labelIdentifierFieldMetadataUniversalIdentifier: 'field-uuid-number', + fields: [ + { + universalIdentifier: 'field-uuid-number', + name: 'amount', + label: 'Amount', + type: FieldMetadataType.NUMBER, + }, + ], + }), + }); + + expect(result).toBeNull(); + }); + + it('should return null when fields array is empty', () => { + const result = computeSearchVectorUniversalSettingsFromObjectManifest({ + objectManifest: buildObjectManifest({ + labelIdentifierFieldMetadataUniversalIdentifier: 'field-uuid-name', + fields: [], + }), + }); + + expect(result).toBeNull(); + }); +}); diff --git a/packages/twenty-server/src/engine/core-modules/application/application-manifest/utils/compute-application-manifest-all-universal-flat-entity-maps.util.ts b/packages/twenty-server/src/engine/core-modules/application/application-manifest/utils/compute-application-manifest-all-universal-flat-entity-maps.util.ts index a011d5e9602..23c9fd16672 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-manifest/utils/compute-application-manifest-all-universal-flat-entity-maps.util.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-manifest/utils/compute-application-manifest-all-universal-flat-entity-maps.util.ts @@ -1,4 +1,6 @@ import { type Manifest } from 'twenty-shared/application'; +import { FieldMetadataType } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; import { fromCommandMenuItemManifestToUniversalFlatCommandMenuItem } from 'src/engine/core-modules/application/application-manifest/converters/from-command-menu-item-manifest-to-universal-flat-command-menu-item.util'; import { fromFieldManifestToUniversalFlatFieldMetadata } from 'src/engine/core-modules/application/application-manifest/converters/from-field-manifest-to-universal-flat-field-metadata.util'; @@ -11,6 +13,7 @@ import { fromPageLayoutTabManifestToUniversalFlatPageLayoutTab } from 'src/engin import { fromPageLayoutWidgetManifestToUniversalFlatPageLayoutWidget } from 'src/engine/core-modules/application/application-manifest/converters/from-page-layout-widget-manifest-to-universal-flat-page-layout-widget.util'; import { fromRoleManifestToUniversalFlatRole } from 'src/engine/core-modules/application/application-manifest/converters/from-role-manifest-to-universal-flat-role.util'; import { fromSkillManifestToUniversalFlatSkill } from 'src/engine/core-modules/application/application-manifest/converters/from-skill-manifest-to-universal-flat-skill.util'; +import { computeSearchVectorUniversalSettingsFromObjectManifest } from 'src/engine/core-modules/application/application-manifest/utils/compute-search-vector-universal-settings-from-object-manifest.util'; import { fromViewFieldGroupManifestToUniversalFlatViewFieldGroup } from 'src/engine/core-modules/application/application-manifest/converters/from-view-field-group-manifest-to-universal-flat-view-field-group.util'; import { fromViewFieldManifestToUniversalFlatViewField } from 'src/engine/core-modules/application/application-manifest/converters/from-view-field-manifest-to-universal-flat-view-field.util'; import { fromViewFilterGroupManifestToUniversalFlatViewFilterGroup } from 'src/engine/core-modules/application/application-manifest/converters/from-view-filter-group-manifest-to-universal-flat-view-filter-group.util'; @@ -49,12 +52,25 @@ export const computeApplicationManifestAllUniversalFlatEntityMaps = ({ }); for (const fieldManifest of objectManifest.fields) { + const enrichedFieldManifest = + fieldManifest.type === FieldMetadataType.TS_VECTOR && + !isDefined(fieldManifest.universalSettings) + ? { + ...fieldManifest, + objectUniversalIdentifier: objectManifest.universalIdentifier, + universalSettings: + computeSearchVectorUniversalSettingsFromObjectManifest({ + objectManifest, + }), + } + : { + ...fieldManifest, + objectUniversalIdentifier: objectManifest.universalIdentifier, + }; + addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({ universalFlatEntity: fromFieldManifestToUniversalFlatFieldMetadata({ - fieldManifest: { - ...fieldManifest, - objectUniversalIdentifier: objectManifest.universalIdentifier, - }, + fieldManifest: enrichedFieldManifest, applicationUniversalIdentifier, now, }), diff --git a/packages/twenty-server/src/engine/core-modules/application/application-manifest/utils/compute-search-vector-universal-settings-from-object-manifest.util.ts b/packages/twenty-server/src/engine/core-modules/application/application-manifest/utils/compute-search-vector-universal-settings-from-object-manifest.util.ts new file mode 100644 index 00000000000..6354b9a745d --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/application/application-manifest/utils/compute-search-vector-universal-settings-from-object-manifest.util.ts @@ -0,0 +1,37 @@ +import { type ObjectManifest } from 'twenty-shared/application'; +import { + type FieldMetadataUniversalSettings, + FieldMetadataType, +} from 'twenty-shared/types'; + +import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/utils/get-ts-vector-column-expression.util'; +import { isDefined, isSearchableFieldType } from 'twenty-shared/utils'; + +export const computeSearchVectorUniversalSettingsFromObjectManifest = ({ + objectManifest, +}: { + objectManifest: ObjectManifest; +}): FieldMetadataUniversalSettings => { + const labelIdentifierField = objectManifest.fields.find( + (field) => + field.universalIdentifier === + objectManifest.labelIdentifierFieldMetadataUniversalIdentifier, + ); + + if ( + !isDefined(labelIdentifierField) || + !isSearchableFieldType(labelIdentifierField.type) + ) { + return null; + } + + return { + asExpression: getTsVectorColumnExpressionFromFields([ + { + name: labelIdentifierField.name, + type: labelIdentifierField.type, + }, + ]), + generatedType: 'STORED', + }; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/__tests__/__snapshots__/all-universal-flat-entity-properties-to-compare-and-stringify.constant.spec.ts.snap b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/__tests__/__snapshots__/all-universal-flat-entity-properties-to-compare-and-stringify.constant.spec.ts.snap index e83586d2d06..795de6a8378 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/__tests__/__snapshots__/all-universal-flat-entity-properties-to-compare-and-stringify.constant.spec.ts.snap +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/__tests__/__snapshots__/all-universal-flat-entity-properties-to-compare-and-stringify.constant.spec.ts.snap @@ -142,6 +142,7 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma "nameSingular", "labelIdentifierFieldMetadataUniversalIdentifier", "standardOverrides", + "isSearchable", ], "propertiesToStringify": [ "standardOverrides", diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant.ts index ef1df2e1292..208451fc037 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant.ts @@ -227,7 +227,7 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = { universalProperty: undefined, }, isSearchable: { - toCompare: false, + toCompare: true, toStringify: false, universalProperty: undefined, }, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/__tests__/flat-entity-update.type-test.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/__tests__/flat-entity-update.type-test.ts index 22dd99cc00c..f396fa8b2ca 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/__tests__/flat-entity-update.type-test.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/__tests__/flat-entity-update.type-test.ts @@ -1,7 +1,7 @@ import { - type HasAllProperties, type Equal, type Expect, + type HasAllProperties, } from 'twenty-shared/testing'; import { type FlatEntityUpdate } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-update.type'; @@ -49,6 +49,7 @@ type Assertions = [ | 'labelPlural' | 'labelIdentifierFieldMetadataId' | 'labelIdentifierFieldMetadataUniversalIdentifier' + | 'isSearchable' > >, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/handle-label-identifier-changes-during-field-update.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/handle-label-identifier-changes-during-field-update.util.ts index 6e47031bc67..755a67a98ad 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/handle-label-identifier-changes-during-field-update.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/handle-label-identifier-changes-during-field-update.util.ts @@ -1,5 +1,5 @@ import { type FieldMetadataType, type FromTo } from 'twenty-shared/types'; -import { findOrThrow } from 'twenty-shared/utils'; +import { findOrThrow, type SearchableFieldType } from 'twenty-shared/utils'; import { FieldMetadataException, @@ -11,7 +11,6 @@ import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-m import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants'; import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/utils/get-ts-vector-column-expression.util'; -import { type SearchableFieldType } from 'src/engine/workspace-manager/utils/is-searchable-field.util'; type HandleLabelIdentifierChangesDuringFieldUpdateArgs = { flatObjectMetadata: FlatObjectMetadata; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-ts-vector-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-ts-vector-flat-field-metadata.util.ts index cd8e14a6235..bdf964289ed 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-ts-vector-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-ts-vector-flat-field-metadata.util.ts @@ -28,5 +28,14 @@ export const validateTsVectorFlatFieldMetadata = ({ }); } + if (!flatEntityToValidate.universalSettings?.asExpression) { + errors.push({ + code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT, + message: + 'Field type TS_VECTOR must have an expression. This may have failed to be built because record identifier field does not exist or is not of a searchable type.', + userFriendlyMessage: msg`Field type TS_VECTOR must have an expression. This may have failed to be built because record identifier field does not exist or is not of a searchable type.`, + }); + } + return errors; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/recompute-search-vector-field-after-label-identifier-update.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/recompute-search-vector-field-after-label-identifier-update.util.ts index eab03623a01..8e798fe9acf 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/recompute-search-vector-field-after-label-identifier-update.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/recompute-search-vector-field-after-label-identifier-update.util.ts @@ -1,5 +1,9 @@ import { type FieldMetadataType } from 'twenty-shared/types'; -import { findOrThrow, isDefined } from 'twenty-shared/utils'; +import { + findOrThrow, + isDefined, + type SearchableFieldType, +} from 'twenty-shared/utils'; import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util'; @@ -12,7 +16,6 @@ import { } from 'src/engine/metadata-modules/object-metadata/object-metadata.exception'; import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants'; import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/utils/get-ts-vector-column-expression.util'; -import { type SearchableFieldType } from 'src/engine/workspace-manager/utils/is-searchable-field.util'; type RecomputeSearchVectorFieldAfterLabelIdentifierUpdateArgs = { existingFlatObjectMetadata: FlatObjectMetadata; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-identifiers.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-identifiers.util.ts index a23a1029b57..9bf487c02ea 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-identifiers.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-identifiers.util.ts @@ -2,6 +2,7 @@ import { msg } from '@lingui/core/macro'; import { isDefined, isLabelIdentifierFieldMetadataTypes, + isSearchableFieldType, } from 'twenty-shared/utils'; import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util'; @@ -50,7 +51,14 @@ export const validateFlatObjectMetadataIdentifiers = ({ code: ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, message: 'labelIdentifierFieldMetadataUniversalIdentifier validation failed: field type not compatible', - userFriendlyMessage: msg`Field cannot be used as label identifier`, + userFriendlyMessage: msg`Field cannot be used as label identifier due to its type: should be of type UUID, text or full name`, + }); + } else if (!isSearchableFieldType(universalFlatFieldMetadata.type)) { + errors.push({ + code: ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, + message: + 'labelIdentifierFieldMetadataUniversalIdentifier validation failed: field type not compatible', + userFriendlyMessage: msg`Field cannot be used as label identifier due to its type`, }); } } diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-favorite-standard-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-favorite-standard-flat-field-metadata.util.ts index 5690ae1b13b..7a1870c6c7c 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-favorite-standard-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-favorite-standard-flat-field-metadata.util.ts @@ -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, diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-message-thread-standard-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-message-thread-standard-flat-field-metadata.util.ts index f6a0d34eb78..54f5975ed02 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-message-thread-standard-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-message-thread-standard-flat-field-metadata.util.ts @@ -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, diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-note-target-standard-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-note-target-standard-flat-field-metadata.util.ts index ba606c95811..f3f238ead79 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-note-target-standard-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-note-target-standard-flat-field-metadata.util.ts @@ -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, diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-task-target-standard-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-task-target-standard-flat-field-metadata.util.ts index 7ab16b48de2..eb6582ff8d9 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-task-target-standard-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-task-target-standard-flat-field-metadata.util.ts @@ -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, diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-workflow-automated-trigger-standard-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-workflow-automated-trigger-standard-flat-field-metadata.util.ts index 0c22eaf2c5c..f77e914d754 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-workflow-automated-trigger-standard-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-workflow-automated-trigger-standard-flat-field-metadata.util.ts @@ -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, diff --git a/packages/twenty-server/src/engine/workspace-manager/utils/get-ts-vector-column-expression.util.ts b/packages/twenty-server/src/engine/workspace-manager/utils/get-ts-vector-column-expression.util.ts index c24412a0add..b37ae98642a 100644 --- a/packages/twenty-server/src/engine/workspace-manager/utils/get-ts-vector-column-expression.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/utils/get-ts-vector-column-expression.util.ts @@ -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}), '')`; } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/__tests__/universal-flat-entity-update.test-type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/__tests__/universal-flat-entity-update.test-type.ts index fe2c2aa30bc..a8f20cc3092 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/__tests__/universal-flat-entity-update.test-type.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/__tests__/universal-flat-entity-update.test-type.ts @@ -35,6 +35,7 @@ type Assertions = [ | 'labelSingular' | 'labelPlural' | 'labelIdentifierFieldMetadataUniversalIdentifier' + | 'isSearchable' > >, ]; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/field/services/update-field-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/field/services/update-field-action-handler.service.ts index 3d42be8b17a..a1089015830 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/field/services/update-field-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/field/services/update-field-action-handler.service.ts @@ -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, diff --git a/packages/twenty-server/test/integration/metadata/suites/application/__snapshots__/failing-sync-application-object-system-fields.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/application/__snapshots__/failing-sync-application-object-system-fields.integration-spec.ts.snap index 15ca8750619..07b8044fa0f 100644 --- a/packages/twenty-server/test/integration/metadata/suites/application/__snapshots__/failing-sync-application-object-system-fields.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/application/__snapshots__/failing-sync-application-object-system-fields.integration-spec.ts.snap @@ -13,11 +13,6 @@ exports[`Sync application should fail due to object system fields integrity shou "message": "System fields cannot be deleted", "userFriendlyMessage": "System fields cannot be deleted", }, - { - "code": "FIELD_MUTATION_NOT_ALLOWED", - "message": "Cannot delete, please update the label identifier field first", - "userFriendlyMessage": "Cannot delete, please update the label identifier field first", - }, ], "flatEntityMinimalInformation": { "name": "id", @@ -79,6 +74,132 @@ exports[`Sync application should fail due to object system fields integrity shou } `; +exports[`Sync application should fail due to object system fields integrity when label identifier is non-searchable type (searchVector has no expression) 1`] = ` +{ + "extensions": { + "code": "METADATA_VALIDATION_FAILED", + "errors": { + "fieldMetadata": [ + { + "errors": [ + { + "code": "INVALID_FIELD_INPUT", + "message": "Field type TS_VECTOR must have an expression. This may have failed to be built because record identifier field does not exist or is not of a searchable type.", + "userFriendlyMessage": "Field type TS_VECTOR must have an expression. This may have failed to be built because record identifier field does not exist or is not of a searchable type.", + }, + ], + "flatEntityMinimalInformation": { + "name": "searchVector", + "objectMetadataUniversalIdentifier": Any, + "universalIdentifier": Any, + }, + "metadataName": "fieldMetadata", + "status": "fail", + "type": "create", + }, + ], + "objectMetadata": [ + { + "errors": [ + { + "code": "MISSING_SYSTEM_FIELD", + "message": "System field searchVector is missing", + "userFriendlyMessage": "System field searchVector is missing", + "value": "searchVector", + }, + { + "code": "INVALID_OBJECT_INPUT", + "message": "labelIdentifierFieldMetadataUniversalIdentifier validation failed: field type not compatible", + "userFriendlyMessage": "Field cannot be used as label identifier due to its type: should be of type UUID, text or full name", + }, + ], + "flatEntityMinimalInformation": { + "namePlural": "noSearchVectorExpressions", + "nameSingular": "noSearchVectorExpression", + "universalIdentifier": Any, + }, + "metadataName": "objectMetadata", + "type": "create", + }, + ], + }, + "message": "Validation failed for 1 fieldMetadata, 1 objectMetadata", + "summary": { + "fieldMetadata": 1, + "objectMetadata": 1, + "totalErrors": 2, + }, + "userFriendlyMessage": "Metadata validation failed", + }, + "message": "Validation errors occurred while syncing application manifest metadata", + "name": "GraphQLError", +} +`; + +exports[`Sync application should fail due to object system fields integrity when object has TS_VECTOR field with wrong name (not searchVector) 1`] = ` +{ + "extensions": { + "code": "METADATA_VALIDATION_FAILED", + "errors": { + "fieldMetadata": [ + { + "errors": [ + { + "code": "INVALID_FIELD_INPUT", + "message": "Field type TS_VECTOR must be named "searchVector", got "wrongSearchVector"", + "userFriendlyMessage": "Field type TS_VECTOR must be named "searchVector"", + "value": "wrongSearchVector", + }, + { + "code": "INVALID_FIELD_INPUT", + "message": "Field type TS_VECTOR must be a system field", + "userFriendlyMessage": "Field type TS_VECTOR must be a system field", + "value": false, + }, + ], + "flatEntityMinimalInformation": { + "name": "wrongSearchVector", + "objectMetadataUniversalIdentifier": Any, + "universalIdentifier": Any, + }, + "metadataName": "fieldMetadata", + "status": "fail", + "type": "create", + }, + ], + "objectMetadata": [ + { + "errors": [ + { + "code": "MISSING_SYSTEM_FIELD", + "message": "System field searchVector is missing", + "userFriendlyMessage": "System field searchVector is missing", + "value": "searchVector", + }, + ], + "flatEntityMinimalInformation": { + "namePlural": "wrongTsVectorNames", + "nameSingular": "wrongTsVectorName", + "universalIdentifier": Any, + }, + "metadataName": "objectMetadata", + "type": "create", + }, + ], + }, + "message": "Validation failed for 1 fieldMetadata, 1 objectMetadata", + "summary": { + "fieldMetadata": 1, + "objectMetadata": 1, + "totalErrors": 2, + }, + "userFriendlyMessage": "Metadata validation failed", + }, + "message": "Validation errors occurred while syncing application manifest metadata", + "name": "GraphQLError", +} +`; + exports[`Sync application should fail due to object system fields integrity when object has id field with wrong type (TEXT instead of UUID) 1`] = ` { "extensions": { @@ -237,6 +358,43 @@ exports[`Sync application should fail due to object system fields integrity when } `; +exports[`Sync application should fail due to object system fields integrity when object has searchVector field with wrong type (TEXT instead of TS_VECTOR) 1`] = ` +{ + "extensions": { + "code": "METADATA_VALIDATION_FAILED", + "errors": { + "objectMetadata": [ + { + "errors": [ + { + "code": "INVALID_SYSTEM_FIELD", + "message": "System field searchVector has invalid type: expected TS_VECTOR, got TEXT", + "userFriendlyMessage": "System field searchVector has invalid type", + "value": "TEXT", + }, + ], + "flatEntityMinimalInformation": { + "namePlural": "wrongSearchVectorTypes", + "nameSingular": "wrongSearchVectorType", + "universalIdentifier": Any, + }, + "metadataName": "objectMetadata", + "type": "create", + }, + ], + }, + "message": "Validation failed for 1 objectMetadata", + "summary": { + "objectMetadata": 1, + "totalErrors": 1, + }, + "userFriendlyMessage": "Metadata validation failed", + }, + "message": "Validation errors occurred while syncing application manifest metadata", + "name": "GraphQLError", +} +`; + exports[`Sync application should fail due to object system fields integrity when object is created without any system fields (missing all 8 system fields) 1`] = ` { "extensions": { diff --git a/packages/twenty-server/test/integration/metadata/suites/application/__snapshots__/successful-sync-application-workspace-migration.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/application/__snapshots__/successful-sync-application-workspace-migration.integration-spec.ts.snap index 12b44af83b2..c0a2c22fa6b 100644 --- a/packages/twenty-server/test/integration/metadata/suites/application/__snapshots__/successful-sync-application-workspace-migration.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/application/__snapshots__/successful-sync-application-workspace-migration.integration-spec.ts.snap @@ -81,7 +81,7 @@ exports[`syncApplication should delete old field and create equivalent one when "isCustom": true, "isLabelSyncedWithName": false, "isRemote": false, - "isSearchable": false, + "isSearchable": true, "isSystem": false, "isUIReadOnly": false, "labelIdentifierFieldMetadataUniversalIdentifier": Any, @@ -311,7 +311,10 @@ exports[`syncApplication should delete old field and create equivalent one when "standardOverrides": null, "type": "TS_VECTOR", "universalIdentifier": Any, - "universalSettings": null, + "universalSettings": { + "asExpression": "to_tsvector('simple', COALESCE("id"::text, ''))", + "generatedType": "STORED", + }, "updatedAt": Any, }, { @@ -434,7 +437,7 @@ exports[`syncApplication should return workspace migration actions on initial sy "isCustom": true, "isLabelSyncedWithName": false, "isRemote": false, - "isSearchable": false, + "isSearchable": true, "isSystem": false, "isUIReadOnly": false, "labelIdentifierFieldMetadataUniversalIdentifier": Any, @@ -664,7 +667,10 @@ exports[`syncApplication should return workspace migration actions on initial sy "standardOverrides": null, "type": "TS_VECTOR", "universalIdentifier": Any, - "universalSettings": null, + "universalSettings": { + "asExpression": "to_tsvector('simple', COALESCE("id"::text, ''))", + "generatedType": "STORED", + }, "updatedAt": Any, }, { diff --git a/packages/twenty-server/test/integration/metadata/suites/application/failing-sync-application-object-system-fields.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/application/failing-sync-application-object-system-fields.integration-spec.ts index 65c7db04cb7..265b5cb973f 100644 --- a/packages/twenty-server/test/integration/metadata/suites/application/failing-sync-application-object-system-fields.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/application/failing-sync-application-object-system-fields.integration-spec.ts @@ -73,6 +73,44 @@ const buildObjectWithLabelField = ({ }; }; +const buildDefaultObjectWithModifiedSearchVector = ({ + nameSingular, + namePlural, + labelSingular, + labelPlural, + description, + searchVectorOverrides, +}: { + nameSingular: string; + namePlural: string; + labelSingular: string; + labelPlural: string; + description: string; + searchVectorOverrides: Partial; +}): Pick => { + const defaultObject = buildDefaultObjectManifest({ + nameSingular, + namePlural, + labelSingular, + labelPlural, + description, + }); + + return { + objects: [ + { + ...defaultObject, + fields: defaultObject.fields.map((field) => + field.name === 'searchVector' + ? ({ ...field, ...searchVectorOverrides } as (typeof defaultObject.fields)[number]) + : field, + ), + }, + ], + fields: [], + }; +}; + const failingSyncApplicationSystemFieldsTestCases: SyncApplicationTestingContext = [ { @@ -153,6 +191,75 @@ const failingSyncApplicationSystemFieldsTestCases: SyncApplicationTestingContext ), }, }, + { + title: + 'when object has searchVector field with wrong type (TEXT instead of TS_VECTOR)', + context: { + manifest: buildManifest( + buildDefaultObjectWithModifiedSearchVector({ + nameSingular: 'wrongSearchVectorType', + namePlural: 'wrongSearchVectorTypes', + labelSingular: 'Wrong SearchVector Type', + labelPlural: 'Wrong SearchVector Types', + description: 'Object with wrong searchVector field type', + searchVectorOverrides: { + type: FieldMetadataType.TEXT, + }, + }), + ), + }, + }, + { + title: + 'when object has TS_VECTOR field with wrong name (not searchVector)', + context: { + manifest: buildManifest( + buildDefaultObjectWithModifiedSearchVector({ + nameSingular: 'wrongTsVectorName', + namePlural: 'wrongTsVectorNames', + labelSingular: 'Wrong TsVector Name', + labelPlural: 'Wrong TsVector Names', + description: 'Object with TS_VECTOR field named incorrectly', + searchVectorOverrides: { + name: 'wrongSearchVector', + }, + }), + ), + }, + }, + { + title: + 'when label identifier is non-searchable type (searchVector has no expression)', + context: (() => { + const nonSearchableFieldId = uuidv4(); + + return { + manifest: buildManifest({ + objects: [ + buildDefaultObjectManifest({ + nameSingular: 'noSearchVectorExpression', + namePlural: 'noSearchVectorExpressions', + labelSingular: 'No SearchVector Expression', + labelPlural: 'No SearchVector Expressions', + description: + 'Object whose label identifier is non-searchable, so searchVector has no expression', + labelIdentifierFieldMetadataUniversalIdentifier: + nonSearchableFieldId, + additionalFields: [ + { + universalIdentifier: nonSearchableFieldId, + type: FieldMetadataType.NUMBER, + name: 'quantity', + label: 'Quantity', + }, + ], + }), + ], + fields: [], + }), + }; + })(), + }, ]; describe('Sync application should fail due to object system fields integrity', () => { @@ -207,12 +314,25 @@ describe('Sync application should fail due to object system fields integrity', ( ); it('should fail when trying to delete a system field after a successful sync', async () => { + const labelIdentifierFieldUniversalIdentifier = uuidv4(); const testObject = buildDefaultObjectManifest({ nameSingular: 'deleteSystemFieldObject', namePlural: 'deleteSystemFieldObjects', labelSingular: 'Delete System Field Object', labelPlural: 'Delete System Field Objects', description: 'Object for testing system field deletion', + labelIdentifierFieldMetadataUniversalIdentifier: + labelIdentifierFieldUniversalIdentifier, + additionalFields: [ + { + universalIdentifier: labelIdentifierFieldUniversalIdentifier, + type: FieldMetadataType.TEXT, + name: 'labelIdentifierField', + label: 'Label Identifier Field', + description: 'Label identifier field', + icon: 'IconTextCaption', + }, + ], }); const validManifest = buildManifest({ @@ -244,12 +364,25 @@ describe('Sync application should fail due to object system fields integrity', ( }, 60000); it('should fail when trying to update a system field after a successful sync', async () => { + const labelIdentifierFieldUniversalIdentifier = uuidv4(); const testObject = buildDefaultObjectManifest({ nameSingular: 'updateSystemFieldObject', namePlural: 'updateSystemFieldObjects', labelSingular: 'Update System Field Object', labelPlural: 'Update System Field Objects', description: 'Object for testing system field update', + labelIdentifierFieldMetadataUniversalIdentifier: + labelIdentifierFieldUniversalIdentifier, + additionalFields: [ + { + universalIdentifier: labelIdentifierFieldUniversalIdentifier, + type: FieldMetadataType.TEXT, + name: 'labelIdentifierField', + label: 'Label Identifier Field', + description: 'Label identifier field', + icon: 'IconTextCaption', + }, + ], }); const validManifest = buildManifest({ diff --git a/packages/twenty-server/test/integration/metadata/suites/application/utils/build-default-object-manifest.util.ts b/packages/twenty-server/test/integration/metadata/suites/application/utils/build-default-object-manifest.util.ts index d28d74297fd..6cb71d299be 100644 --- a/packages/twenty-server/test/integration/metadata/suites/application/utils/build-default-object-manifest.util.ts +++ b/packages/twenty-server/test/integration/metadata/suites/application/utils/build-default-object-manifest.util.ts @@ -11,6 +11,7 @@ export const buildDefaultObjectManifest = ({ icon = 'IconTicket', additionalFields = [], universalIdentifier, + labelIdentifierFieldMetadataUniversalIdentifier, }: { nameSingular: string; namePlural: string; @@ -20,12 +21,15 @@ export const buildDefaultObjectManifest = ({ icon?: string; additionalFields?: ObjectManifest['fields']; universalIdentifier?: string; + labelIdentifierFieldMetadataUniversalIdentifier?: string; }): ObjectManifest => { const idFieldUniversalIdentifier = uuidv4(); return { universalIdentifier: universalIdentifier ?? uuidv4(), - labelIdentifierFieldMetadataUniversalIdentifier: idFieldUniversalIdentifier, + labelIdentifierFieldMetadataUniversalIdentifier: + labelIdentifierFieldMetadataUniversalIdentifier ?? + idFieldUniversalIdentifier, nameSingular, namePlural, labelSingular, diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-update-one-object-metadata.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-update-one-object-metadata.integration-spec.ts.snap index 9991a1bd5fc..13144da0320 100644 --- a/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-update-one-object-metadata.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-update-one-object-metadata.integration-spec.ts.snap @@ -12,7 +12,7 @@ exports[`Object metadata update should fail when labelIdentifier is not a TEXT o { "code": "INVALID_OBJECT_INPUT", "message": "labelIdentifierFieldMetadataUniversalIdentifier validation failed: field type not compatible", - "userFriendlyMessage": "Field cannot be used as label identifier", + "userFriendlyMessage": "Field cannot be used as label identifier due to its type: should be of type UUID, text or full name", }, ], "flatEntityMinimalInformation": { diff --git a/packages/twenty-shared/src/application/objectManifestType.ts b/packages/twenty-shared/src/application/objectManifestType.ts index 380517ca3e0..635fab6e90f 100644 --- a/packages/twenty-shared/src/application/objectManifestType.ts +++ b/packages/twenty-shared/src/application/objectManifestType.ts @@ -8,6 +8,7 @@ export type ObjectManifest = SyncableEntityOptions & { labelPlural: string; description?: string; icon?: string; + isSearchable?: boolean; fields: ObjectFieldManifest[]; labelIdentifierFieldMetadataUniversalIdentifier: string; }; diff --git a/packages/twenty-shared/src/utils/index.ts b/packages/twenty-shared/src/utils/index.ts index 0026976fc55..53174539abe 100644 --- a/packages/twenty-shared/src/utils/index.ts +++ b/packages/twenty-shared/src/utils/index.ts @@ -204,6 +204,8 @@ export { assertIsDefinedOrThrow } from './validation/assertIsDefinedOrThrow'; export { isDefined } from './validation/isDefined'; export { isEmptyObject } from './validation/isEmptyObject'; export { isLabelIdentifierFieldMetadataTypes } from './validation/isLabelIdentifierFieldMetadataTypes'; +export type { SearchableFieldType } from './validation/isSearchableFieldType'; +export { isSearchableFieldType } from './validation/isSearchableFieldType'; export { isValidLocale } from './validation/isValidLocale'; export { isValidTwentySubdomain } from './validation/isValidTwentySubdomain'; export { isValidUuid } from './validation/isValidUuid'; diff --git a/packages/twenty-shared/src/utils/validation/index.ts b/packages/twenty-shared/src/utils/validation/index.ts index 76e77636911..e7036edeaa8 100644 --- a/packages/twenty-shared/src/utils/validation/index.ts +++ b/packages/twenty-shared/src/utils/validation/index.ts @@ -2,5 +2,6 @@ export * from './isDefined'; export * from './assertIsDefinedOrThrow'; export * from './isValidLocale'; export * from './isValidTwentySubdomain'; +export * from './isSearchableFieldType'; export * from './isValidUuid'; export * from './normalizeLocale'; diff --git a/packages/twenty-server/src/engine/workspace-manager/utils/is-searchable-field.util.ts b/packages/twenty-shared/src/utils/validation/isSearchableFieldType.ts similarity index 90% rename from packages/twenty-server/src/engine/workspace-manager/utils/is-searchable-field.util.ts rename to packages/twenty-shared/src/utils/validation/isSearchableFieldType.ts index f8b2690e2b7..5c985c5a827 100644 --- a/packages/twenty-server/src/engine/workspace-manager/utils/is-searchable-field.util.ts +++ b/packages/twenty-shared/src/utils/validation/isSearchableFieldType.ts @@ -1,4 +1,5 @@ -import { FieldMetadataType } from 'twenty-shared/types'; +import { FieldMetadataType } from '@/types'; + const SEARCHABLE_FIELD_TYPES = [ FieldMetadataType.TEXT, FieldMetadataType.FULL_NAME,