diff --git a/documentation/docs/guides-and-concepts/data-provider/handling-filters.md b/documentation/docs/guides-and-concepts/data-provider/handling-filters.md new file mode 100644 index 000000000000..45013dd84261 --- /dev/null +++ b/documentation/docs/guides-and-concepts/data-provider/handling-filters.md @@ -0,0 +1,163 @@ +--- +id: handling-filters +title: Handling Filters +--- + +**refine** expects an array of type `CrudFilters` to filter results based on some field’s values. So you can use more than one filter. Even the `or` operator can be used to combine multiple filters. + +## `CrudFilters` + +`CrudFilters` is an array of objects with the following properties: + +```ts +// Supported operators: +type CrudOperators = + | "eq" + | "ne" + | "lt" + | "gt" + | "lte" + | "gte" + | "in" + | "nin" + | "contains" + | "ncontains" + | "containss" + | "ncontainss" + | "between" + | "nbetween" + | "null" + | "nnull" + | "or"; + +// Supported filter types: +type LogicalFilter = { + field: string; + operator: Exclude; + value: any; +}; + +type ConditionalFilter = { + operator: "or"; + value: LogicalFilter[]; +}; + +type CrudFilter = LogicalFilter | ConditionalFilter; +//highlight-next-line +type CrudFilters = CrudFilter[]; +``` + +## `LogicalFilters` + +`LogicalFilter` works with `AND` logic. For example, if you want to filter by `name` field and `age` field, you can use the following filter: + +```ts +const filter = [ + { + field: "name", + operator: "eq", + value: "John", + }, + { + field: "age", + operator: "lt", + value: 30, + }, +]; +``` + +Here the query will look like: `"name" = "John" AND "age" < 30` + +## `ConditionalFilters` + +`ConditionalFilter` works only with `or` operator and expects an array of `LogicalFilter` objects in the `value` property. For example, if you want to filter by `age` field or `createdAt` field, you can use the following filter: + +```ts +const filter = [ + { + operator: "or", + value: [ + { + field: "age", + operator: "eq", + value: 30, + }, + { + field: "createdAt", + operator: "gte", + value: "2018-01-01", + }, + ], + }, +]; +``` + +Here the query will look like: `"age" = 30 OR "createdAt" <= "2018-01-01"` + +## Combining Filters + +You can group multiple parameters in the same query using the logical filters or the conditional filters operators to filter results based on more than one criteria at the same time. This allows you to create more complex queries. + +Example query: Find posts with 2 possible dates & a specific status + +```ts +filter = [ + { + operator: "or", + value: [ + { + field: "createdAt", + operator: "eq", + value: "2022-01-01", + }, + { + field: "createdAt", + operator: "eq", + value: "2022-01-02", + }, + ], + }, + { + operator: "eq", + field: "status", + value: "published", + }, +]; +``` + +Here the query will look like: +`"status" == "published" AND ("createdAt" == "2022-01-01" OR "createdAt" == "2022-01-02")` + +## Handle filters in a data provider + +```tsx title="dataProvider.ts" +import { DataProvider } from "@pankod/refine-core"; + +const dataProvider = (): DataProvider => ({ + getList: async ({ resource, pagination, filters, sort }) => { + if (filters) { + filters.map((filter) => { + if (filter.operator !== "or") { + // Handle your logical filters here + // console.log(typeof filter); // LogicalFilter + } else { + // Handle your conditional filters here + // console.log(typeof filter); // ConditionalFilter + } + }); + } + }, +}); +``` + +:::tip +Data providers that support `or` and `and` filtering logic are as follows: + +- [NestJS CRUD](https://github.com/pankod/refine/tree/master/packages/nestjsx-crud) +- [Strapi](https://github.com/pankod/refine/tree/master/packages/strapi) - [Strapi v4](https://github.com/pankod/refine/tree/master/packages/strapi-v4) +- [Strapi GraphQL](https://github.com/pankod/refine/tree/master/packages/strapi-graphql) +- [Supabase](https://github.com/pankod/refine/tree/master/packages/supabase) +- [Hasura](https://github.com/pankod/refine/tree/master/packages/hasura) +- [Nhost](https://github.com/pankod/refine/tree/master/packages/nhost) + +::: diff --git a/documentation/sidebars.js b/documentation/sidebars.js index 5694e3238b03..d3c2f0d15ad2 100644 --- a/documentation/sidebars.js +++ b/documentation/sidebars.js @@ -306,6 +306,7 @@ module.exports = { type: "category", label: "Data Provider", items: [ + "guides-and-concepts/data-provider/handling-filters", "guides-and-concepts/data-provider/graphql", "guides-and-concepts/data-provider/strapi-v4", "guides-and-concepts/data-provider/appwrite", diff --git a/examples/dataProvider/strapi-graphql/src/interfaces/index.d.ts b/examples/dataProvider/strapi-graphql/src/interfaces/index.d.ts index 1bf6b6472725..5697a56ebc54 100644 --- a/examples/dataProvider/strapi-graphql/src/interfaces/index.d.ts +++ b/examples/dataProvider/strapi-graphql/src/interfaces/index.d.ts @@ -7,5 +7,5 @@ export interface IPost { id: string; title: string; content: string; - category: ICategory; + category?: ICategory; } diff --git a/examples/dataProvider/strapi-graphql/src/pages/posts/edit.tsx b/examples/dataProvider/strapi-graphql/src/pages/posts/edit.tsx index 190bb3b372ef..512e02a0fa70 100644 --- a/examples/dataProvider/strapi-graphql/src/pages/posts/edit.tsx +++ b/examples/dataProvider/strapi-graphql/src/pages/posts/edit.tsx @@ -41,7 +41,7 @@ export const PostEdit: React.FC = () => { const postData = queryResult?.data?.data; const { selectProps: categorySelectProps } = useSelect({ resource: "categories", - defaultValue: postData?.category.id, + defaultValue: postData?.category?.id, metaData: { fields: ["id", "title"], }, @@ -68,7 +68,7 @@ export const PostEdit: React.FC = () => { onFinish={(values) => formProps.onFinish?.({ ...values, - category: values.category.id, + category: values.category?.id, } as any) } > diff --git a/examples/dataProvider/strapi-graphql/src/pages/posts/list.tsx b/examples/dataProvider/strapi-graphql/src/pages/posts/list.tsx index 95b04d67b181..8faa6e0515e0 100644 --- a/examples/dataProvider/strapi-graphql/src/pages/posts/list.tsx +++ b/examples/dataProvider/strapi-graphql/src/pages/posts/list.tsx @@ -52,7 +52,7 @@ export const PostList: React.FC = () => { id: item.id, title: item.title, content: item.content, - category: item.category.id, + category: item.category?.id, }; }, metaData: { @@ -113,7 +113,7 @@ export const PostList: React.FC = () => { /> )} - render={(_, record) => record.category.title} + render={(_, record) => record.category?.title} /> title="Actions" diff --git a/examples/dataProvider/strapi-graphql/src/pages/posts/show.tsx b/examples/dataProvider/strapi-graphql/src/pages/posts/show.tsx index c6914c0ae329..9cb41f4d37bb 100644 --- a/examples/dataProvider/strapi-graphql/src/pages/posts/show.tsx +++ b/examples/dataProvider/strapi-graphql/src/pages/posts/show.tsx @@ -41,7 +41,7 @@ export const PostShow: React.FC = () => { {record?.title} Category - {record?.category.title} + {record?.category?.title} Content diff --git a/packages/airtable/src/index.ts b/packages/airtable/src/index.ts index d4f602579f5c..3d2cadf411b2 100644 --- a/packages/airtable/src/index.ts +++ b/packages/airtable/src/index.ts @@ -27,8 +27,9 @@ const simpleOperators: Partial> = { const generateFilter = (filters?: CrudFilters): string | undefined => { if (filters) { - const parsedFilter = filters.map( - ({ field, operator, value }): Formula => { + const parsedFilter = filters.map((filter): Formula => { + if (filter.operator !== "or") { + const { field, operator, value } = filter; if (Object.keys(simpleOperators).includes(operator)) { const mappedOperator = simpleOperators[ @@ -70,10 +71,10 @@ const generateFilter = (filters?: CrudFilters): string | undefined => { return [value ? "=" : "!=", { field }, ["BLANK"]]; } + } - throw Error(`Operator ${operator} is not supported`); - }, - ); + throw Error(`Operator ${filter.operator} is not supported`); + }); return compile(["AND", ...parsedFilter]); } diff --git a/packages/altogic/src/index.ts b/packages/altogic/src/index.ts index af3069279cbf..fc839739e161 100644 --- a/packages/altogic/src/index.ts +++ b/packages/altogic/src/index.ts @@ -68,23 +68,27 @@ const generateSort = (sort?: CrudSorting) => { const generateFilter = (filters?: CrudFilters) => { const queryFilters: string[] = []; if (filters) { - filters.map(({ field, operator, value }) => { - const mappedOperator = mapOperator(operator); - - switch (mappedOperator) { - case "IN": - case "NIN": - queryFilters.push( - `${mappedOperator}(${JSON.stringify( - value, - )}, this.${field})`, - ); - break; - - default: - queryFilters.push( - `this.${field} ${mappedOperator} "${value}"`, - ); + filters.map((filter) => { + const mappedOperator = mapOperator(filter.operator); + + if (filter.operator !== "or") { + const { field, value } = filter; + + switch (mappedOperator) { + case "IN": + case "NIN": + queryFilters.push( + `${mappedOperator}(${JSON.stringify( + value, + )}, this.${field})`, + ); + break; + + default: + queryFilters.push( + `this.${field} ${mappedOperator} "${value}"`, + ); + } } }); } diff --git a/packages/antd/src/definitions/table/index.ts b/packages/antd/src/definitions/table/index.ts index cc416b5c1e2b..95d10254812b 100644 --- a/packages/antd/src/definitions/table/index.ts +++ b/packages/antd/src/definitions/table/index.ts @@ -3,6 +3,7 @@ import { CrudOperators, CrudSorting, CrudFilter, + LogicalFilter, } from "@pankod/refine-core"; import { SortOrder, SorterResult } from "antd/lib/table/interface"; @@ -28,10 +29,13 @@ export const getDefaultFilter = ( filters?: CrudFilters, operatorType: CrudOperators = "eq", ): CrudFilter["value"] | undefined => { - const filter = filters?.find( - ({ field, operator }) => - field === columnName && operator === operatorType, - ); + const filter = filters?.find((filter) => { + if (filter.operator !== "or") { + const { operator, field } = filter; + return field === columnName && operator === operatorType; + } + return undefined; + }); if (filter) { return filter.value || []; @@ -84,13 +88,17 @@ export const mapAntdFilterToCrudFilter = ( Object.keys(tableFilters).map((field) => { const value = tableFilters[field]; - const operator = prevFilters.find((p) => p.field === field)?.operator; + const operator = prevFilters + .filter((i) => i.operator !== "or") + .find((p: LogicalFilter) => p.field === field)?.operator; - crudFilters.push({ - field, - operator: operator ?? (Array.isArray(value) ? "in" : "eq"), - value, - }); + if (operator !== "or") { + crudFilters.push({ + field, + operator: operator ?? (Array.isArray(value) ? "in" : "eq"), + value, + }); + } }); return crudFilters; diff --git a/packages/appwrite/src/index.ts b/packages/appwrite/src/index.ts index d45956d9ae01..8c5535979bad 100644 --- a/packages/appwrite/src/index.ts +++ b/packages/appwrite/src/index.ts @@ -26,6 +26,7 @@ const operators = { between: undefined, nbetween: undefined, nnull: undefined, + or: undefined, }; const appwriteEventToRefineEvent = { @@ -66,13 +67,16 @@ export const getAppwriteFilters: GetAppwriteFiltersType = (filters) => { for (const filter of filters) { const operator = operators[filter.operator]; - const filterField = filter.field === "id" ? "$id" : filter.field; if (!operator) { throw new Error(`Operator ${filter.operator} is not supported`); } - appwriteFilters.push(`${filterField}${operator}${filter.value}`); + if (filter.operator !== "or") { + const filterField = filter.field === "id" ? "$id" : filter.field; + + appwriteFilters.push(`${filterField}${operator}${filter.value}`); + } } return appwriteFilters; diff --git a/packages/core/src/contexts/data/IDataContext.ts b/packages/core/src/contexts/data/IDataContext.ts index ad7a10cedecc..56e4a20a2c6f 100644 --- a/packages/core/src/contexts/data/IDataContext.ts +++ b/packages/core/src/contexts/data/IDataContext.ts @@ -39,14 +39,21 @@ export type CrudOperators = | "between" | "nbetween" | "null" - | "nnull"; + | "nnull" + | "or"; -export type CrudFilter = { +export type LogicalFilter = { field: string; - operator: CrudOperators; + operator: Exclude; value: any; }; +export type ConditionalFilter = { + operator: "or"; + value: LogicalFilter[]; +}; + +export type CrudFilter = LogicalFilter | ConditionalFilter; export type CrudSort = { field: string; order: "asc" | "desc"; diff --git a/packages/core/src/definitions/table/__snapshots__/index.spec.ts.snap b/packages/core/src/definitions/table/__snapshots__/index.spec.ts.snap index 17cd79114818..58e1dd002313 100644 --- a/packages/core/src/definitions/table/__snapshots__/index.spec.ts.snap +++ b/packages/core/src/definitions/table/__snapshots__/index.spec.ts.snap @@ -1,5 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`definitions/table stringify table params correctly 1`] = `"current=1&pageSize=10&categoryId__in[]=1&categoryId__in[]=2&sort[]=id&sort[]=title&order[]=desc&order[]=desc"`; +exports[`definitions/table stringify table params correctly 1`] = `"current=1&pageSize=10&sorter[0][field]=id&sorter[0][order]=desc&sorter[1][field]=title&sorter[1][order]=desc&filters[0][field]=categoryId&filters[0][operator]=in&filters[0][value][0]=1&filters[0][value][1]=2"`; -exports[`definitions/table stringify table single sort params correctly 1`] = `"current=1&pageSize=10&categoryId__in[]=1&categoryId__in[]=2&sort[]=id&order[]=desc"`; +exports[`definitions/table stringify table single sort params correctly 1`] = `"current=1&pageSize=10&sorter[0][field]=id&sorter[0][order]=desc&filters[0][field]=categoryId&filters[0][operator]=in&filters[0][value][0]=1&filters[0][value][1]=2"`; diff --git a/packages/core/src/definitions/table/index.spec.ts b/packages/core/src/definitions/table/index.spec.ts index 0c8d108a0c25..72ec4d7dfff1 100644 --- a/packages/core/src/definitions/table/index.spec.ts +++ b/packages/core/src/definitions/table/index.spec.ts @@ -60,7 +60,7 @@ describe("definitions/table", () => { it("parse table params with single sorter correctly", async () => { const url = - "?current=1&pageSize=10&categoryId__in[]=1&categoryId__in[]=2&sort[]=id&order[]=desc"; + "?current=1&pageSize=10&sorter[0][field]=id&sorter[0][order]=desc&filters[0][operator]=in&filters[0][field]=categoryId&filters[0][value][0]=1&filters[0][value][1]=2"; const { parsedCurrent, parsedPageSize, parsedSorter, parsedFilters } = parseTableParams(url); @@ -75,11 +75,30 @@ describe("definitions/table", () => { it("parse table params with advanced query object", async () => { const query = { - current: "1", - pageSize: "10", - id__eq: "1", - sort: ["id", "firstName"], - order: ["asc", "desc"], + current: 1, + pageSize: 10, + sorter: [ + { field: "id", order: "asc" }, + { field: "firstName", order: "desc" }, + ], + filters: [ + { field: "id", operator: "eq", value: "1" }, + { + operator: "or", + value: [ + { + field: "age", + operator: "lt", + value: "18", + }, + { + field: "age", + operator: "gt", + value: "20", + }, + ], + }, + ], }; const { parsedCurrent, parsedPageSize, parsedSorter, parsedFilters } = @@ -93,6 +112,21 @@ describe("definitions/table", () => { ]); expect(parsedFilters).toStrictEqual([ { field: "id", operator: "eq", value: "1" }, + { + operator: "or", + value: [ + { + field: "age", + operator: "lt", + value: "18", + }, + { + field: "age", + operator: "gt", + value: "20", + }, + ], + }, ]); }); diff --git a/packages/core/src/definitions/table/index.ts b/packages/core/src/definitions/table/index.ts index 825666a2403a..69550bcc2689 100644 --- a/packages/core/src/definitions/table/index.ts +++ b/packages/core/src/definitions/table/index.ts @@ -5,47 +5,21 @@ import differenceWith from "lodash/differenceWith"; import { CrudFilters, - CrudOperators, CrudSorting, CrudFilter, CrudSort, } from "../../interfaces"; export const parseTableParams = (url: string) => { - const { current, pageSize, sort, order, ...filters } = qs.parse( + const { current, pageSize, sorter, filters } = qs.parse( url.substring(1), // remove first ? character ); - const parsedSorter: CrudSorting = []; - if (Array.isArray(sort) && Array.isArray(order)) { - sort.forEach((item: unknown, index: number) => { - const sortOrder = order[index] as "asc" | "desc"; - - parsedSorter.push({ - field: `${item}`, - order: sortOrder, - }); - }); - } - - const parsedFilters: CrudFilters = []; - Object.keys(filters).map((item) => { - const [field, operator] = item.split("__"); - const value = filters[item]; - if (operator) { - parsedFilters.push({ - field, - operator: operator as CrudOperators, - value, - }); - } - }); - return { parsedCurrent: current && Number(current), parsedPageSize: pageSize && Number(pageSize), - parsedSorter, - parsedFilters, + parsedSorter: (sorter as CrudSorting) ?? [], + parsedFilters: (filters as CrudFilters) ?? [], }; }; @@ -61,40 +35,35 @@ export const stringifyTableParams = (params: { }): string => { const options: IStringifyOptions = { skipNulls: true, - arrayFormat: "brackets", + arrayFormat: "indices", encode: false, }; - const { pagination, sorter, filters } = params; - const sortFields = sorter.map((item) => item.field); - const sortOrders = sorter.map((item) => item.order); - - const qsSortFields = qs.stringify({ sort: sortFields }, options); - const qsSortOrders = qs.stringify({ order: sortOrders }, options); - let queryString = `current=${pagination.current}&pageSize=${pagination.pageSize}`; - const qsFilterItems: { [key: string]: string } = {}; - filters.map((filterItem) => { - qsFilterItems[`${filterItem.field}__${filterItem.operator}`] = - filterItem.value; - }); - - const qsFilters = qs.stringify(qsFilterItems, options); - if (qsFilters) { - queryString = `${queryString}&${qsFilters}`; + const qsSorters = qs.stringify({ sorter }, options); + if (qsSorters) { + queryString += `&${qsSorters}`; } - if (qsSortFields && qsSortOrders) { - queryString = `${queryString}&${qsSortFields}&${qsSortOrders}`; + const qsFilters = qs.stringify({ filters }, options); + if (qsFilters) { + queryString += `&${qsFilters}`; } return queryString; }; -export const compareFilters = (left: CrudFilter, right: CrudFilter): boolean => - left.field == right.field && left.operator == right.operator; +export const compareFilters = ( + left: CrudFilter, + right: CrudFilter, +): boolean => { + if (left.operator !== "or" && right.operator !== "or") { + return left.field == right.field && left.operator == right.operator; + } + return false; +}; export const compareSorters = (left: CrudSort, right: CrudSort): boolean => left.field == right.field; diff --git a/packages/core/src/index.tsx b/packages/core/src/index.tsx index 1a40a107697f..f2f49afdf04a 100644 --- a/packages/core/src/index.tsx +++ b/packages/core/src/index.tsx @@ -20,6 +20,8 @@ export { TitleProps, CrudFilter, CrudFilters, + LogicalFilter, + ConditionalFilter, CrudOperators, CrudSorting, CrudSort, diff --git a/packages/graphql/src/index.ts b/packages/graphql/src/index.ts index d3c62a9fd966..9f14adcf6423 100644 --- a/packages/graphql/src/index.ts +++ b/packages/graphql/src/index.ts @@ -17,11 +17,30 @@ const genereteSort = (sort?: CrudSorting) => { }; const generateFilter = (filters?: CrudFilters) => { - const queryFilters: { [key: string]: string } = {}; + const queryFilters: { [key: string]: any } = {}; if (filters) { - filters.map(({ field, operator, value }) => { - queryFilters[`${field}_${operator}`] = value; + filters.map((filter) => { + if (filter.operator !== "or") { + const { field, operator, value } = filter; + + if (operator === "eq") { + queryFilters[`${field}`] = value; + } else { + queryFilters[`${field}_${operator}`] = value; + } + } else { + const { value } = filter; + + const orFilters: any[] = []; + value.map((val) => { + orFilters.push({ + [`${val.field}_${val.operator}`]: val.value, + }); + }); + + queryFilters["_or"] = orFilters; + } }); } diff --git a/packages/graphql/test/getList/index.mock.ts b/packages/graphql/test/getList/index.mock.ts index 55e709a2e81b..7ca375975c0f 100644 --- a/packages/graphql/test/getList/index.mock.ts +++ b/packages/graphql/test/getList/index.mock.ts @@ -81,61 +81,78 @@ nock("https://api.strapi.refine.dev:443", { encodedQueryParams: true }) nock("https://api.strapi.refine.dev:443", { encodedQueryParams: true }) .post("/graphql", { query: "query ($sort: String, $where: JSON, $start: Int, $limit: Int) { posts (sort: $sort, where: $where, start: $start, limit: $limit) { title } }", - variables: { where: { title_eq: "GraphQl 3" }, start: 0, limit: 10 }, + variables: { where: { id: "907" }, start: 0, limit: 10 }, }) - .reply(200, { data: { posts: [{ title: "GraphQl 3" }] } }, [ - "Server", - "nginx/1.17.10", - "Date", - "Fri, 17 Sep 2021 07:34:57 GMT", - "Content-Type", - "application/json", - "Content-Length", - "43", - "Connection", - "close", - "Vary", - "Origin", - "Strict-Transport-Security", - "max-age=31536000; includeSubDomains", - "X-Frame-Options", - "SAMEORIGIN", - "X-Powered-By", - "Strapi ", - "X-Response-Time", - "39ms", - ]); + .reply( + 200, + { + data: { + posts: [ + { + title: "Molestias iste voluptatem velit sed voluptate aut voluptatibus explicabo.", + }, + ], + }, + }, + [ + "Server", + "nginx/1.17.10", + "Date", + "Fri, 04 Mar 2022 11:33:59 GMT", + "Content-Type", + "application/json", + "Content-Length", + "107", + "Connection", + "close", + "Vary", + "Origin", + "Strict-Transport-Security", + "max-age=31536000; includeSubDomains", + "X-Frame-Options", + "SAMEORIGIN", + "X-Powered-By", + "Strapi ", + "X-Response-Time", + "24ms", + ], + ); nock("https://api.strapi.refine.dev:443", { encodedQueryParams: true }) .post("/graphql", { query: "query ($sort: String, $where: JSON, $start: Int, $limit: Int) { posts (sort: $sort, where: $where, start: $start, limit: $limit) { id, title, category { id, title } } }", variables: { sort: "title:asc", - where: { category_eq: "2" }, + where: { category: "8" }, start: 0, limit: 10, }, }) .reply( 200, - [ - "1f8b0800000000000003aa564a492c4954b2aa562ac82f2e2956b28aae56ca4c51b252323254d2512ac92cc94905721cf3f24b32528b14fc52cb150280ea8052c98925a9e9f9459520ad100d48ea5d5273f3956a6b756052265434cb948a665950cf2c63032a9a654445b38ca9689619f5cc32c46796426801a9c621077f62710a101169426c6d2d17000000ffff", - "0300efcf3e6706030000", - ], + { + data: { + posts: [ + { + id: "349", + title: "Illo non iusto rem distinctio sequi dolores nobis.", + category: { id: "8", title: "Test" }, + }, + ], + }, + }, [ "Server", "nginx/1.17.10", "Date", - "Fri, 17 Sep 2021 07:45:46 GMT", + "Fri, 04 Mar 2022 11:50:17 GMT", "Content-Type", "application/json", - "Transfer-Encoding", - "chunked", + "Content-Length", + "132", "Connection", "close", "Vary", - "Accept-Encoding", - "Vary", "Origin", "Strict-Transport-Security", "max-age=31536000; includeSubDomains", @@ -144,8 +161,6 @@ nock("https://api.strapi.refine.dev:443", { encodedQueryParams: true }) "X-Powered-By", "Strapi ", "X-Response-Time", - "64ms", - "Content-Encoding", - "gzip", + "37ms", ], ); diff --git a/packages/graphql/test/getList/index.spec.ts b/packages/graphql/test/getList/index.spec.ts index 00e33059fd08..700ebbd74b26 100644 --- a/packages/graphql/test/getList/index.spec.ts +++ b/packages/graphql/test/getList/index.spec.ts @@ -37,9 +37,9 @@ describe("getList", () => { resource: "posts", filters: [ { - field: "title", + field: "id", operator: "eq", - value: "GraphQl 3", + value: "907", }, ], metaData: { @@ -47,7 +47,9 @@ describe("getList", () => { }, }); - expect(data[0]["title"]).toBe("GraphQl 3"); + expect(data[0]["title"]).toBe( + "Molestias iste voluptatem velit sed voluptate aut voluptatibus explicabo.", + ); }); it("correct filter and sort response", async () => { @@ -57,7 +59,7 @@ describe("getList", () => { { field: "category", operator: "eq", - value: "2", + value: "8", }, ], sort: [ @@ -71,7 +73,7 @@ describe("getList", () => { }, }); - expect(response.data[0]["id"]).toBe("21"); - expect(response.data[0]["category"].title).toBe("Demo"); + expect(response.data[0]["id"]).toBe("349"); + expect(response.data[0]["category"].title).toBe("Test"); }); }); diff --git a/packages/graphql/test/gqlClient.ts b/packages/graphql/test/gqlClient.ts index 60a2af854984..d87338356efd 100644 --- a/packages/graphql/test/gqlClient.ts +++ b/packages/graphql/test/gqlClient.ts @@ -6,7 +6,7 @@ const client = new GraphQLClient(API_URL); client.setHeader( "Authorization", - "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiaWF0IjoxNjMxNzk1ODUyLCJleHAiOjE2MzQzODc4NTJ9.d7-y7lmdWv_duYJ7kEvupXnu6k9N7zWmX4UDBrTaT2I", + "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiaWF0IjoxNjQ2MzkzNDY2LCJleHAiOjE2NDg5ODU0NjZ9.5TjmTOLL7x7kcNKpq9MFwI_w1fReF4f-wlir2rocvi8", ); export default client; diff --git a/packages/hasura/src/index.ts b/packages/hasura/src/index.ts index 2f902df2245a..93372814c6fb 100644 --- a/packages/hasura/src/index.ts +++ b/packages/hasura/src/index.ts @@ -60,6 +60,7 @@ const hasuraFilters: Record = containss: "_like", ncontainss: "_nlike", null: "_is_null", + or: "_or", between: undefined, nbetween: undefined, nnull: undefined, @@ -72,15 +73,34 @@ export const generateFilters: any = (filters?: CrudFilters) => { const resultFilter: any = {}; - filters.forEach((filter) => { - resultFilter[filter.field] = {}; + filters.map((filter) => { const operator = hasuraFilters[filter.operator]; - if (!operator) { throw new Error(`Operator ${filter.operator} is not supported`); } - resultFilter[filter.field][operator] = filter.value; + if (filter.operator !== "or") { + resultFilter[filter.field] = {}; + resultFilter[filter.field][operator] = filter.value; + } else { + const orFilter: any = []; + + filter.value.map((val) => { + const filterObject: any = {}; + const mapedOperator = hasuraFilters[val.operator]; + + if (!mapedOperator) { + throw new Error( + `Operator ${val.operator} is not supported`, + ); + } + filterObject[val.field] = {}; + filterObject[val.field][mapedOperator] = val.value; + orFilter.push(filterObject); + }); + + resultFilter[operator] = orFilter; + } }); return resultFilter; diff --git a/packages/nestjsx-crud/src/index.ts b/packages/nestjsx-crud/src/index.ts index 48eb7c84cb74..cc7d6ccf6696 100644 --- a/packages/nestjsx-crud/src/index.ts +++ b/packages/nestjsx-crud/src/index.ts @@ -89,19 +89,33 @@ const generateSort = (sort?: CrudSorting): SortBy | undefined => { return; }; -const generateFilter = (filters?: RefineCrudFilter): CrudFilters => { +const generateFilter = ( + filters?: RefineCrudFilter, +): { crudFilters: CrudFilters; orFilters: CrudFilters } => { const crudFilters: CrudFilters = []; + const orFilters: CrudFilters = []; + if (filters) { - filters.map(({ field, operator, value }) => { - crudFilters.push({ - field, - operator: mapOperator(operator), - value, - }); + filters.map((filter) => { + if (filter.operator !== "or") { + crudFilters.push({ + field: filter.field, + operator: mapOperator(filter.operator), + value: filter.value, + }); + } else { + filter.value.map((orFilter) => { + orFilters.push({ + field: orFilter.field, + operator: mapOperator(orFilter.operator), + value: orFilter.value, + }); + }); + } }); } - return crudFilters; + return { crudFilters, orFilters }; }; const NestsxCrud = ( @@ -113,10 +127,11 @@ const NestsxCrud = ( const current = pagination?.current || 1; const pageSize = pagination?.pageSize || 10; - const generetedFilters = generateFilter(filters); + const { crudFilters, orFilters } = generateFilter(filters); const query = RequestQueryBuilder.create() - .setFilter(generetedFilters) + .setFilter(crudFilters) + .setOr(orFilters) .setLimit(pageSize) .setPage(current) .setOffset((current - 1) * pageSize); @@ -233,9 +248,9 @@ const NestsxCrud = ( }, custom: async ({ url, method, filters, sort, payload, query, headers }) => { - const requestQueryBuilder = RequestQueryBuilder.create().setFilter( - generateFilter(filters), - ); + const { crudFilters } = generateFilter(filters); + const requestQueryBuilder = + RequestQueryBuilder.create().setFilter(crudFilters); const sortBy = generateSort(sort); if (sortBy) { diff --git a/packages/nhost/src/index.ts b/packages/nhost/src/index.ts index 0bfe319aba8b..87e717fac4a4 100644 --- a/packages/nhost/src/index.ts +++ b/packages/nhost/src/index.ts @@ -60,6 +60,7 @@ const hasuraFilters: Record = containss: "_like", ncontainss: "_nlike", null: "_is_null", + or: "_or", between: undefined, nbetween: undefined, nnull: undefined, @@ -72,15 +73,34 @@ export const generateFilters: any = (filters?: CrudFilters) => { const resultFilter: any = {}; - filters.forEach((filter) => { - resultFilter[filter.field] = {}; + filters.map((filter) => { const operator = hasuraFilters[filter.operator]; - if (!operator) { throw new Error(`Operator ${filter.operator} is not supported`); } - resultFilter[filter.field][operator] = filter.value; + if (filter.operator !== "or") { + resultFilter[filter.field] = {}; + resultFilter[filter.field][operator] = filter.value; + } else { + const orFilter: any = []; + + filter.value.map((val) => { + const filterObject: any = {}; + const mapedOperator = hasuraFilters[val.operator]; + + if (!mapedOperator) { + throw new Error( + `Operator ${val.operator} is not supported`, + ); + } + filterObject[val.field] = {}; + filterObject[val.field][mapedOperator] = val.value; + orFilter.push(filterObject); + }); + + resultFilter[operator] = orFilter; + } }); return resultFilter; diff --git a/packages/react-table/src/useTable/index.ts b/packages/react-table/src/useTable/index.ts index 43aece9ef48b..a4d8da9ce48e 100644 --- a/packages/react-table/src/useTable/index.ts +++ b/packages/react-table/src/useTable/index.ts @@ -1,9 +1,9 @@ import { useEffect, useMemo } from "react"; import { BaseRecord, - CrudFilters, CrudOperators, HttpError, + LogicalFilter, useTable as useTableCore, useTableProps as useTablePropsCore, } from "@pankod/refine-core"; @@ -43,6 +43,13 @@ export const useTable = < setFilters, } = useTableResult; + const logicalFilters: LogicalFilter[] = []; + filtersCore.map((filter) => { + if (filter.operator !== "or") { + logicalFilters.push(filter); + } + }); + const memoizedData = useMemo(() => data?.data ?? [], [data]); const reactTableResult = useTableRT( { @@ -54,7 +61,7 @@ export const useTable = < id: sorting.field, desc: sorting.order === "desc", })), - filters: filtersCore.map((filter) => ({ + filters: logicalFilters.map((filter) => ({ id: filter.field, value: filter.value, })), @@ -90,12 +97,12 @@ export const useTable = < }, [sortBy]); useEffect(() => { - const crudFilters: CrudFilters = []; + const crudFilters: LogicalFilter[] = []; filters?.map((filter) => { const operator = reactTableResult.columns.find( (c) => c.id === filter.id, - )?.filter as CrudOperators; + )?.filter as Exclude; crudFilters.push({ field: filter.id, @@ -105,7 +112,7 @@ export const useTable = < }); }); - const filteredArray = filtersCore.filter( + const filteredArray = logicalFilters.filter( (value) => !crudFilters.some( (b) => diff --git a/packages/simple-rest/src/index.ts b/packages/simple-rest/src/index.ts index 9be85aaffc71..9e85c4083612 100644 --- a/packages/simple-rest/src/index.ts +++ b/packages/simple-rest/src/index.ts @@ -61,14 +61,18 @@ const generateSort = (sort?: CrudSorting) => { const generateFilter = (filters?: CrudFilters) => { const queryFilters: { [key: string]: string } = {}; if (filters) { - filters.map(({ field, operator, value }) => { - if (field === "q") { - queryFilters[field] = value; - return; - } + filters.map((filter) => { + if (filter.operator !== "or") { + const { field, operator, value } = filter; + + if (field === "q") { + queryFilters[field] = value; + return; + } - const mappedOperator = mapOperator(operator); - queryFilters[`${field}${mappedOperator}`] = value; + const mappedOperator = mapOperator(operator); + queryFilters[`${field}${mappedOperator}`] = value; + } }); } diff --git a/packages/strapi-graphql/src/index.ts b/packages/strapi-graphql/src/index.ts index e694677c3ace..cb7db666b26c 100644 --- a/packages/strapi-graphql/src/index.ts +++ b/packages/strapi-graphql/src/index.ts @@ -17,14 +17,29 @@ const genereteSort = (sort?: CrudSorting) => { }; const generateFilter = (filters?: CrudFilters) => { - const queryFilters: { [key: string]: string } = {}; + const queryFilters: { [key: string]: any } = {}; if (filters) { - filters.map(({ field, operator, value }) => { - if (operator === "eq") { - queryFilters[`${field}`] = value; + filters.map((filter) => { + if (filter.operator !== "or") { + const { field, operator, value } = filter; + + if (operator === "eq") { + queryFilters[`${field}`] = value; + } else { + queryFilters[`${field}_${operator}`] = value; + } } else { - queryFilters[`${field}_${operator}`] = value; + const { value } = filter; + + const orFilters: any[] = []; + value.map((val) => { + orFilters.push({ + [`${val.field}_${val.operator}`]: val.value, + }); + }); + + queryFilters["_or"] = orFilters; } }); } diff --git a/packages/strapi-v4/src/dataProvider.ts b/packages/strapi-v4/src/dataProvider.ts index 2228084ff7cf..78c396e32fa6 100644 --- a/packages/strapi-v4/src/dataProvider.ts +++ b/packages/strapi-v4/src/dataProvider.ts @@ -59,15 +59,29 @@ const generateFilter = (filters?: CrudFilters) => { let rawQuery = ""; if (filters) { - filters.map(({ field, operator, value }) => { - const mapedOperator = mapOperator(operator); + filters.map((filter) => { + if (filter.operator !== "or") { + const { field, operator, value } = filter; + + const mapedOperator = mapOperator(operator); + + if (Array.isArray(value)) { + value.map((val, index) => { + rawQuery += `&filters[${field}][$${mapedOperator}][${index}]=${val}`; + }); + } else { + rawQuery += `&filters[${field}][$${mapedOperator}]=${value}`; + } + } else { + const { value } = filter; + + value.map((item, index) => { + const { field, operator, value } = item; - if (Array.isArray(value)) { - value.map((val: string) => { - rawQuery += `&filters${field}[$${mapedOperator}]=${val}`; + const mapedOperator = mapOperator(operator); + + rawQuery += `&filters[$or][${index}][${field}][$${mapedOperator}]=${value}`; }); - } else { - rawQuery += `&filters[${field}][$${mapedOperator}]=${value}`; } }); } diff --git a/packages/strapi/src/dataProvider.ts b/packages/strapi/src/dataProvider.ts index 6601e8cb8f23..b1a087109740 100644 --- a/packages/strapi/src/dataProvider.ts +++ b/packages/strapi/src/dataProvider.ts @@ -40,18 +40,37 @@ const generateSort = (sort?: CrudSorting) => { }; const generateFilter = (filters?: CrudFilters) => { - const queryFilters: { [key: string]: string } = {}; + let rawQuery = ""; + if (filters) { - filters.map(({ field, operator, value }) => { - if (operator === "eq") { - queryFilters[`${field}`] = value; + filters.map((filter) => { + if (filter.operator !== "or") { + const { field, operator, value } = filter; + + if (operator === "eq") { + rawQuery += `&${field}=${value}`; + } else { + if (Array.isArray(value)) { + value.map((val) => { + rawQuery += `&[${field}_${operator}]=${val}`; + }); + } else { + rawQuery += `&[${field}_${operator}]=${value}`; + } + } } else { - queryFilters[`${field}_${operator}`] = value; + const { value } = filter; + + value.map((item, index) => { + const { field, operator, value } = item; + + rawQuery += `&_where[_or][${index}][${field}_${operator}]=${value}`; + }); } }); } - return queryFilters; + return rawQuery; }; export const DataProvider = ( @@ -74,10 +93,8 @@ export const DataProvider = ( }; const response = await Promise.all([ - httpClient.get( - `${url}?${stringify(query)}&${stringify(queryFilters)}`, - ), - httpClient.get(`${url}/count?${stringify(queryFilters)}`), + httpClient.get(`${url}?${stringify(query)}&${queryFilters}`), + httpClient.get(`${url}/count?${queryFilters}`), ]); return { @@ -186,7 +203,7 @@ export const DataProvider = ( if (filters) { const filterQuery = generateFilter(filters); - requestUrl = `${requestUrl}&${stringify(filterQuery)}`; + requestUrl = `${requestUrl}&${filterQuery}`; } if (query) { diff --git a/packages/supabase/src/index.ts b/packages/supabase/src/index.ts index aee0b22509ab..aec97e34c9d8 100644 --- a/packages/supabase/src/index.ts +++ b/packages/supabase/src/index.ts @@ -4,6 +4,7 @@ import { CrudFilter, LiveEvent, HttpError, + CrudOperators, } from "@pankod/refine-core"; import { createClient, @@ -30,25 +31,66 @@ const supabaseTypes: Record = { "*": "*", }; +const mapOperator = (operator: CrudOperators) => { + switch (operator) { + case "ne": + return "neq"; + case "nin": + return "not.in"; + case "contains": + return "ilike"; + case "ncontains": + return "not.ilike"; + case "containss": + return "like"; + case "ncontainss": + return "not.like"; + case "null": + return "is"; + case "nnull": + return "not.is"; + case "between": + case "nbetween": + throw Error(`Operator ${operator} is not supported`); + default: + return operator; + } +}; + const generateFilter = (filter: CrudFilter, query: any) => { switch (filter.operator) { + case "eq": + return query.eq(filter.field, filter.value); case "ne": - return query.filter(filter.field, "neq", filter.value); + return query.neq(filter.field, filter.value); case "in": return query.in(filter.field, filter.value); + case "gt": + return query.gt(filter.field, filter.value); + case "gte": + return query.gte(filter.field, filter.value); + case "lt": + return query.lt(filter.field, filter.value); + case "lte": + return query.lte(filter.field, filter.value); case "contains": return query.ilike(filter.field, `%${filter.value}%`); case "containss": return query.like(filter.field, `%${filter.value}%`); - case "ncontainss": - case "ncontains": - case "nin": - throw Error(`Operator ${filter.operator} is not supported`); case "null": return query.is(filter.field, null); + case "or": + const orSyntax = filter.value + .map((item) => `${item.field}.${item.operator}.${item.value}`) + .join(","); + return query.or(orSyntax); + default: + return query.filter( + filter.field, + mapOperator(filter.operator), + filter.value, + ); } - - return query.filter(filter.field, filter.operator, filter.value); }; const handleError = (error: PostgrestError) => { diff --git a/packages/supabase/test/getList/index.spec.ts b/packages/supabase/test/getList/index.spec.ts index 96b4a396cf73..94201bc9e8a9 100644 --- a/packages/supabase/test/getList/index.spec.ts +++ b/packages/supabase/test/getList/index.spec.ts @@ -153,23 +153,6 @@ describe("filtering", () => { expect(total).toBe(2); }); - it("nin operator should throw error", async () => { - try { - await dataProvider(supabaseClient).getList({ - resource: "posts", - filters: [ - { - field: "id", - operator: "nin", - value: ["2", "3"], - }, - ], - }); - } catch (error) { - expect(error).toEqual(Error("Operator nin is not supported")); - } - }); - it("contains operator should work correctly", async () => { const { data, total } = await dataProvider(supabaseClient).getList({ resource: "posts", @@ -186,23 +169,6 @@ describe("filtering", () => { expect(total).toBe(0); }); - it("ncontains operator should throw error", async () => { - try { - await dataProvider(supabaseClient).getList({ - resource: "posts", - filters: [ - { - field: "id", - operator: "ncontains", - value: "world", - }, - ], - }); - } catch (error) { - expect(error).toEqual(Error("Operator ncontains is not supported")); - } - }); - it("containss operator should work correctly", async () => { const { data, total } = await dataProvider(supabaseClient).getList({ resource: "posts", @@ -219,25 +185,6 @@ describe("filtering", () => { expect(total).toBe(1); }); - it("ncontainss operator should throw error", async () => { - try { - await dataProvider(supabaseClient).getList({ - resource: "posts", - filters: [ - { - field: "id", - operator: "ncontainss", - value: "world", - }, - ], - }); - } catch (error) { - expect(error).toEqual( - Error("Operator ncontainss is not supported"), - ); - } - }); - it("null operator should work correctly", async () => { const { data, total } = await dataProvider(supabaseClient).getList({ resource: "posts",