Fix filter rest api (#10537)
## Context Rest API is now using cache metadata but the change was not done everywhere which was breaking the API <img width="953" alt="Screenshot 2025-02-27 at 10 48 31" src="https://github.com/user-attachments/assets/84f549a6-59b5-4989-b1ba-2d5c515ee47c" />
This commit is contained in:
@ -2,12 +2,14 @@ import { Injectable } from '@nestjs/common';
|
|||||||
|
|
||||||
import { Request } from 'express';
|
import { Request } from 'express';
|
||||||
|
|
||||||
import { LimitInputFactory } from 'src/engine/api/rest/input-factories/limit-input.factory';
|
|
||||||
import { OrderByInputFactory } from 'src/engine/api/rest/input-factories/order-by-input.factory';
|
|
||||||
import { FilterInputFactory } from 'src/engine/api/rest/input-factories/filter-input.factory';
|
|
||||||
import { QueryVariables } from 'src/engine/api/rest/core/types/query-variables.type';
|
import { QueryVariables } from 'src/engine/api/rest/core/types/query-variables.type';
|
||||||
import { EndingBeforeInputFactory } from 'src/engine/api/rest/input-factories/ending-before-input.factory';
|
import { EndingBeforeInputFactory } from 'src/engine/api/rest/input-factories/ending-before-input.factory';
|
||||||
|
import { FilterInputFactory } from 'src/engine/api/rest/input-factories/filter-input.factory';
|
||||||
|
import { LimitInputFactory } from 'src/engine/api/rest/input-factories/limit-input.factory';
|
||||||
|
import { OrderByInputFactory } from 'src/engine/api/rest/input-factories/order-by-input.factory';
|
||||||
import { StartingAfterInputFactory } from 'src/engine/api/rest/input-factories/starting-after-input.factory';
|
import { StartingAfterInputFactory } from 'src/engine/api/rest/input-factories/starting-after-input.factory';
|
||||||
|
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';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GetVariablesFactory {
|
export class GetVariablesFactory {
|
||||||
@ -22,7 +24,10 @@ export class GetVariablesFactory {
|
|||||||
create(
|
create(
|
||||||
id: string | undefined,
|
id: string | undefined,
|
||||||
request: Request,
|
request: Request,
|
||||||
objectMetadata,
|
objectMetadata: {
|
||||||
|
objectMetadataMaps: ObjectMetadataMaps;
|
||||||
|
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
|
||||||
|
},
|
||||||
): QueryVariables {
|
): QueryVariables {
|
||||||
if (id) {
|
if (id) {
|
||||||
return { filter: { id: { eq: id } } };
|
return { filter: { id: { eq: id } } };
|
||||||
|
|||||||
@ -1,31 +1,70 @@
|
|||||||
import { objectMetadataItemMock } from 'src/engine/api/__mocks__/object-metadata-item.mock';
|
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||||
|
|
||||||
|
import {
|
||||||
|
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 { 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 = {
|
||||||
|
id: 'field-number-id',
|
||||||
|
type: fieldNumberMock.type,
|
||||||
|
name: fieldNumberMock.name,
|
||||||
|
label: 'Field Number',
|
||||||
|
objectMetadataId: 'object-metadata-id',
|
||||||
|
isNullable: fieldNumberMock.isNullable,
|
||||||
|
defaultValue: fieldNumberMock.defaultValue,
|
||||||
|
};
|
||||||
|
|
||||||
|
const fieldsById: FieldMetadataMap = {
|
||||||
|
'field-number-id': completeFieldNumberMock,
|
||||||
|
};
|
||||||
|
|
||||||
|
const fieldsByName: FieldMetadataMap = {
|
||||||
|
[completeFieldNumberMock.name]: completeFieldNumberMock,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockObjectMetadataWithFieldMaps = {
|
||||||
|
...objectMetadataItemMock,
|
||||||
|
fieldsById,
|
||||||
|
fieldsByName,
|
||||||
|
};
|
||||||
|
|
||||||
it('should check field types', () => {
|
it('should check field types', () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
checkFields(objectMetadataItemMock, ['fieldNumber']),
|
checkFields(mockObjectMetadataWithFieldMaps, ['fieldNumber']),
|
||||||
).not.toThrow();
|
).not.toThrow();
|
||||||
|
|
||||||
expect(() => checkFields(objectMetadataItemMock, ['wrongField'])).toThrow();
|
expect(() =>
|
||||||
|
checkFields(mockObjectMetadataWithFieldMaps, ['wrongField']),
|
||||||
|
).toThrow();
|
||||||
|
|
||||||
expect(() =>
|
expect(() =>
|
||||||
checkFields(objectMetadataItemMock, ['fieldNumber', 'wrongField']),
|
checkFields(mockObjectMetadataWithFieldMaps, [
|
||||||
|
'fieldNumber',
|
||||||
|
'wrongField',
|
||||||
|
]),
|
||||||
).toThrow();
|
).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should check field types from array of fields', () => {
|
it('should check field types from array of fields', () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
checkArrayFields(objectMetadataItemMock, [{ fieldNumber: undefined }]),
|
checkArrayFields(mockObjectMetadataWithFieldMaps, [
|
||||||
|
{ fieldNumber: undefined },
|
||||||
|
]),
|
||||||
).not.toThrow();
|
).not.toThrow();
|
||||||
|
|
||||||
expect(() =>
|
expect(() =>
|
||||||
checkArrayFields(objectMetadataItemMock, [{ wrongField: undefined }]),
|
checkArrayFields(mockObjectMetadataWithFieldMaps, [
|
||||||
|
{ wrongField: undefined },
|
||||||
|
]),
|
||||||
).toThrow();
|
).toThrow();
|
||||||
|
|
||||||
expect(() =>
|
expect(() =>
|
||||||
checkArrayFields(objectMetadataItemMock, [
|
checkArrayFields(mockObjectMetadataWithFieldMaps, [
|
||||||
{ fieldNumber: undefined },
|
{ fieldNumber: undefined },
|
||||||
{ wrongField: undefined },
|
{ wrongField: undefined },
|
||||||
]),
|
]),
|
||||||
|
|||||||
@ -1,12 +1,42 @@
|
|||||||
import { FieldMetadataType } from 'twenty-shared';
|
import { FieldMetadataType } from 'twenty-shared';
|
||||||
|
|
||||||
import { objectMetadataItemMock } from 'src/engine/api/__mocks__/object-metadata-item.mock';
|
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||||
|
|
||||||
|
import {
|
||||||
|
fieldNumberMock,
|
||||||
|
objectMetadataItemMock,
|
||||||
|
} from 'src/engine/api/__mocks__/object-metadata-item.mock';
|
||||||
import { getFieldType } from 'src/engine/api/rest/core/query-builder/utils/get-field-type.utils';
|
import { getFieldType } from 'src/engine/api/rest/core/query-builder/utils/get-field-type.utils';
|
||||||
|
import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
|
||||||
|
|
||||||
describe('getFieldType', () => {
|
describe('getFieldType', () => {
|
||||||
|
const completeFieldNumberMock: FieldMetadataInterface = {
|
||||||
|
id: 'field-number-id',
|
||||||
|
type: fieldNumberMock.type,
|
||||||
|
name: fieldNumberMock.name,
|
||||||
|
label: 'Field Number',
|
||||||
|
objectMetadataId: 'object-metadata-id',
|
||||||
|
isNullable: fieldNumberMock.isNullable,
|
||||||
|
defaultValue: fieldNumberMock.defaultValue,
|
||||||
|
};
|
||||||
|
|
||||||
|
const fieldsById: FieldMetadataMap = {
|
||||||
|
'field-number-id': completeFieldNumberMock,
|
||||||
|
};
|
||||||
|
|
||||||
|
const fieldsByName: FieldMetadataMap = {
|
||||||
|
[completeFieldNumberMock.name]: completeFieldNumberMock,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockObjectMetadataWithFieldMaps = {
|
||||||
|
...objectMetadataItemMock,
|
||||||
|
fieldsById,
|
||||||
|
fieldsByName,
|
||||||
|
};
|
||||||
|
|
||||||
it('should get field type', () => {
|
it('should get field type', () => {
|
||||||
expect(getFieldType(objectMetadataItemMock, 'fieldNumber')).toEqual(
|
expect(
|
||||||
FieldMetadataType.NUMBER,
|
getFieldType(mockObjectMetadataWithFieldMaps, 'fieldNumber'),
|
||||||
);
|
).toEqual(FieldMetadataType.NUMBER);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,16 +1,15 @@
|
|||||||
import { BadRequestException } from '@nestjs/common';
|
import { BadRequestException } from '@nestjs/common';
|
||||||
|
|
||||||
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
|
||||||
|
|
||||||
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 { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
||||||
|
|
||||||
export const checkFields = (
|
export const checkFields = (
|
||||||
objectMetadata: ObjectMetadataInterface,
|
objectMetadataItem: ObjectMetadataItemWithFieldMaps,
|
||||||
fieldNames: string[],
|
fieldNames: string[],
|
||||||
): void => {
|
): void => {
|
||||||
const fieldMetadataNames = objectMetadata.fields
|
const fieldMetadataNames = objectMetadataItem.fields
|
||||||
.map((field) => {
|
.map((field) => {
|
||||||
if (isCompositeFieldMetadataType(field.type)) {
|
if (isCompositeFieldMetadataType(field.type)) {
|
||||||
const compositeType = compositeTypeDefinitions.get(field.type);
|
const compositeType = compositeTypeDefinitions.get(field.type);
|
||||||
@ -38,7 +37,7 @@ export const checkFields = (
|
|||||||
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(
|
||||||
objectMetadata,
|
objectMetadataItem,
|
||||||
)}' object`,
|
)}' object`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +1,17 @@
|
|||||||
import { BadRequestException } from '@nestjs/common';
|
import { BadRequestException } from '@nestjs/common';
|
||||||
|
|
||||||
import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||||
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
|
||||||
|
|
||||||
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 { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
||||||
|
|
||||||
export const checkArrayFields = (
|
export const checkArrayFields = (
|
||||||
objectMetadata: ObjectMetadataInterface,
|
objectMetadataItem: ObjectMetadataItemWithFieldMaps,
|
||||||
fields: Array<Partial<ObjectRecord>>,
|
fields: Array<Partial<ObjectRecord>>,
|
||||||
): void => {
|
): void => {
|
||||||
const fieldMetadataNames = objectMetadata.fields
|
const fieldMetadataNames = objectMetadataItem.fields
|
||||||
.map((field) => {
|
.map((field) => {
|
||||||
if (isCompositeFieldMetadataType(field.type)) {
|
if (isCompositeFieldMetadataType(field.type)) {
|
||||||
const compositeType = compositeTypeDefinitions.get(field.type);
|
const compositeType = compositeTypeDefinitions.get(field.type);
|
||||||
@ -39,7 +39,7 @@ export const checkArrayFields = (
|
|||||||
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(
|
||||||
objectMetadata,
|
objectMetadataItem,
|
||||||
)}' object`,
|
)}' object`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,19 +1,47 @@
|
|||||||
import { FieldMetadataType } from 'twenty-shared';
|
import { FieldMetadataType } from 'twenty-shared';
|
||||||
|
|
||||||
|
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
fieldSelectMock,
|
fieldSelectMock,
|
||||||
objectMetadataItemMock,
|
objectMetadataItemMock,
|
||||||
} from 'src/engine/api/__mocks__/object-metadata-item.mock';
|
} from 'src/engine/api/__mocks__/object-metadata-item.mock';
|
||||||
import { checkFilterEnumValues } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/check-filter-enum-values';
|
import { checkFilterEnumValues } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/check-filter-enum-values';
|
||||||
|
import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
|
||||||
|
|
||||||
describe('checkFilterEnumValues', () => {
|
describe('checkFilterEnumValues', () => {
|
||||||
|
const completeFieldSelectMock: FieldMetadataInterface = {
|
||||||
|
id: 'field-select-id',
|
||||||
|
type: fieldSelectMock.type,
|
||||||
|
name: fieldSelectMock.name,
|
||||||
|
label: 'Field Select',
|
||||||
|
objectMetadataId: 'object-metadata-id',
|
||||||
|
isNullable: fieldSelectMock.isNullable,
|
||||||
|
defaultValue: fieldSelectMock.defaultValue,
|
||||||
|
options: fieldSelectMock.options,
|
||||||
|
};
|
||||||
|
|
||||||
|
const fieldsById: FieldMetadataMap = {
|
||||||
|
'field-select-id': completeFieldSelectMock,
|
||||||
|
};
|
||||||
|
|
||||||
|
const fieldsByName: FieldMetadataMap = {
|
||||||
|
[completeFieldSelectMock.name]: completeFieldSelectMock,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockObjectMetadataWithFieldMaps = {
|
||||||
|
...objectMetadataItemMock,
|
||||||
|
fieldsById,
|
||||||
|
fieldsByName,
|
||||||
|
};
|
||||||
|
|
||||||
it('should check properly', () => {
|
it('should check properly', () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
checkFilterEnumValues(
|
checkFilterEnumValues(
|
||||||
FieldMetadataType.SELECT,
|
FieldMetadataType.SELECT,
|
||||||
fieldSelectMock.name,
|
fieldSelectMock.name,
|
||||||
'OPTION_1',
|
'OPTION_1',
|
||||||
objectMetadataItemMock,
|
mockObjectMetadataWithFieldMaps,
|
||||||
),
|
),
|
||||||
).not.toThrow();
|
).not.toThrow();
|
||||||
|
|
||||||
@ -22,7 +50,7 @@ describe('checkFilterEnumValues', () => {
|
|||||||
FieldMetadataType.SELECT,
|
FieldMetadataType.SELECT,
|
||||||
fieldSelectMock.name,
|
fieldSelectMock.name,
|
||||||
'MISSING_OPTION',
|
'MISSING_OPTION',
|
||||||
objectMetadataItemMock,
|
mockObjectMetadataWithFieldMaps,
|
||||||
),
|
),
|
||||||
).toThrow(
|
).toThrow(
|
||||||
`'filter' enum value 'MISSING_OPTION' not available in '${fieldSelectMock.name}' enum. Available enum values are ['OPTION_1', 'OPTION_2']`,
|
`'filter' enum value 'MISSING_OPTION' not available in '${fieldSelectMock.name}' enum. Available enum values are ['OPTION_1', 'OPTION_2']`,
|
||||||
|
|||||||
@ -1,12 +1,55 @@
|
|||||||
import { objectMetadataItemMock } from 'src/engine/api/__mocks__/object-metadata-item.mock';
|
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||||
|
|
||||||
|
import {
|
||||||
|
fieldNumberMock,
|
||||||
|
fieldTextMock,
|
||||||
|
objectMetadataItemMock,
|
||||||
|
} from 'src/engine/api/__mocks__/object-metadata-item.mock';
|
||||||
import { parseFilter } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/parse-filter.utils';
|
import { parseFilter } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/parse-filter.utils';
|
||||||
|
import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
|
||||||
|
|
||||||
describe('parseFilter', () => {
|
describe('parseFilter', () => {
|
||||||
|
const completeFieldNumberMock: FieldMetadataInterface = {
|
||||||
|
id: 'field-number-id',
|
||||||
|
type: fieldNumberMock.type,
|
||||||
|
name: fieldNumberMock.name,
|
||||||
|
label: 'Field Number',
|
||||||
|
objectMetadataId: 'object-metadata-id',
|
||||||
|
isNullable: fieldNumberMock.isNullable,
|
||||||
|
defaultValue: fieldNumberMock.defaultValue,
|
||||||
|
};
|
||||||
|
|
||||||
|
const completeFieldTextMock: FieldMetadataInterface = {
|
||||||
|
id: 'field-text-id',
|
||||||
|
type: fieldTextMock.type,
|
||||||
|
name: fieldTextMock.name,
|
||||||
|
label: 'Field Text',
|
||||||
|
objectMetadataId: 'object-metadata-id',
|
||||||
|
isNullable: fieldTextMock.isNullable,
|
||||||
|
defaultValue: fieldTextMock.defaultValue,
|
||||||
|
};
|
||||||
|
|
||||||
|
const fieldsById: FieldMetadataMap = {
|
||||||
|
'field-number-id': completeFieldNumberMock,
|
||||||
|
'field-text-id': completeFieldTextMock,
|
||||||
|
};
|
||||||
|
|
||||||
|
const fieldsByName: FieldMetadataMap = {
|
||||||
|
[completeFieldNumberMock.name]: completeFieldNumberMock,
|
||||||
|
[completeFieldTextMock.name]: completeFieldTextMock,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockObjectMetadataWithFieldMaps = {
|
||||||
|
...objectMetadataItemMock,
|
||||||
|
fieldsById,
|
||||||
|
fieldsByName,
|
||||||
|
};
|
||||||
|
|
||||||
it('should parse string filter test 1', () => {
|
it('should parse string filter test 1', () => {
|
||||||
expect(
|
expect(
|
||||||
parseFilter(
|
parseFilter(
|
||||||
'and(fieldNumber[eq]:1,fieldNumber[eq]:2)',
|
'and(fieldNumber[eq]:1,fieldNumber[eq]:2)',
|
||||||
objectMetadataItemMock,
|
mockObjectMetadataWithFieldMaps,
|
||||||
),
|
),
|
||||||
).toEqual({
|
).toEqual({
|
||||||
and: [{ fieldNumber: { eq: 1 } }, { fieldNumber: { eq: 2 } }],
|
and: [{ fieldNumber: { eq: 1 } }, { fieldNumber: { eq: 2 } }],
|
||||||
@ -17,7 +60,7 @@ describe('parseFilter', () => {
|
|||||||
expect(
|
expect(
|
||||||
parseFilter(
|
parseFilter(
|
||||||
'and(fieldNumber[eq]:1,or(fieldNumber[eq]:2,fieldNumber[eq]:3))',
|
'and(fieldNumber[eq]:1,or(fieldNumber[eq]:2,fieldNumber[eq]:3))',
|
||||||
objectMetadataItemMock,
|
mockObjectMetadataWithFieldMaps,
|
||||||
),
|
),
|
||||||
).toEqual({
|
).toEqual({
|
||||||
and: [
|
and: [
|
||||||
@ -31,7 +74,7 @@ describe('parseFilter', () => {
|
|||||||
expect(
|
expect(
|
||||||
parseFilter(
|
parseFilter(
|
||||||
'and(fieldNumber[eq]:1,or(fieldNumber[eq]:2,fieldNumber[eq]:3,and(fieldNumber[eq]:6,fieldNumber[eq]:7)),or(fieldNumber[eq]:4,fieldNumber[eq]:5))',
|
'and(fieldNumber[eq]:1,or(fieldNumber[eq]:2,fieldNumber[eq]:3,and(fieldNumber[eq]:6,fieldNumber[eq]:7)),or(fieldNumber[eq]:4,fieldNumber[eq]:5))',
|
||||||
objectMetadataItemMock,
|
mockObjectMetadataWithFieldMaps,
|
||||||
),
|
),
|
||||||
).toEqual({
|
).toEqual({
|
||||||
and: [
|
and: [
|
||||||
@ -52,7 +95,7 @@ describe('parseFilter', () => {
|
|||||||
expect(
|
expect(
|
||||||
parseFilter(
|
parseFilter(
|
||||||
'and(fieldText[gt]:"val,ue",or(fieldNumber[is]:NOT_NULL,not(fieldText[startsWith]:"val"),and(fieldNumber[eq]:6,fieldText[ilike]:"%val%")),or(fieldNumber[eq]:4,fieldText[is]:NULL))',
|
'and(fieldText[gt]:"val,ue",or(fieldNumber[is]:NOT_NULL,not(fieldText[startsWith]:"val"),and(fieldNumber[eq]:6,fieldText[ilike]:"%val%")),or(fieldNumber[eq]:4,fieldText[is]:NULL))',
|
||||||
objectMetadataItemMock,
|
mockObjectMetadataWithFieldMaps,
|
||||||
),
|
),
|
||||||
).toEqual({
|
).toEqual({
|
||||||
and: [
|
and: [
|
||||||
@ -78,7 +121,7 @@ describe('parseFilter', () => {
|
|||||||
expect(
|
expect(
|
||||||
parseFilter(
|
parseFilter(
|
||||||
'and(fieldNumber[eq]:1,not(fieldNumber[eq]:2))',
|
'and(fieldNumber[eq]:1,not(fieldNumber[eq]:2))',
|
||||||
objectMetadataItemMock,
|
mockObjectMetadataWithFieldMaps,
|
||||||
),
|
),
|
||||||
).toEqual({
|
).toEqual({
|
||||||
and: [
|
and: [
|
||||||
|
|||||||
@ -2,13 +2,13 @@ import { BadRequestException } from '@nestjs/common';
|
|||||||
|
|
||||||
import { FieldMetadataType } from 'twenty-shared';
|
import { FieldMetadataType } from 'twenty-shared';
|
||||||
|
|
||||||
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||||
|
|
||||||
export const checkFilterEnumValues = (
|
export const checkFilterEnumValues = (
|
||||||
fieldType: FieldMetadataType | undefined,
|
fieldType: FieldMetadataType | undefined,
|
||||||
fieldName: string,
|
fieldName: string,
|
||||||
value: string,
|
value: string,
|
||||||
objectMetadataItem: ObjectMetadataInterface,
|
objectMetadataItem: ObjectMetadataItemWithFieldMaps,
|
||||||
): void => {
|
): void => {
|
||||||
if (
|
if (
|
||||||
!fieldType ||
|
!fieldType ||
|
||||||
@ -18,9 +18,7 @@ export const checkFilterEnumValues = (
|
|||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const field = objectMetadataItem.fields.find(
|
const field = objectMetadataItem.fieldsByName[fieldName];
|
||||||
(field) => field.name === fieldName,
|
|
||||||
);
|
|
||||||
|
|
||||||
const values = /^\[.*\]$/.test(value)
|
const values = /^\[.*\]$/.test(value)
|
||||||
? value.slice(1, -1).split(',')
|
? value.slice(1, -1).split(',')
|
||||||
|
|||||||
@ -1,14 +1,13 @@
|
|||||||
import { BadRequestException } from '@nestjs/common';
|
import { BadRequestException } from '@nestjs/common';
|
||||||
|
|
||||||
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
|
||||||
|
|
||||||
import { parseFilterContent } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/parse-filter-content.utils';
|
|
||||||
import { parseBaseFilter } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/parse-base-filter.utils';
|
|
||||||
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 { formatFieldValue } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/format-field-values.utils';
|
|
||||||
import { FieldValue } from 'src/engine/api/rest/core/types/field-value.type';
|
|
||||||
import { checkFilterEnumValues } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/check-filter-enum-values';
|
import { checkFilterEnumValues } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/check-filter-enum-values';
|
||||||
|
import { formatFieldValue } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/format-field-values.utils';
|
||||||
|
import { parseBaseFilter } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/parse-base-filter.utils';
|
||||||
|
import { parseFilterContent } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/parse-filter-content.utils';
|
||||||
import { getFieldType } from 'src/engine/api/rest/core/query-builder/utils/get-field-type.utils';
|
import { getFieldType } from 'src/engine/api/rest/core/query-builder/utils/get-field-type.utils';
|
||||||
|
import { FieldValue } from 'src/engine/api/rest/core/types/field-value.type';
|
||||||
|
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||||
|
|
||||||
export enum Conjunctions {
|
export enum Conjunctions {
|
||||||
or = 'or',
|
or = 'or',
|
||||||
@ -18,7 +17,7 @@ export enum Conjunctions {
|
|||||||
|
|
||||||
export const parseFilter = (
|
export const parseFilter = (
|
||||||
filterQuery: string,
|
filterQuery: string,
|
||||||
objectMetadataItem: ObjectMetadataInterface,
|
objectMetadataItem: ObjectMetadataItemWithFieldMaps,
|
||||||
): Record<string, FieldValue> => {
|
): Record<string, FieldValue> => {
|
||||||
const result = {};
|
const result = {};
|
||||||
const match = filterQuery.match(
|
const match = filterQuery.match(
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import { FieldMetadataType } from 'twenty-shared';
|
import { FieldMetadataType } from 'twenty-shared';
|
||||||
|
|
||||||
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||||
|
|
||||||
export const getFieldType = (
|
export const getFieldType = (
|
||||||
objectMetadata: ObjectMetadataInterface,
|
objectMetadataItem: ObjectMetadataItemWithFieldMaps,
|
||||||
fieldName: string,
|
fieldName: string,
|
||||||
): FieldMetadataType | undefined => {
|
): FieldMetadataType | undefined => {
|
||||||
return objectMetadata.fields.find((field) => field.name === fieldName)?.type;
|
return objectMetadataItem.fieldsByName[fieldName]?.type;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,10 +1,79 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
|
||||||
import { objectMetadataItemMock } from 'src/engine/api/__mocks__/object-metadata-item.mock';
|
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||||
|
|
||||||
|
import {
|
||||||
|
fieldCurrencyMock,
|
||||||
|
fieldNumberMock,
|
||||||
|
fieldTextMock,
|
||||||
|
objectMetadataItemMock,
|
||||||
|
} from 'src/engine/api/__mocks__/object-metadata-item.mock';
|
||||||
import { FilterInputFactory } from 'src/engine/api/rest/input-factories/filter-input.factory';
|
import { FilterInputFactory } from 'src/engine/api/rest/input-factories/filter-input.factory';
|
||||||
|
import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
|
||||||
|
|
||||||
describe('FilterInputFactory', () => {
|
describe('FilterInputFactory', () => {
|
||||||
const objectMetadata = { objectMetadataItem: objectMetadataItemMock };
|
const completeFieldNumberMock: FieldMetadataInterface = {
|
||||||
|
id: 'field-number-id',
|
||||||
|
type: fieldNumberMock.type,
|
||||||
|
name: fieldNumberMock.name,
|
||||||
|
label: 'Field Number',
|
||||||
|
objectMetadataId: 'object-metadata-id',
|
||||||
|
isNullable: fieldNumberMock.isNullable,
|
||||||
|
defaultValue: fieldNumberMock.defaultValue,
|
||||||
|
};
|
||||||
|
|
||||||
|
const completeFieldTextMock: FieldMetadataInterface = {
|
||||||
|
id: 'field-text-id',
|
||||||
|
type: fieldTextMock.type,
|
||||||
|
name: fieldTextMock.name,
|
||||||
|
label: 'Field Text',
|
||||||
|
objectMetadataId: 'object-metadata-id',
|
||||||
|
isNullable: fieldTextMock.isNullable,
|
||||||
|
defaultValue: fieldTextMock.defaultValue,
|
||||||
|
};
|
||||||
|
|
||||||
|
const completeFieldCurrencyMock: FieldMetadataInterface = {
|
||||||
|
id: 'field-currency-id',
|
||||||
|
type: fieldCurrencyMock.type,
|
||||||
|
name: fieldCurrencyMock.name,
|
||||||
|
label: 'Field Currency',
|
||||||
|
objectMetadataId: 'object-metadata-id',
|
||||||
|
isNullable: fieldCurrencyMock.isNullable,
|
||||||
|
defaultValue: fieldCurrencyMock.defaultValue,
|
||||||
|
};
|
||||||
|
|
||||||
|
const fieldsById: FieldMetadataMap = {
|
||||||
|
'field-number-id': completeFieldNumberMock,
|
||||||
|
'field-text-id': completeFieldTextMock,
|
||||||
|
'field-currency-id': completeFieldCurrencyMock,
|
||||||
|
};
|
||||||
|
|
||||||
|
const fieldsByName: FieldMetadataMap = {
|
||||||
|
[completeFieldNumberMock.name]: completeFieldNumberMock,
|
||||||
|
[completeFieldTextMock.name]: completeFieldTextMock,
|
||||||
|
[completeFieldCurrencyMock.name]: completeFieldCurrencyMock,
|
||||||
|
};
|
||||||
|
|
||||||
|
const objectMetadataMapItem = {
|
||||||
|
...objectMetadataItemMock,
|
||||||
|
fieldsById,
|
||||||
|
fieldsByName,
|
||||||
|
};
|
||||||
|
|
||||||
|
const objectMetadataMaps = {
|
||||||
|
byId: {
|
||||||
|
[objectMetadataItemMock.id || 'mock-id']: objectMetadataMapItem,
|
||||||
|
},
|
||||||
|
idByNameSingular: {
|
||||||
|
[objectMetadataItemMock.nameSingular]:
|
||||||
|
objectMetadataItemMock.id || 'mock-id',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const objectMetadata = {
|
||||||
|
objectMetadataMaps,
|
||||||
|
objectMetadataMapItem,
|
||||||
|
};
|
||||||
|
|
||||||
let service: FilterInputFactory;
|
let service: FilterInputFactory;
|
||||||
|
|
||||||
|
|||||||
@ -6,10 +6,18 @@ import { addDefaultConjunctionIfMissing } from 'src/engine/api/rest/core/query-b
|
|||||||
import { checkFilterQuery } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/check-filter-query.utils';
|
import { checkFilterQuery } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/check-filter-query.utils';
|
||||||
import { parseFilter } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/parse-filter.utils';
|
import { parseFilter } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/parse-filter.utils';
|
||||||
import { FieldValue } from 'src/engine/api/rest/core/types/field-value.type';
|
import { FieldValue } from 'src/engine/api/rest/core/types/field-value.type';
|
||||||
|
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';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class FilterInputFactory {
|
export class FilterInputFactory {
|
||||||
create(request: Request, objectMetadata): Record<string, FieldValue> {
|
create(
|
||||||
|
request: Request,
|
||||||
|
objectMetadata: {
|
||||||
|
objectMetadataMaps: ObjectMetadataMaps;
|
||||||
|
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
|
||||||
|
},
|
||||||
|
): Record<string, FieldValue> {
|
||||||
let filterQuery = request.query.filter;
|
let filterQuery = request.query.filter;
|
||||||
|
|
||||||
if (typeof filterQuery !== 'string') {
|
if (typeof filterQuery !== 'string') {
|
||||||
@ -20,6 +28,6 @@ export class FilterInputFactory {
|
|||||||
|
|
||||||
filterQuery = addDefaultConjunctionIfMissing(filterQuery);
|
filterQuery = addDefaultConjunctionIfMissing(filterQuery);
|
||||||
|
|
||||||
return parseFilter(filterQuery, objectMetadata.objectMetadataItem);
|
return parseFilter(filterQuery, objectMetadata.objectMetadataMapItem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user