Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ describe('getOperatorsForFieldType', () => {
expect(result).toContain('isEmptyArray');
});

it('allows the full-text search operator only for TS_VECTOR', () => {
expect(getOperatorsForFieldType(FieldMetadataType.TS_VECTOR)).toEqual([
'search',
]);
expect(getOperatorsForFieldType(FieldMetadataType.TEXT)).not.toContain(
'search',
);
});

it.each([FieldMetadataType.SELECT, FieldMetadataType.RATING])(
'should allow enum operators and ordering operators for %s',
(fieldType) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ export const getOperatorsForFieldType = (
return RICH_TEXT_FILTER_OPERATORS;

case FieldMetadataType.TS_VECTOR:
return ['search'];

case FieldMetadataType.ACTOR:
case FieldMetadataType.ADDRESS:
case FieldMetadataType.CURRENCY:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,15 @@ describe('parseBaseFilter', () => {
value: '["2023-12-01T14:23:23.914Z","2024-12-01T14:23:23.914Z"]',
});
});

it('accepts the search comparator only for the TS_VECTOR searchVector field', () => {
expect(parseBaseFilter('searchVector[search]:custom value')).toEqual({
fields: ['searchVector'],
comparator: 'search',
value: 'custom value',
});
expect(() => parseBaseFilter('name[search]:custom value')).toThrow(
'comparator search is only supported on searchVector',
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export enum FilterComparators {
endsWith = 'endsWith',
like = 'like',
ilike = 'ilike',
search = 'search',

// Not handled rigth now
// regex = 'regex',
Expand Down Expand Up @@ -65,5 +66,14 @@ export const parseBaseFilter = (
);
}

// REST parsing runs before object metadata is available. `searchVector` is the
// sole system TS_VECTOR field exposed by this grammar; keep `search` confined
// to it so scalar/custom JSON fields can never opt into the TS query operator.
if (comparator === FilterComparators.search && fields !== 'searchVector') {
throw new BadRequestException(
`'filter' invalid for '${baseFilter}', comparator search is only supported on searchVector`,
);
}

return { fields: fields.split('.'), comparator, value };
};
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { type Expect, type HasAllProperties } from 'twenty-shared/testing';
import {
type AllFieldMetadataSettings,
type FieldMetadataDefaultValueForAnyType,
type FieldMetadataDefaultValueMapping,
type FieldMetadataOptionForAnyType,
type FieldMetadataSettingsMapping,
type FieldMetadataSettings,
type FieldMetadataType,
type NullablePartial,
type RegieCustomFieldSettings,
} from 'twenty-shared/types';
import { type Relation as TypeOrmRelation } from 'typeorm';

Expand Down Expand Up @@ -108,29 +108,27 @@ type RelationAssertions = [
>,
];

type NotDefinedSettings = {
settings: never | null;
type RegieMarkerSettings = {
settings: JsonbProperty<RegieCustomFieldSettings | null>;
};

// oxlint-disable-next-line unused-imports/no-unused-vars
type SettingsAssertions = [
Expect<HasAllProperties<FullNameFieldMetadata, NotDefinedSettings>>,
Expect<HasAllProperties<RatingFieldMetadata, NotDefinedSettings>>,
Expect<HasAllProperties<SelectFieldMetadata, NotDefinedSettings>>,
Expect<HasAllProperties<MultiSelectFieldMetadata, NotDefinedSettings>>,
Expect<HasAllProperties<PositionFieldMetadata, NotDefinedSettings>>,
Expect<HasAllProperties<RawJsonFieldMetadata, NotDefinedSettings>>,
Expect<HasAllProperties<ActorFieldMetadata, NotDefinedSettings>>,
Expect<HasAllProperties<UUIDFieldMetadata, NotDefinedSettings>>,
Expect<HasAllProperties<BooleanFieldMetadata, NotDefinedSettings>>,
Expect<HasAllProperties<FullNameFieldMetadata, RegieMarkerSettings>>,
Expect<HasAllProperties<RatingFieldMetadata, RegieMarkerSettings>>,
Expect<HasAllProperties<SelectFieldMetadata, RegieMarkerSettings>>,
Expect<HasAllProperties<MultiSelectFieldMetadata, RegieMarkerSettings>>,
Expect<HasAllProperties<PositionFieldMetadata, RegieMarkerSettings>>,
Expect<HasAllProperties<RawJsonFieldMetadata, RegieMarkerSettings>>,
Expect<HasAllProperties<ActorFieldMetadata, RegieMarkerSettings>>,
Expect<HasAllProperties<UUIDFieldMetadata, RegieMarkerSettings>>,
Expect<HasAllProperties<BooleanFieldMetadata, RegieMarkerSettings>>,

Expect<
HasAllProperties<
TextFieldMetadata,
{
settings: JsonbProperty<
FieldMetadataSettingsMapping[FieldMetadataType.TEXT]
>;
settings: JsonbProperty<FieldMetadataSettings<FieldMetadataType.TEXT>>;
}
>
>,
Expand All @@ -139,7 +137,7 @@ type SettingsAssertions = [
NumberFieldMetadata,
{
settings: JsonbProperty<
FieldMetadataSettingsMapping[FieldMetadataType.NUMBER]
FieldMetadataSettings<FieldMetadataType.NUMBER>
>;
}
>
Expand All @@ -149,7 +147,7 @@ type SettingsAssertions = [
CurrencyFieldMetadata,
{
settings: JsonbProperty<
FieldMetadataSettingsMapping[FieldMetadataType.CURRENCY]
FieldMetadataSettings<FieldMetadataType.CURRENCY>
>;
}
>
Expand All @@ -158,9 +156,7 @@ type SettingsAssertions = [
HasAllProperties<
DateFieldMetadata,
{
settings: JsonbProperty<
FieldMetadataSettingsMapping[FieldMetadataType.DATE]
>;
settings: JsonbProperty<FieldMetadataSettings<FieldMetadataType.DATE>>;
}
>
>,
Expand All @@ -169,7 +165,7 @@ type SettingsAssertions = [
DateTimeFieldMetadata,
{
settings: JsonbProperty<
FieldMetadataSettingsMapping[FieldMetadataType.DATE_TIME]
FieldMetadataSettings<FieldMetadataType.DATE_TIME>
>;
}
>
Expand All @@ -178,9 +174,7 @@ type SettingsAssertions = [
HasAllProperties<
ArrayFieldMetadata,
{
settings: JsonbProperty<
FieldMetadataSettingsMapping[FieldMetadataType.ARRAY]
>;
settings: JsonbProperty<FieldMetadataSettings<FieldMetadataType.ARRAY>>;
}
>
>,
Expand All @@ -189,7 +183,7 @@ type SettingsAssertions = [
PhonesFieldMetadata,
{
settings: JsonbProperty<
FieldMetadataSettingsMapping[FieldMetadataType.PHONES]
FieldMetadataSettings<FieldMetadataType.PHONES>
>;
}
>
Expand All @@ -199,7 +193,7 @@ type SettingsAssertions = [
EmailsFieldMetadata,
{
settings: JsonbProperty<
FieldMetadataSettingsMapping[FieldMetadataType.EMAILS]
FieldMetadataSettings<FieldMetadataType.EMAILS>
>;
}
>
Expand All @@ -208,9 +202,7 @@ type SettingsAssertions = [
HasAllProperties<
LinksFieldMetadata,
{
settings: JsonbProperty<
FieldMetadataSettingsMapping[FieldMetadataType.LINKS]
>;
settings: JsonbProperty<FieldMetadataSettings<FieldMetadataType.LINKS>>;
}
>
>,
Expand All @@ -220,7 +212,7 @@ type SettingsAssertions = [
RelationFieldMetadata,
{
settings: JsonbProperty<
FieldMetadataSettingsMapping[FieldMetadataType.RELATION]
FieldMetadataSettings<FieldMetadataType.RELATION>
>;
}
>
Expand All @@ -230,7 +222,7 @@ type SettingsAssertions = [
MorphRelationFieldMetadata,
{
settings: JsonbProperty<
FieldMetadataSettingsMapping[FieldMetadataType.MORPH_RELATION]
FieldMetadataSettings<FieldMetadataType.MORPH_RELATION>
>;
}
>
Expand All @@ -239,7 +231,7 @@ type SettingsAssertions = [
Expect<
HasAllProperties<
AbstractFieldMetadata,
{ settings: JsonbProperty<AllFieldMetadataSettings> | null }
{ settings: JsonbProperty<FieldMetadataSettings> | null }
>
>,
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,3 +305,40 @@ describe('fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test s
);
});
});

describe('Regie marker preservation for enum field creation', () => {
const marker = {
twentyOwnedSibling: 'preserve-me',
regieCustomField: {
version: 1 as const,
target: 'account' as const,
format: 'plain' as const,
searchable: true,
},
};

it.each([FieldMetadataType.SELECT, FieldMetadataType.MULTI_SELECT])(
'preserves the full marker in %s universal settings',
async (type) => {
const result = await fromCreateFieldInputToFlatFieldMetadatasToCreate({
flatApplication: MOCK_FLAT_APPLICATION,
createFieldInput: {
name: 'regieEnum',
label: 'Regie Enum',
type,
objectMetadataId: COMPANY_FLAT_OBJECT_MOCK.id,
settings: marker,
options: [{ label: 'One', position: 0, value: 'ONE' }],
},
flatObjectMetadataMaps,
flatFieldMetadataMaps: emptyFlatFieldMetadataMaps,
});

expect(result.status).toBe('success');
if (result.status !== 'success') throw new Error('expected success');
expect(result.result.flatFieldMetadatas[0].universalSettings).toEqual(
marker,
);
},
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { FieldMetadataType } from 'twenty-shared/types';

import { computeFlatFieldToUpdateAndRelatedFlatFieldToUpdate } from 'src/engine/metadata-modules/flat-field-metadata/utils/compute-flat-field-to-update-and-related-flat-field-to-update.util';

const marker = {
version: 1 as const,
target: 'person' as const,
format: 'plain' as const,
searchable: true,
};

describe('Regie marker settings updates', () => {
it.each([FieldMetadataType.SELECT, FieldMetadataType.MULTI_SELECT])(
'merges marker and sibling settings for %s updates',
(type) => {
const fromFlatFieldMetadata = {
id: 'field-id',
universalIdentifier: 'field-uid',
type,
isCustom: true,
settings: {
twentyOwnedSibling: 'retain',
regieCustomField: marker,
},
};

const result = computeFlatFieldToUpdateAndRelatedFlatFieldToUpdate({
fromFlatFieldMetadata: fromFlatFieldMetadata as never,
rawUpdateFieldInput: {
id: 'field-id',
settings: { twentyOwnedSibling: 'updated' },
} as never,
flatFieldMetadataMaps: {} as never,
flatObjectMetadata: {} as never,
isSystemBuild: false,
});

expect(
result.flatFieldMetadataFromTo.toFlatFieldMetadata.settings,
).toEqual({
twentyOwnedSibling: 'updated',
regieCustomField: marker,
});
expect(
result.flatFieldMetadataFromTo.toFlatFieldMetadata.universalSettings,
).toEqual({
twentyOwnedSibling: 'updated',
regieCustomField: marker,
});
},
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,36 @@ import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object
import { belongsToTwentyStandardApp } from 'src/engine/metadata-modules/utils/belongs-to-twenty-standard-app.util';
import { mergeUpdateInExistingRecord } from 'src/utils/merge-update-in-existing-record.util';

const hasRegieCustomFieldSetting = (settings: unknown): boolean =>
typeof settings === 'object' &&
settings !== null &&
Object.prototype.hasOwnProperty.call(settings, 'regieCustomField');

const mergeRegieCustomFieldSettings = <Settings>(
existingSettings: Settings,
updatedSettings: Settings,
): Settings => {
// Settings are normally replaced by a field update. A Regie marker is a
// namespaced extension, though, so a partial settings update must not erase
// either the marker or sibling Twenty-owned settings.
if (
!hasRegieCustomFieldSetting(existingSettings) &&
!hasRegieCustomFieldSetting(updatedSettings)
) {
return updatedSettings;
}
if (
typeof existingSettings !== 'object' ||
existingSettings === null ||
typeof updatedSettings !== 'object' ||
updatedSettings === null
) {
return updatedSettings;
}

return { ...existingSettings, ...updatedSettings } as Settings;
};

type ComputeFlatFieldToUpdateAndRelatedFlatFieldToUpdateReturnType = {
flatFieldMetadataFromTo: FromTo<FlatFieldMetadata, 'flatFieldMetadata'>;
relatedFlatFieldMetadatasFromTo: FromTo<
Expand Down Expand Up @@ -66,7 +96,10 @@ export const computeFlatFieldToUpdateAndRelatedFlatFieldToUpdate = ({
};

if (updatedEditableFieldProperties.settings !== undefined) {
const updatedSettings = toFlatFieldMetadata.settings;
const updatedSettings = mergeRegieCustomFieldSettings(
fromFlatFieldMetadata.settings,
toFlatFieldMetadata.settings,
);

const isRelationSettings =
isFieldMetadataSettingsOfType(
Expand Down Expand Up @@ -102,6 +135,8 @@ export const computeFlatFieldToUpdateAndRelatedFlatFieldToUpdate = ({
} else {
toFlatFieldMetadata.universalSettings = updatedSettings;
}

toFlatFieldMetadata.settings = updatedSettings;
}

if (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ export const fromCreateFieldInputToFlatFieldMetadatasToCreate = async ({
type: createFieldInput.type,
options,
defaultValue: commonFlatFieldMetadata.defaultValue as string, // Could this be improved ?
universalSettings: null,
universalSettings: commonFlatFieldMetadata.universalSettings,
} satisfies UniversalFlatFieldMetadata<
typeof createFieldInput.type
>,
Expand Down
Loading
Loading