Fix REST API filters (#12929)

# Introduction
close https://github.com/twentyhq/twenty/issues/12921

### Done here:
- Removed
[check-order-by.utils.ts](https://github.com/twentyhq/twenty/pull/12929/files#diff-d044effc0b77b3b67523595ce0febd786d3a0fd74ae905ce2efc349134d7c7d0)
that was a duplicated
- new debug entry `twenty-server` entrypoint
- fixed the fields name computation in case of a relation field metadata
type
- Updated and refactored coverage both unit and integration


![image](https://github.com/user-attachments/assets/e3f0937a-8b54-4ab5-8348-0cd742c107ea)
This commit is contained in:
Paul Rastoin
2025-06-30 16:29:57 +02:00
committed by GitHub
parent 1b72d901a5
commit 3e7f2074e5
8 changed files with 594 additions and 123 deletions

21
.vscode/launch.json vendored
View File

@ -82,6 +82,27 @@
"env": { "env": {
"NODE_ENV": "test" "NODE_ENV": "test"
}, },
},
{
"type": "node",
"request": "launch",
"name": "twenty-server - debug unit test file (to launch with test file open)",
"runtimeExecutable": "npx",
"runtimeArgs": [
"nx",
"run",
"twenty-server:jest",
"--",
"--config",
"./jest.config.ts",
"${relativeFile}"
],
"cwd": "${workspaceFolder}/packages/twenty-server",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"env": {
"NODE_ENV": "test"
},
} }
] ]
} }

View File

@ -0,0 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`checkFields should accept valid relation field name 1`] = `"field 'pointOfContact' does not exist in 'opportunity' object"`;
exports[`checkFields should reject invalid field name 1`] = `"field 'wrongField' does not exist in 'opportunity' object"`;
exports[`checkFields should reject mix of valid and invalid field names 1`] = `"field 'wrongField' does not exist in 'opportunity' object"`;

View File

@ -1,76 +1,63 @@
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; import { EachTestingContext } from 'twenty-shared/testing';
import { import { OPPORTUNITY_WITH_FIELDS_MAPS } from 'src/engine/api/rest/core/query-builder/utils/__tests__/mocks/opportunity-field-maps.mock';
fieldNumberMock,
objectMetadataItemMock,
} from 'src/engine/api/__mocks__/object-metadata-item.mock';
import { checkFields } from 'src/engine/api/rest/core/query-builder/utils/check-fields.utils'; import { checkFields } from 'src/engine/api/rest/core/query-builder/utils/check-fields.utils';
import { checkArrayFields } from 'src/engine/api/rest/core/query-builder/utils/check-order-by.utils';
import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
describe('checkFields', () => { describe('checkFields', () => {
const completeFieldNumberMock: FieldMetadataInterface = { const testCases: EachTestingContext<{
id: 'field-number-id', fields: string[];
type: fieldNumberMock.type, shouldThrow?: boolean;
name: fieldNumberMock.name, }>[] = [
label: 'Field Number', {
objectMetadataId: 'object-metadata-id', title: 'should accept valid join column id',
isNullable: fieldNumberMock.isNullable, context: {
defaultValue: fieldNumberMock.defaultValue, fields: ['pointOfContactId'],
isLabelSyncedWithName: true, },
createdAt: new Date(),
updatedAt: new Date(),
};
const fieldsById: FieldMetadataMap = {
'field-number-id': completeFieldNumberMock,
};
const mockObjectMetadataWithFieldMaps = {
...objectMetadataItemMock,
fieldsById,
fieldIdByName: {
[completeFieldNumberMock.name]: completeFieldNumberMock.id,
}, },
fieldIdByJoinColumnName: {}, {
indexMetadatas: [], title: 'should accept valid relation field name',
}; context: {
shouldThrow: true,
fields: ['pointOfContact'],
},
},
{
title: 'should accept valid field name',
context: {
fields: ['position'],
},
},
{
title: 'should reject invalid field name',
context: {
fields: ['wrongField'],
shouldThrow: true,
},
},
{
title: 'should reject mix of valid and invalid field names',
context: {
fields: ['position', 'wrongField'],
shouldThrow: true,
},
},
{
title: 'should accept composite field',
context: {
fields: ['source'],
},
},
];
it('should check field types', () => { it.each(testCases)('$title', ({ context }) => {
expect(() => if (context.shouldThrow) {
checkFields(mockObjectMetadataWithFieldMaps, ['fieldNumber']), expect(() =>
).not.toThrow(); checkFields(OPPORTUNITY_WITH_FIELDS_MAPS, context.fields),
).toThrowErrorMatchingSnapshot();
expect(() => } else {
checkFields(mockObjectMetadataWithFieldMaps, ['wrongField']), expect(() =>
).toThrow(); checkFields(OPPORTUNITY_WITH_FIELDS_MAPS, context.fields),
).not.toThrow();
expect(() => }
checkFields(mockObjectMetadataWithFieldMaps, [
'fieldNumber',
'wrongField',
]),
).toThrow();
});
it('should check field types from array of fields', () => {
expect(() =>
checkArrayFields(mockObjectMetadataWithFieldMaps, [
{ fieldNumber: undefined },
]),
).not.toThrow();
expect(() =>
checkArrayFields(mockObjectMetadataWithFieldMaps, [
{ wrongField: undefined },
]),
).toThrow();
expect(() =>
checkArrayFields(mockObjectMetadataWithFieldMaps, [
{ fieldNumber: undefined },
{ wrongField: undefined },
]),
).toThrow();
}); });
}); });

View File

@ -0,0 +1,463 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
export const OPPORTUNITY_WITH_FIELDS_MAPS = {
id: '20202020-6e2c-42f6-a83c-cc58d776af88',
nameSingular: 'opportunity',
namePlural: 'opportunities',
labelSingular: 'Opportunity',
labelPlural: 'Opportunities',
description: 'An opportunity',
icon: 'IconTargetArrow',
targetTableName: 'DEPRECATED',
isCustom: false,
isRemote: false,
isActive: true,
isSystem: false,
isAuditLogged: true,
isSearchable: true,
labelIdentifierFieldMetadataId: '20202020-c2f1-4435-adca-22931f8b41b6',
imageIdentifierFieldMetadataId: null,
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
indexMetadatas: [], // unused
fieldsById: {
'20202020-c2f1-4435-adca-22931f8b41b6': {
id: '20202020-c2f1-4435-adca-22931f8b41b6',
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88',
type: FieldMetadataType.TEXT,
name: 'name',
label: 'Name',
defaultValue: "''",
description: 'The opportunity name',
icon: 'IconTargetArrow',
isCustom: false,
isActive: true,
isSystem: false,
isNullable: false,
isUnique: false,
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
isLabelSyncedWithName: true,
createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
},
'20202020-5eef-417a-b517-ebeedaa8e10b': {
id: '20202020-5eef-417a-b517-ebeedaa8e10b',
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88',
type: FieldMetadataType.CURRENCY,
name: 'amount',
label: 'Amount',
defaultValue: { amountMicros: null, currencyCode: "''" },
description: 'Opportunity amount',
icon: 'IconCurrencyDollar',
isCustom: false,
isActive: true,
isSystem: false,
isNullable: true,
isUnique: false,
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
isLabelSyncedWithName: true,
createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
},
'20202020-597c-44d3-98ec-ea71aea5256b': {
id: '20202020-597c-44d3-98ec-ea71aea5256b',
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88',
type: FieldMetadataType.DATE_TIME,
name: 'closeDate',
label: 'Close date',
defaultValue: null,
description: 'Opportunity close date',
icon: 'IconCalendarEvent',
isCustom: false,
isActive: true,
isSystem: false,
isNullable: true,
isUnique: false,
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
isLabelSyncedWithName: true,
createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
},
'20202020-9b94-454a-94ca-8afb09c68faf': {
id: '20202020-9b94-454a-94ca-8afb09c68faf',
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88',
type: FieldMetadataType.SELECT,
name: 'stage',
label: 'Stage',
defaultValue: "'NEW'",
description: 'Opportunity stage',
icon: 'IconProgressCheck',
options: [
{
id: '20202020-dba8-4975-81bd-b29a41c0a387',
color: 'red',
label: 'New',
value: 'NEW',
position: 0,
},
{
id: '20202020-1c9d-490c-940c-bf47addcd6a1',
color: 'purple',
label: 'Screening',
value: 'SCREENING',
position: 1,
},
{
id: '20202020-1368-438b-8702-6d5e5727a888',
color: 'sky',
label: 'Meeting',
value: 'MEETING',
position: 2,
},
{
id: '20202020-41e4-4b4e-a038-f02645f55767',
color: 'turquoise',
label: 'Proposal',
value: 'PROPOSAL',
position: 3,
},
{
id: '20202020-8acf-4934-8519-42f7c9133cd5',
color: 'yellow',
label: 'Customer',
value: 'CUSTOMER',
position: 4,
},
],
isCustom: false,
isActive: true,
isSystem: false,
isNullable: false,
isUnique: false,
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
isLabelSyncedWithName: true,
createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
},
'20202020-30a5-4d8e-9b93-12d31ece0aaa': {
id: '20202020-30a5-4d8e-9b93-12d31ece0aaa',
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88',
type: FieldMetadataType.POSITION,
name: 'position',
label: 'Position',
defaultValue: 0,
description: 'Opportunity record position',
icon: 'IconHierarchy2',
isCustom: false,
isActive: true,
isSystem: true,
isNullable: false,
isUnique: false,
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
isLabelSyncedWithName: true,
createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
},
'20202020-f95f-424f-ab32-65961e8e9635': {
id: '20202020-f95f-424f-ab32-65961e8e9635',
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88',
type: FieldMetadataType.ACTOR,
name: 'createdBy',
label: 'Created by',
defaultValue: { name: "'System'", source: "'MANUAL'", context: {} },
description: 'The creator of the record',
icon: 'IconCreativeCommonsSa',
isCustom: false,
isActive: true,
isSystem: false,
isNullable: false,
isUnique: false,
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
isLabelSyncedWithName: true,
createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
},
'20202020-5e10-4780-babb-38a465ac546c': {
id: '20202020-5e10-4780-babb-38a465ac546c',
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88',
type: FieldMetadataType.TEXT,
name: 'searchVector',
label: 'Search vector',
defaultValue: null,
description: 'Field used for full-text search',
icon: 'IconUser',
isCustom: false,
isActive: true,
isSystem: true,
isNullable: true,
isUnique: false,
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
isLabelSyncedWithName: true,
createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
},
'20202020-8f4a-4f8d-822e-90fe72f75b79': {
id: '20202020-8f4a-4f8d-822e-90fe72f75b79',
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88',
type: FieldMetadataType.UUID,
name: 'id',
label: 'Id',
defaultValue: 'uuid',
description: 'Id',
icon: 'Icon123',
isCustom: false,
isActive: true,
isSystem: true,
isNullable: false,
isUnique: false,
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
isLabelSyncedWithName: true,
createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
},
'20202020-f120-4b59-b239-f7f1d8eb243e': {
id: '20202020-f120-4b59-b239-f7f1d8eb243e',
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88',
type: FieldMetadataType.DATE_TIME,
name: 'createdAt',
label: 'Creation date',
defaultValue: 'now',
description: 'Creation date',
icon: 'IconCalendar',
settings: { displayFormat: 'RELATIVE' } as any,
isCustom: false,
isActive: true,
isSystem: false,
isNullable: false,
isUnique: false,
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
isLabelSyncedWithName: false,
createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
},
'20202020-dcc8-4318-9756-b87377692561': {
id: '20202020-dcc8-4318-9756-b87377692561',
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88',
type: FieldMetadataType.DATE_TIME,
name: 'updatedAt',
label: 'Last update',
defaultValue: 'now',
description: 'Last time the record was changed',
icon: 'IconCalendarClock',
settings: { displayFormat: 'RELATIVE' } as any,
isCustom: false,
isActive: true,
isSystem: false,
isNullable: false,
isUnique: false,
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
isLabelSyncedWithName: false,
createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
},
'20202020-1694-4f8b-8760-61a5ff330022': {
id: '20202020-1694-4f8b-8760-61a5ff330022',
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88',
type: FieldMetadataType.DATE_TIME,
name: 'deletedAt',
label: 'Deleted at',
defaultValue: null,
description: 'Date when the record was deleted',
icon: 'IconCalendarMinus',
settings: { displayFormat: 'RELATIVE' } as any,
isCustom: false,
isActive: true,
isSystem: false,
isNullable: true,
isUnique: false,
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
isLabelSyncedWithName: true,
createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
},
'20202020-4f52-4dea-a116-723f9bf7f082': {
id: '20202020-4f52-4dea-a116-723f9bf7f082',
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88',
type: FieldMetadataType.RELATION,
name: 'pointOfContact',
label: 'Point of Contact',
defaultValue: null,
description: 'Opportunity point of contact',
icon: 'IconUser',
settings: {
onDelete: 'SET_NULL',
relationType: 'MANY_TO_ONE',
joinColumnName: 'pointOfContactId',
} as any,
isCustom: false,
isActive: true,
isSystem: false,
isNullable: true,
isUnique: false,
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
isLabelSyncedWithName: true,
relationTargetFieldMetadataId: '20202020-a36b-4889-97d4-63a578423688',
relationTargetObjectMetadataId: '20202020-6799-4a38-92d3-8e844a7ea8ab',
createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
},
'20202020-fc02-4be2-be1a-e121daf5400d': {
id: '20202020-fc02-4be2-be1a-e121daf5400d',
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88',
type: FieldMetadataType.RELATION,
name: 'company',
label: 'Company',
defaultValue: null,
description: 'Opportunity company',
icon: 'IconBuildingSkyscraper',
settings: {
onDelete: 'SET_NULL',
relationType: 'MANY_TO_ONE',
joinColumnName: 'companyId',
} as any,
isCustom: false,
isActive: true,
isSystem: false,
isNullable: true,
isUnique: false,
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
isLabelSyncedWithName: true,
relationTargetFieldMetadataId: '20202020-bd16-4f63-8165-0a7f5d78170d',
relationTargetObjectMetadataId: '20202020-0be8-4764-8e0d-7a2e1c66f78c',
createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
},
'20202020-fd9f-48f0-bd5f-5b0fec6a5de4': {
id: '20202020-fd9f-48f0-bd5f-5b0fec6a5de4',
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88',
type: FieldMetadataType.RELATION,
name: 'favorites',
label: 'Favorites',
defaultValue: null,
description: 'Favorites linked to the opportunity',
icon: 'IconHeart',
settings: { relationType: RelationType.ONE_TO_MANY } as any,
isCustom: false,
isActive: true,
isSystem: true,
isNullable: true,
isUnique: false,
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
isLabelSyncedWithName: true,
relationTargetFieldMetadataId: '20202020-7c3c-4149-b400-5a958910c7a2',
relationTargetObjectMetadataId: '20202020-df10-42dc-baed-ea77db7ad96c',
createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
},
'20202020-88ab-4138-98ce-80533bb423e3': {
id: '20202020-88ab-4138-98ce-80533bb423e3',
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88',
type: FieldMetadataType.RELATION,
name: 'taskTargets',
label: 'Tasks',
defaultValue: null,
description: 'Tasks tied to the opportunity',
icon: 'IconCheckbox',
settings: { relationType: RelationType.ONE_TO_MANY } as any,
isCustom: false,
isActive: true,
isSystem: false,
isNullable: true,
isUnique: false,
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
isLabelSyncedWithName: false,
relationTargetFieldMetadataId: '20202020-eb77-4b1c-b6a6-d5dcd13b1634',
relationTargetObjectMetadataId: '20202020-3af1-4c4f-90f4-cd43c53f7f41',
createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
},
'20202020-4258-422b-b35b-db3f090af8da': {
id: '20202020-4258-422b-b35b-db3f090af8da',
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88',
type: FieldMetadataType.RELATION,
name: 'noteTargets',
label: 'Notes',
defaultValue: null,
description: 'Notes tied to the opportunity',
icon: 'IconNotes',
settings: { relationType: RelationType.ONE_TO_MANY } as any,
isCustom: false,
isActive: true,
isSystem: false,
isNullable: true,
isUnique: false,
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
isLabelSyncedWithName: false,
relationTargetFieldMetadataId: '20202020-d927-4c91-9893-28cea5aff979',
relationTargetObjectMetadataId: '20202020-8a5e-4dea-868b-71611e718a73',
createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
},
'20202020-16ca-40a7-a1ba-712975c916cd': {
id: '20202020-16ca-40a7-a1ba-712975c916cd',
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88',
type: FieldMetadataType.RELATION,
name: 'attachments',
label: 'Attachments',
defaultValue: null,
description: 'Attachments linked to the opportunity',
icon: 'IconFileImport',
settings: { relationType: RelationType.ONE_TO_MANY } as any,
isCustom: false,
isActive: true,
isSystem: false,
isNullable: true,
isUnique: false,
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
isLabelSyncedWithName: true,
relationTargetFieldMetadataId: '20202020-9236-427a-8a8a-a2296b93b542',
relationTargetObjectMetadataId: '20202020-3005-4c93-a04c-2941f7424f54',
createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
},
'20202020-92a5-47bf-a38d-c1c72b2c3e4d': {
id: '20202020-92a5-47bf-a38d-c1c72b2c3e4d',
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88',
type: FieldMetadataType.RELATION,
name: 'timelineActivities',
label: 'Timeline Activities',
defaultValue: null,
description: 'Timeline Activities linked to the opportunity.',
icon: 'IconTimelineEvent',
settings: { relationType: RelationType.ONE_TO_MANY } as any,
isCustom: false,
isActive: true,
isSystem: false,
isNullable: true,
isUnique: false,
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
isLabelSyncedWithName: true,
relationTargetFieldMetadataId: '20202020-2d54-41c7-b886-29deca3c28d5',
relationTargetObjectMetadataId: '20202020-a89e-4e4d-b3d9-c3f99e7c7483',
createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
},
},
fieldIdByName: {
name: '20202020-c2f1-4435-adca-22931f8b41b6',
amount: '20202020-5eef-417a-b517-ebeedaa8e10b',
closeDate: '20202020-597c-44d3-98ec-ea71aea5256b',
stage: '20202020-9b94-454a-94ca-8afb09c68faf',
position: '20202020-30a5-4d8e-9b93-12d31ece0aaa',
createdBy: '20202020-f95f-424f-ab32-65961e8e9635',
searchVector: '20202020-5e10-4780-babb-38a465ac546c',
id: '20202020-8f4a-4f8d-822e-90fe72f75b79',
createdAt: '20202020-f120-4b59-b239-f7f1d8eb243e',
updatedAt: '20202020-dcc8-4318-9756-b87377692561',
deletedAt: '20202020-1694-4f8b-8760-61a5ff330022',
pointOfContact: '20202020-4f52-4dea-a116-723f9bf7f082',
company: '20202020-fc02-4be2-be1a-e121daf5400d',
favorites: '20202020-fd9f-48f0-bd5f-5b0fec6a5de4',
taskTargets: '20202020-88ab-4138-98ce-80533bb423e3',
noteTargets: '20202020-4258-422b-b35b-db3f090af8da',
attachments: '20202020-16ca-40a7-a1ba-712975c916cd',
timelineActivities: '20202020-92a5-47bf-a38d-c1c72b2c3e4d',
},
fieldIdByJoinColumnName: {
pointOfContactId: '20202020-4f52-4dea-a116-723f9bf7f082',
companyId: '20202020-fc02-4be2-be1a-e121daf5400d',
},
} as const satisfies ObjectMetadataItemWithFieldMaps;

View File

@ -1,16 +1,22 @@
import { BadRequestException } from '@nestjs/common'; import { BadRequestException } from '@nestjs/common';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
import { isFieldMetadataInterfaceOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
export const checkFields = ( export const checkFields = (
objectMetadataItem: ObjectMetadataItemWithFieldMaps, objectMetadataItemWithFieldsMaps: ObjectMetadataItemWithFieldMaps,
fieldNames: string[], fieldNames: string[],
): void => { ): void => {
const fieldMetadataNames = Object.values(objectMetadataItem.fieldsById) const fieldMetadataNames: string[] = Object.values(
.map((field) => { objectMetadataItemWithFieldsMaps.fieldsById,
)
.flatMap((field) => {
if (isCompositeFieldMetadataType(field.type)) { if (isCompositeFieldMetadataType(field.type)) {
const compositeType = compositeTypeDefinitions.get(field.type); const compositeType = compositeTypeDefinitions.get(field.type);
@ -29,15 +35,19 @@ export const checkFields = (
].flat(); ].flat();
} }
if (isFieldMetadataInterfaceOfType(field, FieldMetadataType.RELATION)) {
return field.settings?.joinColumnName;
}
return field.name; return field.name;
}) })
.flat(); .filter(isDefined);
for (const fieldName of fieldNames) { for (const fieldName of fieldNames) {
if (!fieldMetadataNames.includes(fieldName)) { if (!fieldMetadataNames.includes(fieldName)) {
throw new BadRequestException( throw new BadRequestException(
`field '${fieldName}' does not exist in '${computeObjectTargetTable( `field '${fieldName}' does not exist in '${computeObjectTargetTable(
objectMetadataItem, objectMetadataItemWithFieldsMaps,
)}' object`, )}' object`,
); );
} }

View File

@ -1,48 +0,0 @@
import { BadRequestException } from '@nestjs/common';
import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
export const checkArrayFields = (
objectMetadataItem: ObjectMetadataItemWithFieldMaps,
fields: Array<Partial<ObjectRecord>>,
): void => {
const fieldMetadataNames = Object.values(objectMetadataItem.fieldsById)
.map((field) => {
if (isCompositeFieldMetadataType(field.type)) {
const compositeType = compositeTypeDefinitions.get(field.type);
if (!compositeType) {
throw new BadRequestException(
`Composite type '${field.type}' not found`,
);
}
return [
field.name,
compositeType.properties.map(
(compositeProperty) => compositeProperty.name,
),
].flat();
}
return field.name;
})
.flat();
for (const fieldObj of fields) {
for (const fieldName in fieldObj) {
if (!fieldMetadataNames.includes(fieldName)) {
throw new BadRequestException(
`field '${fieldName}' does not exist in '${computeObjectTargetTable(
objectMetadataItem,
)}' object`,
);
}
}
}
};

View File

@ -7,7 +7,7 @@ import {
OrderByDirection, OrderByDirection,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { checkArrayFields } from 'src/engine/api/rest/core/query-builder/utils/check-order-by.utils'; import { checkFields } from 'src/engine/api/rest/core/query-builder/utils/check-fields.utils';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
@ -82,7 +82,10 @@ export class OrderByInputFactory {
result = [...result, ...resultFields]; result = [...result, ...resultFields];
} }
checkArrayFields(objectMetadata.objectMetadataMapItem, result); checkFields(
objectMetadata.objectMetadataMapItem,
result.flatMap((fields) => Object.keys(fields)),
);
return this.addDefaultOrderById(result); return this.addDefaultOrderById(result);
} }

View File

@ -197,6 +197,34 @@ describe('Core REST API Find Many endpoint', () => {
); );
}); });
it('should support filtering on a relation field id', async () => {
const response = await makeRestAPIRequest({
method: 'get',
path: `/people?filter=companyId[in]:["${TEST_COMPANY_1_ID}"]`,
}).expect(200);
const filteredPeople = response.body.data.people;
expect(filteredPeople.length).toBeGreaterThan(0);
});
it('should fail to filter on a relation field name', async () => {
const response = await makeRestAPIRequest({
method: 'get',
path: `/people?filter=company[in]:["${TEST_COMPANY_1_ID}"]`,
});
expect(response.body).toMatchInlineSnapshot(`
{
"error": "BadRequestException",
"messages": [
"field 'company' does not exist in 'person' object",
],
"statusCode": 400,
}
`);
});
it('should support ordering Desc of results', async () => { it('should support ordering Desc of results', async () => {
const descResponse = await makeRestAPIRequest({ const descResponse = await makeRestAPIRequest({
method: 'get', method: 'get',