Fix multi select filtering (#5358) (#8452)

Allow filtering by multi-select fields.

<img width="1053" alt="Screenshot 2024-11-11 at 11 54 45"
src="https://github.com/user-attachments/assets/a79b2251-94e3-48f8-abda-e808103a6c39">

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
ad-elias 2024-11-17 15:27:38 +01:00 committed by GitHub
parent ac1197afe1
commit badebc513f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 408 additions and 177 deletions

View File

@ -61,7 +61,7 @@ describe('useColumnDefinitionsFromFieldMetadata', () => {
result.current;
expect(columnDefinitions.length).toBe(21);
expect(filterDefinitions.length).toBe(14);
expect(filterDefinitions.length).toBe(15);
expect(sortDefinitions.length).toBe(14);
});
});

View File

@ -35,6 +35,7 @@ export const formatFieldMetadataItemsAsFilterDefinitions = ({
FieldMetadataType.Address,
FieldMetadataType.Relation,
FieldMetadataType.Select,
FieldMetadataType.MultiSelect,
FieldMetadataType.Currency,
FieldMetadataType.Rating,
FieldMetadataType.Actor,

View File

@ -21,11 +21,7 @@ export type BooleanFilter = {
export type StringFilter = {
eq?: string;
gt?: string;
gte?: string;
in?: string[];
lt?: string;
lte?: string;
neq?: string;
startsWith?: string;
like?: string;
@ -35,6 +31,12 @@ export type StringFilter = {
is?: IsFilter;
};
export type RatingFilter = {
eq?: string;
in?: string[];
is?: IsFilter;
};
export type FloatFilter = {
eq?: number;
gt?: number;
@ -104,10 +106,21 @@ export type PhonesFilter = {
primaryPhoneCountryCode?: StringFilter;
};
export type ArrayFilter = {
contains?: string[];
not_contains?: string[];
export type SelectFilter = {
is?: IsFilter;
in?: string[];
};
export type MultiSelectFilter = {
is?: IsFilter;
isEmptyArray?: boolean;
containsAny?: string[];
};
export type ArrayFilter = {
is?: IsFilter;
isEmptyArray?: boolean;
containsIlike?: string;
};
export type RawJsonFilter = {

View File

@ -93,7 +93,9 @@ export const ObjectFilterDropdownFilterInput = ({
<ObjectFilterDropdownSourceSelect />
</>
)}
{filterDefinitionUsedInDropdown.type === 'SELECT' && (
{['SELECT', 'MULTI_SELECT'].includes(
filterDefinitionUsedInDropdown.type,
) && (
<>
<ObjectFilterDropdownSearchInput />
<ObjectFilterDropdownOptionSelect />

View File

@ -58,8 +58,14 @@ export const getOperandsForFilterDefinition = (
];
case 'RELATION':
return [...relationOperands, ...emptyOperands];
case 'MULTI_SELECT':
return [
ViewFilterOperand.Contains,
ViewFilterOperand.DoesNotContain,
...emptyOperands,
];
case 'SELECT':
return [...relationOperands];
return [ViewFilterOperand.Is, ViewFilterOperand.IsNot, ...emptyOperands];
case 'ACTOR': {
if (isActorSourceCompositeFilter(filterDefinition)) {
return [

View File

@ -158,62 +158,6 @@ describe('isMatchingStringFilter', () => {
});
});
describe('gt', () => {
it('value is greater than gt filter', () => {
expect(
isMatchingStringFilter({ stringFilter: { gt: 'a' }, value: 'b' }),
).toBe(true);
});
it('value is not greater than gt filter', () => {
expect(
isMatchingStringFilter({ stringFilter: { gt: 'b' }, value: 'a' }),
).toBe(false);
});
});
describe('gte', () => {
it('value is greater than or equal to gte filter', () => {
expect(
isMatchingStringFilter({ stringFilter: { gte: 'a' }, value: 'a' }),
).toBe(true);
});
it('value is not greater than or equal to gte filter', () => {
expect(
isMatchingStringFilter({ stringFilter: { gte: 'b' }, value: 'a' }),
).toBe(false);
});
});
describe('lt', () => {
it('value is less than lt filter', () => {
expect(
isMatchingStringFilter({ stringFilter: { lt: 'b' }, value: 'a' }),
).toBe(true);
});
it('value is not less than lt filter', () => {
expect(
isMatchingStringFilter({ stringFilter: { lt: 'a' }, value: 'b' }),
).toBe(false);
});
});
describe('lte', () => {
it('value is less than or equal to lte filter', () => {
expect(
isMatchingStringFilter({ stringFilter: { lte: 'a' }, value: 'a' }),
).toBe(true);
});
it('value is not less than or equal to lte filter', () => {
expect(
isMatchingStringFilter({ stringFilter: { lte: 'a' }, value: 'b' }),
).toBe(false);
});
});
describe('startsWith', () => {
it('value starts with the startsWith filter', () => {
expect(

View File

@ -3,15 +3,18 @@ import { isNonEmptyString } from '@sniptt/guards';
import {
ActorFilter,
AddressFilter,
ArrayFilter,
CurrencyFilter,
DateFilter,
EmailsFilter,
FloatFilter,
MultiSelectFilter,
RatingFilter,
RawJsonFilter,
RecordGqlOperationFilter,
RelationFilter,
SelectFilter,
StringFilter,
UUIDFilter,
} from '@/object-record/graphql/types/RecordGqlOperationFilter';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { Field } from '~/generated/graphql';
@ -241,7 +244,7 @@ const computeFilterRecordGqlOperationFilter = (
return {
[correspondingField.name]: {
eq: convertRatingToRatingValue(parseFloat(filter.value)),
} as StringFilter,
} as RatingFilter,
};
case ViewFilterOperand.GreaterThan:
return {
@ -249,7 +252,7 @@ const computeFilterRecordGqlOperationFilter = (
in: convertGreaterThanRatingToArrayOfRatingValues(
parseFloat(filter.value),
),
} as StringFilter,
} as RatingFilter,
};
case ViewFilterOperand.LessThan:
return {
@ -257,7 +260,7 @@ const computeFilterRecordGqlOperationFilter = (
in: convertLessThanRatingToArrayOfRatingValues(
parseFloat(filter.value),
),
} as StringFilter,
} as RatingFilter,
};
case ViewFilterOperand.IsEmpty:
case ViewFilterOperand.IsNotEmpty:
@ -309,30 +312,28 @@ const computeFilterRecordGqlOperationFilter = (
const parsedRecordIds = JSON.parse(filter.value) as string[];
if (parsedRecordIds.length > 0) {
switch (filter.operand) {
case ViewFilterOperand.Is:
return {
if (parsedRecordIds.length === 0) return;
switch (filter.operand) {
case ViewFilterOperand.Is:
return {
[correspondingField.name + 'Id']: {
in: parsedRecordIds,
} as RelationFilter,
};
case ViewFilterOperand.IsNot: {
if (parsedRecordIds.length === 0) return;
return {
not: {
[correspondingField.name + 'Id']: {
in: parsedRecordIds,
} as RelationFilter,
};
case ViewFilterOperand.IsNot:
if (parsedRecordIds.length > 0) {
return {
not: {
[correspondingField.name + 'Id']: {
in: parsedRecordIds,
} as RelationFilter,
},
};
}
break;
default:
throw new Error(
`Unknown operand ${filter.operand} for ${filter.definition.type} filter`,
);
},
};
}
default:
throw new Error(
`Unknown operand ${filter.operand} for ${filter.definition.type} filter`,
);
}
} else {
switch (filter.operand) {
@ -349,7 +350,6 @@ const computeFilterRecordGqlOperationFilter = (
);
}
}
break;
}
case 'CURRENCY':
switch (filter.operand) {
@ -601,6 +601,56 @@ const computeFilterRecordGqlOperationFilter = (
`Unknown operand ${filter.operand} for ${filter.definition.type} filter`,
);
}
case 'MULTI_SELECT': {
if (isEmptyOperand) {
return getEmptyRecordGqlOperationFilter(
filter.operand,
correspondingField,
filter.definition,
);
}
const options = resolveFilterValue(
filter as Filter & { definition: { type: 'MULTI_SELECT' } },
);
if (options.length === 0) return;
switch (filter.operand) {
case ViewFilterOperand.Contains:
return {
[correspondingField.name]: {
containsAny: options,
} as MultiSelectFilter,
};
case ViewFilterOperand.DoesNotContain:
return {
or: [
{
not: {
[correspondingField.name]: {
containsAny: options,
} as MultiSelectFilter,
},
},
{
[correspondingField.name]: {
isEmptyArray: true,
} as MultiSelectFilter,
},
{
[correspondingField.name]: {
is: 'NULL',
} as MultiSelectFilter,
},
],
};
default:
throw new Error(
`Unknown operand ${filter.operand} for ${filter.definition.type} filter`,
);
}
}
case 'SELECT': {
if (isEmptyOperand) {
return getEmptyRecordGqlOperationFilter(
@ -609,44 +659,61 @@ const computeFilterRecordGqlOperationFilter = (
filter.definition,
);
}
const stringifiedSelectValues = filter.value;
let parsedOptionValues: string[] = [];
const options = resolveFilterValue(
filter as Filter & { definition: { type: 'SELECT' } },
);
if (!isNonEmptyString(stringifiedSelectValues)) {
break;
}
if (options.length === 0) return;
try {
parsedOptionValues = JSON.parse(stringifiedSelectValues);
} catch (e) {
throw new Error(
`Cannot parse filter value for SELECT filter : "${stringifiedSelectValues}"`,
);
}
if (parsedOptionValues.length > 0) {
switch (filter.operand) {
case ViewFilterOperand.Is:
return {
switch (filter.operand) {
case ViewFilterOperand.Is:
return {
[correspondingField.name]: {
in: options,
} as SelectFilter,
};
case ViewFilterOperand.IsNot:
return {
not: {
[correspondingField.name]: {
in: parsedOptionValues,
} as UUIDFilter,
};
case ViewFilterOperand.IsNot:
return {
not: {
[correspondingField.name]: {
in: parsedOptionValues,
} as UUIDFilter,
},
};
default:
throw new Error(
`Unknown operand ${filter.operand} for ${filter.definition.type} filter`,
);
}
in: options,
} as SelectFilter,
},
};
default:
throw new Error(
`Unknown operand ${filter.operand} for ${filter.definition.type} filter`,
);
}
}
case 'ARRAY': {
switch (filter.operand) {
case ViewFilterOperand.Contains:
return {
[correspondingField.name]: {
containsIlike: `%${filter.value}%`,
} as ArrayFilter,
};
case ViewFilterOperand.DoesNotContain:
return {
not: {
[correspondingField.name]: {
containsIlike: `%${filter.value}%`,
} as ArrayFilter,
},
};
case ViewFilterOperand.IsEmpty:
case ViewFilterOperand.IsNotEmpty:
return getEmptyRecordGqlOperationFilter(
filter.operand,
correspondingField,
filter.definition,
);
default:
throw new Error(
`Unknown operand ${filter.operand} for ${filter.definition.type} filter`,
);
}
break;
}
// TODO: fix this with a new composite field in ViewFilter entity
case 'ACTOR': {
@ -665,18 +732,17 @@ const computeFilterRecordGqlOperationFilter = (
case ViewFilterOperand.IsNot: {
const parsedRecordIds = JSON.parse(filter.value) as string[];
if (parsedRecordIds.length > 0) {
return {
not: {
[correspondingField.name]: {
source: {
in: parsedRecordIds,
} as RelationFilter,
},
if (parsedRecordIds.length === 0) return;
return {
not: {
[correspondingField.name]: {
source: {
in: parsedRecordIds,
} as RelationFilter,
},
};
}
break;
},
};
}
case ViewFilterOperand.Contains:
return {
@ -716,7 +782,6 @@ const computeFilterRecordGqlOperationFilter = (
`Unknown operand ${filter.operand} for ${filter.definition.label} filter`,
);
}
break;
}
case 'EMAILS':
switch (filter.operand) {
@ -806,7 +871,7 @@ const computeViewFilterGroupRecordGqlOperationFilter = (
);
if (!currentViewFilterGroup) {
return undefined;
return;
}
const groupFilters = filters.filter(

View File

@ -6,12 +6,14 @@ import {
DateFilter,
EmailsFilter,
FloatFilter,
MultiSelectFilter,
RatingFilter,
RawJsonFilter,
RecordGqlOperationFilter,
RelationFilter,
SelectFilter,
StringFilter,
URLFilter,
UUIDFilter,
} from '@/object-record/graphql/types/RecordGqlOperationFilter';
import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
@ -255,7 +257,7 @@ export const getEmptyRecordGqlOperationFilter = (
break;
case 'RATING':
emptyRecordFilter = {
[correspondingField.name]: { is: 'NULL' } as StringFilter,
[correspondingField.name]: { is: 'NULL' } as RatingFilter,
};
break;
case 'DATE':
@ -266,7 +268,21 @@ export const getEmptyRecordGqlOperationFilter = (
break;
case 'SELECT':
emptyRecordFilter = {
[correspondingField.name]: { is: 'NULL' } as UUIDFilter,
[correspondingField.name]: { is: 'NULL' } as SelectFilter,
};
break;
case 'MULTI_SELECT':
emptyRecordFilter = {
or: [
{
[correspondingField.name]: { is: 'NULL' } as MultiSelectFilter,
},
{
[correspondingField.name]: {
isEmptyArray: true,
} as MultiSelectFilter,
},
],
};
break;
case 'RELATION':
@ -296,6 +312,9 @@ export const getEmptyRecordGqlOperationFilter = (
{
[correspondingField.name]: { is: 'NULL' } as ArrayFilter,
},
{
[correspondingField.name]: { isEmptyArray: true } as ArrayFilter,
},
],
};
break;

View File

@ -5,19 +5,9 @@ export const isMatchingArrayFilter = ({
value,
}: {
arrayFilter: ArrayFilter;
value: string[];
value: string[] | null;
}) => {
if (value === null || !Array.isArray(value)) {
return false;
}
switch (true) {
case arrayFilter.contains !== undefined: {
return arrayFilter.contains.every((item) => value.includes(item));
}
case arrayFilter.not_contains !== undefined: {
return !arrayFilter.not_contains.some((item) => value.includes(item));
}
case arrayFilter.is !== undefined: {
if (arrayFilter.is === 'NULL') {
return value === null;
@ -25,6 +15,16 @@ export const isMatchingArrayFilter = ({
return value !== null;
}
}
case arrayFilter.isEmptyArray !== undefined: {
return Array.isArray(value) && value.length === 0;
}
case arrayFilter.containsIlike !== undefined: {
const searchTerm = arrayFilter.containsIlike.toLowerCase();
return (
Array.isArray(value) &&
value.some((item) => item.toLowerCase().includes(searchTerm))
);
}
default: {
throw new Error(
`Unexpected value for array filter: ${JSON.stringify(arrayFilter)}`,

View File

@ -0,0 +1,35 @@
import { MultiSelectFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter';
export const isMatchingMultiSelectFilter = ({
multiSelectFilter,
value,
}: {
multiSelectFilter: MultiSelectFilter;
value: string[] | null;
}) => {
switch (true) {
case multiSelectFilter.containsAny !== undefined: {
return (
Array.isArray(value) &&
multiSelectFilter.containsAny.every((item) => value.includes(item))
);
}
case multiSelectFilter.isEmptyArray !== undefined: {
return Array.isArray(value) && value.length === 0;
}
case multiSelectFilter.is !== undefined: {
if (multiSelectFilter.is === 'NULL') {
return value === null;
} else {
return value !== null;
}
}
default: {
throw new Error(
`Unexpected value for multi-select filter: ${JSON.stringify(
multiSelectFilter,
)}`,
);
}
}
};

View File

@ -0,0 +1,30 @@
import { RatingFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter';
export const isMatchingRatingFilter = ({
ratingFilter,
value,
}: {
ratingFilter: RatingFilter;
value: string | null;
}) => {
switch (true) {
case ratingFilter.eq !== undefined: {
return value === ratingFilter.eq;
}
case ratingFilter.in !== undefined: {
return value !== null && ratingFilter.in.includes(value);
}
case ratingFilter.is !== undefined: {
if (ratingFilter.is === 'NULL') {
return value === null;
} else {
return value !== null;
}
}
default: {
throw new Error(
`Unexpected value for rating filter : ${JSON.stringify(ratingFilter)}`,
);
}
}
};

View File

@ -0,0 +1,27 @@
import { SelectFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter';
export const isMatchingSelectFilter = ({
selectFilter,
value,
}: {
selectFilter: SelectFilter;
value: string;
}) => {
switch (true) {
case selectFilter.in !== undefined: {
return selectFilter.in.includes(value);
}
case selectFilter.is !== undefined: {
if (selectFilter.is === 'NULL') {
return value === null;
} else {
return value !== null;
}
}
default: {
throw new Error(
`Unexpected value for select filter : ${JSON.stringify(selectFilter)}`,
);
}
}
};

View File

@ -48,18 +48,6 @@ export const isMatchingStringFilter = ({
return regexCaseInsensitive.test(value);
}
case stringFilter.gt !== undefined: {
return value > stringFilter.gt;
}
case stringFilter.gte !== undefined: {
return value >= stringFilter.gte;
}
case stringFilter.lt !== undefined: {
return value < stringFilter.lt;
}
case stringFilter.lte !== undefined: {
return value <= stringFilter.lte;
}
case stringFilter.startsWith !== undefined: {
return value.startsWith(stringFilter.startsWith);
}

View File

@ -14,11 +14,14 @@ import {
FullNameFilter,
LeafObjectRecordFilter,
LinksFilter,
MultiSelectFilter,
NotObjectRecordFilter,
OrObjectRecordFilter,
PhonesFilter,
RatingFilter,
RawJsonFilter,
RecordGqlOperationFilter,
SelectFilter,
StringFilter,
UUIDFilter,
} from '@/object-record/graphql/types/RecordGqlOperationFilter';
@ -27,7 +30,10 @@ import { isMatchingBooleanFilter } from '@/object-record/record-filter/utils/isM
import { isMatchingCurrencyFilter } from '@/object-record/record-filter/utils/isMatchingCurrencyFilter';
import { isMatchingDateFilter } from '@/object-record/record-filter/utils/isMatchingDateFilter';
import { isMatchingFloatFilter } from '@/object-record/record-filter/utils/isMatchingFloatFilter';
import { isMatchingMultiSelectFilter } from '@/object-record/record-filter/utils/isMatchingMultiSelectFilter';
import { isMatchingRatingFilter } from '@/object-record/record-filter/utils/isMatchingRatingFilter';
import { isMatchingRawJsonFilter } from '@/object-record/record-filter/utils/isMatchingRawJsonFilter';
import { isMatchingSelectFilter } from '@/object-record/record-filter/utils/isMatchingSelectFilter';
import { isMatchingStringFilter } from '@/object-record/record-filter/utils/isMatchingStringFilter';
import { isMatchingUUIDFilter } from '@/object-record/record-filter/utils/isMatchingUUIDFilter';
import { FieldMetadataType } from '~/generated-metadata/graphql';
@ -160,15 +166,27 @@ export const isRecordMatchingFilter = ({
}
switch (objectMetadataField.type) {
case FieldMetadataType.Select:
case FieldMetadataType.Rating:
case FieldMetadataType.MultiSelect:
return isMatchingRatingFilter({
ratingFilter: filterValue as RatingFilter,
value: record[filterKey],
});
case FieldMetadataType.Text: {
return isMatchingStringFilter({
stringFilter: filterValue as StringFilter,
value: record[filterKey],
});
}
case FieldMetadataType.Select:
return isMatchingSelectFilter({
selectFilter: filterValue as SelectFilter,
value: record[filterKey],
});
case FieldMetadataType.MultiSelect:
return isMatchingMultiSelectFilter({
multiSelectFilter: filterValue as MultiSelectFilter,
value: record[filterKey],
});
case FieldMetadataType.Array: {
return isMatchingArrayFilter({
arrayFilter: filterValue as ArrayFilter,

View File

@ -75,7 +75,10 @@ export const ViewBarFilterEffect = ({
? JSON.parse(viewFilterUsedInDropdown.value)
: [];
setObjectFilterDropdownSelectedRecordIds(viewFilterSelectedRecords);
} else if (filterDefinitionUsedInDropdown?.type === 'SELECT') {
} else if (
isDefined(filterDefinitionUsedInDropdown) &&
['SELECT', 'MULTI_SELECT'].includes(filterDefinitionUsedInDropdown.type)
) {
const viewFilterUsedInDropdown =
currentViewWithCombinedFiltersAndSorts?.viewFilters.find(
(filter) =>

View File

@ -2,6 +2,7 @@ import { Filter } from '@/object-record/object-filter-dropdown/types/Filter';
import { FilterableFieldType } from '@/object-record/object-filter-dropdown/types/FilterableFieldType';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { resolveNumberViewFilterValue } from '@/views/view-filter-value/utils/resolveNumberViewFilterValue';
import { resolveSelectViewFilterValue } from '@/views/view-filter-value/utils/resolveSelectViewFilterValue';
import {
resolveDateViewFilterValue,
ResolvedDateViewFilterValue,
@ -14,7 +15,9 @@ type ResolvedFilterValue<
? ResolvedDateViewFilterValue<O>
: T extends 'NUMBER'
? ReturnType<typeof resolveNumberViewFilterValue>
: string;
: T extends 'SELECT' | 'MULTI_SELECT'
? string[]
: string;
type PartialFilter<
T extends FilterableFieldType,
@ -36,6 +39,9 @@ export const resolveFilterValue = <
return resolveDateViewFilterValue(filter) as ResolvedFilterValue<T, O>;
case 'NUMBER':
return resolveNumberViewFilterValue(filter) as ResolvedFilterValue<T, O>;
case 'SELECT':
case 'MULTI_SELECT':
return resolveSelectViewFilterValue(filter) as ResolvedFilterValue<T, O>;
default:
return filter.value as ResolvedFilterValue<T, O>;
}

View File

@ -0,0 +1,19 @@
import { ViewFilter } from '@/views/types/ViewFilter';
import { z } from 'zod';
const selectViewFilterValueSchema = z
.string()
.transform((val) => (val === '' ? [] : JSON.parse(val)))
.refine(
(parsed) =>
Array.isArray(parsed) && parsed.every((item) => typeof item === 'string'),
{
message: 'Expected an array of strings',
},
);
export const resolveSelectViewFilterValue = (
viewFilter: Pick<ViewFilter, 'value'>,
) => {
return selectViewFilterValueSchema.parse(viewFilter.value);
};

View File

@ -13,7 +13,7 @@ import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metada
import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory';
import { capitalize } from 'src/utils/capitalize';
const ARRAY_OPERATORS = ['in', 'contains', 'not_contains'];
const ARRAY_OPERATORS = ['in', 'contains', 'notContains'];
export class GraphqlQueryFilterFieldParser {
private fieldMetadataMapByName: FieldMetadataMap;

View File

@ -19,6 +19,11 @@ export const computeWhereConditionParts = (
const uuid = Math.random().toString(36).slice(2, 7);
switch (operator) {
case 'isEmptyArray':
return {
sql: `"${objectNameSingular}"."${key}" = '{}'`,
params: {},
};
case 'eq':
return {
sql: `"${objectNameSingular}"."${key}" = :${key}${uuid}`,
@ -84,10 +89,19 @@ export const computeWhereConditionParts = (
sql: `"${objectNameSingular}"."${key}" @> ARRAY[:...${key}${uuid}]`,
params: { [`${key}${uuid}`]: value },
};
case 'not_contains':
case 'notContains':
return {
sql: `NOT ("${objectNameSingular}"."${key}" && ARRAY[:...${key}${uuid}])`,
sql: `NOT ("${objectNameSingular}"."${key}"::text[] && ARRAY[:...${key}${uuid}]::text[])`,
params: { [`${key}${uuid}`]: value },
};
case 'containsAny':
return {
sql: `"${objectNameSingular}"."${key}"::text[] && ARRAY[:...${key}${uuid}]::text[]`,
params: { [`${key}${uuid}`]: value },
};
case 'containsIlike':
return {
sql: `EXISTS (SELECT 1 FROM unnest("${objectNameSingular}"."${key}") AS elem WHERE elem ILIKE :${key}${uuid})`,
params: { [`${key}${uuid}`]: value },
};

View File

@ -1,6 +1,11 @@
import { Injectable, Logger } from '@nestjs/common';
import { GraphQLInputObjectType, GraphQLInputType, GraphQLList } from 'graphql';
import {
GraphQLBoolean,
GraphQLInputObjectType,
GraphQLInputType,
GraphQLList,
} from 'graphql';
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
@ -103,7 +108,9 @@ export class InputTypeFactory {
eq: { type: enumType },
neq: { type: enumType },
in: { type: new GraphQLList(enumType) },
containsAny: { type: new GraphQLList(enumType) },
is: { type: FilterIs },
isEmptyArray: { type: GraphQLBoolean },
}),
});
}

View File

@ -1,12 +1,12 @@
import { GraphQLInputObjectType, GraphQLList, GraphQLString } from 'graphql';
import { GraphQLBoolean, GraphQLInputObjectType, GraphQLString } from 'graphql';
import { FilterIs } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/filter-is.input-type';
export const ArrayFilterType = new GraphQLInputObjectType({
name: 'ArrayFilter',
fields: {
contains: { type: new GraphQLList(GraphQLString) },
not_contains: { type: new GraphQLList(GraphQLString) },
containsIlike: { type: GraphQLString },
is: { type: FilterIs },
isEmptyArray: { type: GraphQLBoolean },
},
});

View File

@ -0,0 +1,17 @@
import {
GraphQLBoolean,
GraphQLInputObjectType,
GraphQLList,
GraphQLString,
} from 'graphql';
import { FilterIs } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/filter-is.input-type';
export const MultiSelectFilterType = new GraphQLInputObjectType({
name: 'MultiSelectFilter',
fields: {
containsAny: { type: new GraphQLList(GraphQLString) },
is: { type: FilterIs },
isEmptyArray: { type: GraphQLBoolean },
},
});

View File

@ -0,0 +1,11 @@
import { GraphQLInputObjectType, GraphQLList, GraphQLString } from 'graphql';
import { FilterIs } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/filter-is.input-type';
export const SelectFilterType = new GraphQLInputObjectType({
name: 'SelectFilter',
fields: {
in: { type: new GraphQLList(GraphQLString) },
is: { type: FilterIs },
},
});

View File

@ -27,6 +27,8 @@ import {
StringFilterType,
} from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input';
import { IDFilterType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/id-filter.input-type';
import { MultiSelectFilterType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/multi-select-filter.input-type';
import { SelectFilterType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/select-filter.input-type';
import {
BigFloatScalarType,
UUIDScalarType,
@ -115,6 +117,8 @@ export class TypeMapperService {
[FieldMetadataType.RAW_JSON, RawJsonFilterType],
[FieldMetadataType.RICH_TEXT, StringFilterType],
[FieldMetadataType.ARRAY, ArrayFilterType],
[FieldMetadataType.MULTI_SELECT, MultiSelectFilterType],
[FieldMetadataType.SELECT, SelectFilterType],
[FieldMetadataType.TS_VECTOR, StringFilterType], // TODO: Add TSVectorFilterType
]);

View File

@ -58,7 +58,9 @@ export const generateFields = <
? {
nullable: fieldMetadata.isNullable,
defaultValue: fieldMetadata.defaultValue,
isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT,
isArray:
kind !== InputTypeDefinitionKind.Filter &&
fieldMetadata.type === FieldMetadataType.MULTI_SELECT,
settings: fieldMetadata.settings,
isIdField: fieldMetadata.name === 'id',
}