-
Notifications
You must be signed in to change notification settings - Fork 2.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add filter on array and jsonb field types #7839
Changes from 1 commit
2879c0d
a7c8588
fe13470
0eb2fe1
8c9b769
da1d23f
6288888
f901804
585789e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -104,6 +104,17 @@ export type PhonesFilter = { | |
primaryPhoneCountryCode?: StringFilter; | ||
}; | ||
|
||
export type ArrayFilter = { | ||
contains?: string[]; | ||
contains_any?: string[]; | ||
not_contains?: string[]; | ||
}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: Consider adding a type for the array elements instead of using |
||
|
||
export type RawJsonFilter = { | ||
contains_filter?: string; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. filter is redundant here, let's use "contains" as it is the name for other filters |
||
is?: IsFilter; | ||
}; | ||
|
||
export type LeafFilter = | ||
| UUIDFilter | ||
| StringFilter | ||
|
@@ -117,6 +128,8 @@ export type LeafFilter = | |
| LinksFilter | ||
| ActorFilter | ||
| PhonesFilter | ||
| ArrayFilter | ||
| RawJsonFilter | ||
| undefined; | ||
|
||
export type AndObjectRecordFilter = { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,4 +19,5 @@ export type FilterableFieldType = PickLiteral< | |
| 'MULTI_SELECT' | ||
| 'ACTOR' | ||
| 'ARRAY' | ||
| 'RAW_JSON' | ||
>; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,7 +18,6 @@ export const getOperandsForFilterDefinition = ( | |
case 'FULL_NAME': | ||
case 'ADDRESS': | ||
case 'LINKS': | ||
case 'ARRAY': | ||
case 'PHONES': | ||
return [ | ||
ViewFilterOperand.Contains, | ||
|
@@ -32,6 +31,12 @@ export const getOperandsForFilterDefinition = ( | |
ViewFilterOperand.LessThan, | ||
...emptyOperands, | ||
]; | ||
case 'RAW_JSON': | ||
return [ | ||
ViewFilterOperand.Contains, | ||
ViewFilterOperand.DoesNotContain, | ||
...emptyOperands, | ||
]; | ||
case 'DATE_TIME': | ||
case 'DATE': | ||
return [ | ||
|
@@ -70,6 +75,8 @@ export const getOperandsForFilterDefinition = ( | |
...emptyOperands, | ||
]; | ||
} | ||
case 'ARRAY': | ||
return [ViewFilterOperand.Contains, ViewFilterOperand.DoesNotContain]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: ARRAY type doesn't include empty operands. Consider adding them for consistency with other types There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes let's add isEmpty here as greptile suggested 👍 |
||
default: | ||
return []; | ||
} | ||
Comment on lines
84
to
86
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: Default case returns an empty array. Consider throwing an error or logging a warning for unexpected filter types |
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,12 @@ | ||
import { | ||
ActorFilter, | ||
AddressFilter, | ||
ArrayFilter, | ||
CurrencyFilter, | ||
DateFilter, | ||
EmailsFilter, | ||
FloatFilter, | ||
RawJsonFilter, | ||
RecordGqlOperationFilter, | ||
RelationFilter, | ||
StringFilter, | ||
|
@@ -290,6 +292,20 @@ export const applyEmptyFilters = ( | |
], | ||
}; | ||
break; | ||
case 'ARRAY': | ||
emptyRecordFilter = { | ||
[correspondingField.name]: { not_contains: undefined } as ArrayFilter, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. applyEmptyFilter won't be called for array types, missing in turnObjectDropdownFilterIntoQueryFilter There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think 'not_contains: undefined' will work as intended 🤔 Have you tested this use case? |
||
}; | ||
break; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logic: The 'not_contains: undefined' filter for ARRAY might not work as expected. Consider using an empty array instead |
||
case 'RAW_JSON': | ||
emptyRecordFilter = { | ||
or: [ | ||
{ | ||
[correspondingField.name]: { is: 'NULL' } as RawJsonFilter, | ||
}, | ||
], | ||
}; | ||
break; | ||
Comment on lines
+304
to
+312
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: The RAW_JSON filter only checks for NULL. Consider adding a check for empty object {} |
||
case 'EMAILS': | ||
emptyRecordFilter = { | ||
or: [ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { ArrayFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter'; | ||
|
||
export const isMatchingArrayFilter = ({ | ||
arrayFilter, | ||
value, | ||
}: { | ||
arrayFilter: ArrayFilter; | ||
value: string; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logic: The |
||
}) => { | ||
switch (true) { | ||
case arrayFilter.contains !== undefined: { | ||
return arrayFilter.contains.every((item) => value.includes(item)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logic: This implementation assumes |
||
} | ||
case arrayFilter.not_contains !== undefined: { | ||
return !arrayFilter.not_contains.every((item) => value.includes(item)); | ||
Weiko marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's add "IS" filter for array type to be able to filter with NULL, NOT_NULL in those columns. |
||
default: { | ||
throw new Error( | ||
`Unexpected value for array filter: ${JSON.stringify(arrayFilter)}`, | ||
); | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: The switch statement on |
||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import { RawJsonFilter } from '../../graphql/types/RecordGqlOperationFilter'; | ||
|
||
export const isMatchingRawJsonFilter = ({ | ||
rawJsonFilter, | ||
value, | ||
}: { | ||
rawJsonFilter: RawJsonFilter; | ||
value: string; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logic: value is typed as string, but could be null |
||
}) => { | ||
switch (true) { | ||
case rawJsonFilter.contains_filter !== undefined: { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To my point above, let's use the term "like" like the string type here since that's actually what happens behind the scene |
||
const regexPattern = rawJsonFilter.contains_filter.replace(/%/g, '.*'); | ||
const regexCaseInsensitive = new RegExp(`^${regexPattern}$`, 'i'); | ||
|
||
const stringValue = JSON.stringify(value); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logic: JSON.stringify may fail if value is not JSON-serializable |
||
|
||
return regexCaseInsensitive.test(stringValue); | ||
} | ||
case rawJsonFilter.is !== undefined: { | ||
if (rawJsonFilter.is === 'NULL') { | ||
return value === null; | ||
} else { | ||
return value !== null; | ||
} | ||
Comment on lines
+20
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: consider using strict equality (===) for null checks |
||
} | ||
default: { | ||
throw new Error( | ||
`Unexpected value for string filter : ${JSON.stringify(rawJsonFilter)}`, | ||
); | ||
} | ||
} | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -44,7 +44,12 @@ export class GraphqlQueryFilterFieldParser { | |
} | ||
const [[operator, value]] = Object.entries(filterValue); | ||
|
||
if (operator === 'in') { | ||
if ( | ||
operator === 'in' || | ||
operator === 'contains' || | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we will have some issues in the future with this name. Contains is actually a bit "vague" and could be mistaken for "like" in StringsFilter 🤔 (the FE actually displays "Contains" in both cases) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. cc @charlesBochet operators are not mapped to particular types and can be mistaken. We could add "array" in the operator here for example, just so you know. |
||
operator === 'contains_any' || | ||
operator === 'not_contains' | ||
) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: Consider using an array or Set for better readability and maintainability |
||
if (!Array.isArray(value) || value.length === 0) { | ||
throw new GraphqlQueryRunnerException( | ||
`Invalid filter value for field ${key}. Expected non-empty array`, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -79,6 +79,29 @@ export const computeWhereConditionParts = ( | |
sql: `"${objectNameSingular}"."${key}" LIKE :${key}${uuid}`, | ||
params: { [`${key}${uuid}`]: `${value}` }, | ||
}; | ||
case 'contains': | ||
return { | ||
sql: `"${objectNameSingular}"."${key}" @> ARRAY[:...${key}${uuid}]`, | ||
params: { [`${key}${uuid}`]: value }, | ||
}; | ||
Comment on lines
+82
to
+86
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: The 'contains' operator for arrays uses the @> operator, which checks if the array column contains all elements of the input array. This is correct, but consider adding a comment explaining the behavior for clarity. |
||
|
||
case 'contains_any': | ||
return { | ||
sql: `"${objectNameSingular}"."${key}" && ARRAY[:...${key}${uuid}]`, | ||
params: { [`${key}${uuid}`]: value }, | ||
}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: The 'contains_any' operator uses the && operator, which checks for any overlap between arrays. This is correct, but a comment explaining the behavior would be helpful. |
||
|
||
case 'not_contains': | ||
return { | ||
sql: `NOT ("${objectNameSingular}"."${key}" && ARRAY[:...${key}${uuid}])`, | ||
params: { [`${key}${uuid}`]: value }, | ||
}; | ||
case 'contains_filter': | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As mentioned above, let's not introduce a new operator and reuse "like" for JSON. We can imagine better operators later when we will actually leverage jsonb operators but right now we are simply doing a LIKE operation. |
||
return { | ||
sql: `"${objectNameSingular}"."${key}"::text LIKE :${key}${uuid}`, | ||
params: { [`${key}${uuid}`]: `%${value}%` }, | ||
}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: The 'contains_filter' operator casts the column to text and uses LIKE. This works for jsonb, but might not be efficient for large objects. Consider using jsonb-specific operators for better performance. |
||
|
||
default: | ||
throw new GraphqlQueryRunnerException( | ||
`Operator "${operator}" is not supported`, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,11 @@ | ||
import { GraphQLInputObjectType } from 'graphql'; | ||
import { GraphQLInputObjectType, GraphQLString } from 'graphql'; | ||
|
||
import { FilterIs } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/filter-is.input-type'; | ||
|
||
export const RawJsonFilterType = new GraphQLInputObjectType({ | ||
name: 'RawJsonFilter', | ||
fields: { | ||
is: { type: FilterIs }, | ||
contains_filter: { type: GraphQLString }, | ||
}, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logic: Ensure that the 'contains_filter' implementation in the backend correctly handles JSON string matching. |
||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wouldn't put contains_any in this PR if not fully implemented.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We will actually re-implement it later when we will have advanced filter (using OR)