[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:
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
+8
-4
@@ -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<SelectOption<string | null>>((fieldMetadataItem) => ({
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+149
@@ -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<ObjectManifest> & {
|
||||
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();
|
||||
});
|
||||
});
|
||||
+20
-4
@@ -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,
|
||||
}),
|
||||
|
||||
+37
@@ -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<FieldMetadataType.TS_VECTOR> => {
|
||||
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',
|
||||
};
|
||||
};
|
||||
+1
@@ -142,6 +142,7 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma
|
||||
"nameSingular",
|
||||
"labelIdentifierFieldMetadataUniversalIdentifier",
|
||||
"standardOverrides",
|
||||
"isSearchable",
|
||||
],
|
||||
"propertiesToStringify": [
|
||||
"standardOverrides",
|
||||
|
||||
+1
-1
@@ -227,7 +227,7 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
|
||||
universalProperty: undefined,
|
||||
},
|
||||
isSearchable: {
|
||||
toCompare: false,
|
||||
toCompare: true,
|
||||
toStringify: false,
|
||||
universalProperty: undefined,
|
||||
},
|
||||
|
||||
+2
-1
@@ -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'
|
||||
>
|
||||
>,
|
||||
|
||||
|
||||
+1
-2
@@ -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;
|
||||
|
||||
+9
@@ -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;
|
||||
};
|
||||
|
||||
+5
-2
@@ -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;
|
||||
|
||||
+9
-1
@@ -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`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+7
@@ -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,
|
||||
|
||||
+7
@@ -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,
|
||||
|
||||
+7
@@ -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,
|
||||
|
||||
+7
@@ -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,
|
||||
|
||||
+7
@@ -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,
|
||||
|
||||
+5
-2
@@ -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
@@ -35,6 +35,7 @@ type Assertions = [
|
||||
| 'labelSingular'
|
||||
| 'labelPlural'
|
||||
| 'labelIdentifierFieldMetadataUniversalIdentifier'
|
||||
| 'isSearchable'
|
||||
>
|
||||
>,
|
||||
];
|
||||
|
||||
+2
-3
@@ -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,
|
||||
|
||||
+163
-5
@@ -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<String>,
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"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<String>,
|
||||
},
|
||||
"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<String>,
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"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<String>,
|
||||
},
|
||||
"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<String>,
|
||||
},
|
||||
"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": {
|
||||
|
||||
+10
-4
@@ -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<String>,
|
||||
@@ -311,7 +311,10 @@ exports[`syncApplication should delete old field and create equivalent one when
|
||||
"standardOverrides": null,
|
||||
"type": "TS_VECTOR",
|
||||
"universalIdentifier": Any<String>,
|
||||
"universalSettings": null,
|
||||
"universalSettings": {
|
||||
"asExpression": "to_tsvector('simple', COALESCE("id"::text, ''))",
|
||||
"generatedType": "STORED",
|
||||
},
|
||||
"updatedAt": Any<String>,
|
||||
},
|
||||
{
|
||||
@@ -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<String>,
|
||||
@@ -664,7 +667,10 @@ exports[`syncApplication should return workspace migration actions on initial sy
|
||||
"standardOverrides": null,
|
||||
"type": "TS_VECTOR",
|
||||
"universalIdentifier": Any<String>,
|
||||
"universalSettings": null,
|
||||
"universalSettings": {
|
||||
"asExpression": "to_tsvector('simple', COALESCE("id"::text, ''))",
|
||||
"generatedType": "STORED",
|
||||
},
|
||||
"updatedAt": Any<String>,
|
||||
},
|
||||
{
|
||||
|
||||
+133
@@ -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<ObjectManifest['fields'][number]>;
|
||||
}): Pick<Manifest, 'objects' | 'fields'> => {
|
||||
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({
|
||||
|
||||
+5
-1
@@ -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,
|
||||
|
||||
+1
-1
@@ -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": {
|
||||
|
||||
@@ -8,6 +8,7 @@ export type ObjectManifest = SyncableEntityOptions & {
|
||||
labelPlural: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
isSearchable?: boolean;
|
||||
fields: ObjectFieldManifest[];
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: string;
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { FieldMetadataType } from '@/types';
|
||||
|
||||
const SEARCHABLE_FIELD_TYPES = [
|
||||
FieldMetadataType.TEXT,
|
||||
FieldMetadataType.FULL_NAME,
|
||||
Reference in New Issue
Block a user