diff --git a/.changeset/few-ads-matter.md b/.changeset/few-ads-matter.md new file mode 100644 index 000000000000..496ce2be4d7a --- /dev/null +++ b/.changeset/few-ads-matter.md @@ -0,0 +1,7 @@ +--- +"@refinedev/core": minor +"@refinedev/supabase": minor +--- + +Added ina and nina CrudOperators. Added filtering by these operators to Supabase data provider +#5902 diff --git a/documentation/docs/advanced-tutorials/data-provider/handling-filters.md b/documentation/docs/advanced-tutorials/data-provider/handling-filters.md index 68ce3215436c..95cd67e312c4 100644 --- a/documentation/docs/advanced-tutorials/data-provider/handling-filters.md +++ b/documentation/docs/advanced-tutorials/data-provider/handling-filters.md @@ -20,6 +20,8 @@ type CrudOperators = | "gte" | "in" | "nin" + | "ina" + | "nina" | "contains" | "ncontains" | "containss" @@ -234,7 +236,7 @@ filter = [ ]; ``` -Here the query will look like: +Here the query will look like: `"status" == "published" AND ("createdAt" == "2022-01-01" OR "createdAt" == "2022-01-02")` ## Handle filters in a data provider diff --git a/documentation/docs/core/interface-references/index.md b/documentation/docs/core/interface-references/index.md index efca277bafde..eaa3c81a9e93 100644 --- a/documentation/docs/core/interface-references/index.md +++ b/documentation/docs/core/interface-references/index.md @@ -46,6 +46,8 @@ type CrudOperators = | "gte" // Greater than or equal to | "in" // Included in an array | "nin" // Not included in an array + | "ina" // Column contains every element in an array + | "nina" // Column doesn't contain every element in an array | "contains" // Contains | "ncontains" // Doesn't contain | "containss" // Contains, case sensitive diff --git a/packages/core/src/contexts/data/types.ts b/packages/core/src/contexts/data/types.ts index 8e88aaec9142..3c09dd74abb2 100644 --- a/packages/core/src/contexts/data/types.ts +++ b/packages/core/src/contexts/data/types.ts @@ -224,6 +224,8 @@ export type CrudOperators = | "gte" | "in" | "nin" + | "ina" + | "nina" | "contains" | "ncontains" | "containss" diff --git a/packages/supabase/src/utils/generateFilter.ts b/packages/supabase/src/utils/generateFilter.ts index 3bfbf5fd77b9..be50a79dfe17 100644 --- a/packages/supabase/src/utils/generateFilter.ts +++ b/packages/supabase/src/utils/generateFilter.ts @@ -9,6 +9,15 @@ export const generateFilter = (filter: CrudFilter, query: any) => { return query.neq(filter.field, filter.value); case "in": return query.in(filter.field, filter.value); + case "ina": + return query.contains(filter.field, filter.value); + case "nina": + return query.not( + filter.field, + "cs", + `{${filter.value.map((val: any) => `"${val}"`).join(",")}}`, + ); + case "gt": return query.gt(filter.field, filter.value); case "gte": @@ -35,7 +44,13 @@ export const generateFilter = (filter: CrudFilter, query: any) => { item.operator !== "and" && "field" in item ) { - return `${item.field}.${mapOperator(item.operator)}.${item.value}`; + let value = item.value; + + if (item.operator === "ina" || item.operator === "nina") { + value = `{${item.value.map((val: any) => `"${val}"`).join(",")}}`; + } + + return `${item.field}.${mapOperator(item.operator)}.${value}`; } return; }) diff --git a/packages/supabase/src/utils/mapOperator.ts b/packages/supabase/src/utils/mapOperator.ts index 2ea7cad5b12b..50d17e2dc3d7 100644 --- a/packages/supabase/src/utils/mapOperator.ts +++ b/packages/supabase/src/utils/mapOperator.ts @@ -18,6 +18,10 @@ export const mapOperator = (operator: CrudOperators) => { return "is"; case "nnull": return "not.is"; + case "ina": + return "cs"; + case "nina": + return "not.cs"; case "between": case "nbetween": throw Error(`Operator ${operator} is not supported`); diff --git a/packages/supabase/test/getList/index.mock.ts b/packages/supabase/test/getList/index.mock.ts index 130a139fe2a0..ad7a0a6ad802 100644 --- a/packages/supabase/test/getList/index.mock.ts +++ b/packages/supabase/test/getList/index.mock.ts @@ -1535,8 +1535,8 @@ nock("https://iwdfzvfqbtokqetmbmbp.supabase.co:443", { nock("https://iwdfzvfqbtokqetmbmbp.supabase.co:443", { encodedQueryParams: true, }) - .get("/rest/v1/posts") - .query({ select: "%2A", offset: "0", limit: "10" }) + .get("/rest/v1/products") + .query({ select: "*", offset: "0", limit: "10" }) .reply( 406, { @@ -1578,3 +1578,424 @@ nock("https://iwdfzvfqbtokqetmbmbp.supabase.co:443", { 'h3=":443"; ma=86400', ], ); + +nock("https://iwdfzvfqbtokqetmbmbp.supabase.co:443", { + encodedQueryParams: true, +}) + .get("/rest/v1/posts") + .query({ + select: "%2A", + offset: "0", + limit: "10", + or: "%28id.eq.2%2Ctags.cs.%7B%22recipes%22%2C%22personal%22%2C%22food%22%7D%29", + }) + .reply( + 200, + [ + { + id: 8, + created_at: "2024-04-24T13:20:10.200327+00:00", + title: "Oakwoods Prairie Clover", + slug: "41ed56fc-7644-475a-9c83-ea50e1b9fc76", + content: + "Nam ultrices, libero non mattis pulvinar, nulla pede ullamcorper augue, a suscipit nulla elit ac nulla. Sed vel enim sit amet nunc viverra dapibus. Nulla suscipit ligula in lacus.", + categoryId: 5, + tags: ["travel", "food", "recipes", "personal"], + images: null, + }, + { + id: 2, + created_at: "2024-04-24T13:20:10.200327+00:00", + title: "Samsung Galaxy S21", + slug: "5aecd7b0-cf28-40b4-ad48-7d4c6718837e", + content: + "In hac habitasse platea dictumst. Etiam faucibus cursus urna. Ut tellus.\n\nNulla ut erat id mauris vulputate elementum. Nullam varius. Nulla facilisi.\n\nCras non velit nec nisi vulputate nonummy. Maecenas tincidunt lacus at velit. Vivamus vel nulla eget eros elementum pellentesque.", + categoryId: 8, + tags: ["lifestyle", "health", "travel"], + images: null, + }, + { + id: 1, + created_at: "2024-05-06T11:27:01.700396+00:00", + title: "Black Psorotichia Lichen", + slug: "61a31089-c85d-48a0-a4be-d5dce5c96b6a", + content: "test content", + categoryId: 5, + tags: [ + "lifestyle", + "health", + "travel", + "food", + "recipes", + "personal", + "technology", + "fashion", + "beauty", + "skincare", + "education", + "mental health", + ], + images: "", + }, + ], + [ + "Date", + "Thu, 23 May 2024 14:47:49 GMT", + "Content-Type", + "application/json; charset=utf-8", + "Transfer-Encoding", + "chunked", + "Connection", + "close", + "Content-Range", + "0-2/3", + "CF-Ray", + "8885d7e6c86c5aef-VIE", + "CF-Cache-Status", + "DYNAMIC", + "Content-Location", + "/posts?limit=10&offset=0&or=%28id.eq.2%2Ctags.cs.%7B%22recipes%22%2C%22personal%22%2C%22food%22%7D%29&select=%2A", + "Strict-Transport-Security", + "max-age=15552000; includeSubDomains", + "content-profile", + "public", + "preference-applied", + "count=exact", + "sb-gateway-version", + "1", + "x-envoy-upstream-service-time", + "14", + "Vary", + "Accept-Encoding", + "Server", + "cloudflare", + "alt-svc", + 'h3=":443"; ma=86400', + ], + ); + +nock("https://iwdfzvfqbtokqetmbmbp.supabase.co:443", { + encodedQueryParams: true, +}) + .get("/rest/v1/posts") + .query({ + select: "%2A", + offset: "0", + limit: "10", + tags: "cs.%7Bhealth%2Ctravel%7D", + }) + .reply( + 200, + [ + { + id: 6, + created_at: "2024-04-24T13:20:10.200327+00:00", + title: "Walter's Sedge", + slug: "61a985f0-5078-49de-8ed8-23018c5381d6", + content: + "Cras mi pede, malesuada in, imperdiet et, commodo vulputate, justo. In blandit ultrices enim. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", + categoryId: 1, + tags: ["lifestyle", "personal", "health", "travel", "food"], + images: null, + }, + { + id: 2, + created_at: "2024-04-24T13:20:10.200327+00:00", + title: "Samsung Galaxy S21", + slug: "5aecd7b0-cf28-40b4-ad48-7d4c6718837e", + content: + "In hac habitasse platea dictumst. Etiam faucibus cursus urna. Ut tellus.\n\nNulla ut erat id mauris vulputate elementum. Nullam varius. Nulla facilisi.\n\nCras non velit nec nisi vulputate nonummy. Maecenas tincidunt lacus at velit. Vivamus vel nulla eget eros elementum pellentesque.", + categoryId: 8, + tags: ["lifestyle", "health", "travel"], + images: null, + }, + { + id: 1, + created_at: "2024-05-06T11:27:01.700396+00:00", + title: "Black Psorotichia Lichen", + slug: "61a31089-c85d-48a0-a4be-d5dce5c96b6a", + content: "test content", + categoryId: 5, + tags: [ + "lifestyle", + "health", + "travel", + "food", + "recipes", + "personal", + "technology", + "fashion", + "beauty", + "skincare", + "education", + "mental health", + ], + images: "", + }, + ], + [ + "Date", + "Thu, 23 May 2024 14:33:53 GMT", + "Content-Type", + "application/json; charset=utf-8", + "Transfer-Encoding", + "chunked", + "Connection", + "close", + "Content-Range", + "0-2/3", + "CF-Ray", + "8885c3798cb23bbd-WAW", + "CF-Cache-Status", + "DYNAMIC", + "Content-Location", + "/posts?limit=10&offset=0&select=%2A&tags=cs.%7Bhealth%2Ctravel%7D", + "Strict-Transport-Security", + "max-age=15552000; includeSubDomains", + "content-profile", + "public", + "preference-applied", + "count=exact", + "sb-gateway-version", + "1", + "x-envoy-upstream-service-time", + "2", + "Vary", + "Accept-Encoding", + "Server", + "cloudflare", + "alt-svc", + 'h3=":443"; ma=86400', + ], + ); + +nock("https://iwdfzvfqbtokqetmbmbp.supabase.co:443", { + encodedQueryParams: true, +}) + .get("/rest/v1/posts") + .query({ + select: "%2A", + offset: "0", + limit: "10", + tags: "not.cs.%7B%22lifestyle%22%2C%22personal%22%7D", + }) + .reply( + 200, + [ + { + id: 7, + created_at: "2024-04-24T13:20:10.200327+00:00", + title: "Sickle Island Spleenwort", + slug: "e4ab8737-a719-4fc1-900c-f72f2d0d7700", + content: + "Praesent blandit. Nam nulla. Integer pede justo, lacinia eget, tincidunt eget, tempus vel, pede.\n\nMorbi porttitor lorem id ligula. Suspendisse ornare consequat lectus. In est risus, auctor sed, tristique in, tempus sit amet, sem.\n\nFusce consequat. Nulla nisl. Nunc nisl.", + categoryId: 3, + tags: ["fashion", "beauty", "mental health"], + images: null, + }, + { + id: 8, + created_at: "2024-04-24T13:20:10.200327+00:00", + title: "Oakwoods Prairie Clover", + slug: "41ed56fc-7644-475a-9c83-ea50e1b9fc76", + content: + "Nam ultrices, libero non mattis pulvinar, nulla pede ullamcorper augue, a suscipit nulla elit ac nulla. Sed vel enim sit amet nunc viverra dapibus. Nulla suscipit ligula in lacus.", + categoryId: 5, + tags: ["travel", "food", "recipes", "personal"], + images: null, + }, + { + id: 11, + created_at: "2024-04-24T13:20:10.200327+00:00", + title: "Funck's Wart Lichen", + slug: "9d5b7d39-2c6e-40c3-ad4e-9cc46eabe1fb", + content: + "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Proin risus. Praesent lectus.\n\nVestibulum quam sapien, varius ut, blandit non, interdum in, ante. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Duis faucibus accumsan odio. Curabitur convallis.", + categoryId: 8, + tags: ["recipes", "food"], + images: null, + }, + { + id: 5, + created_at: "2024-04-24T13:20:10.200327+00:00", + title: "test", + slug: "6ca3651b-ce0b-45c7-9bd3-ece235142da5", + content: "test content", + categoryId: 12, + tags: ["technology", "education"], + images: null, + }, + { + id: 2, + created_at: "2024-04-24T13:20:10.200327+00:00", + title: "Samsung Galaxy S21", + slug: "5aecd7b0-cf28-40b4-ad48-7d4c6718837e", + content: + "In hac habitasse platea dictumst. Etiam faucibus cursus urna. Ut tellus.\n\nNulla ut erat id mauris vulputate elementum. Nullam varius. Nulla facilisi.\n\nCras non velit nec nisi vulputate nonummy. Maecenas tincidunt lacus at velit. Vivamus vel nulla eget eros elementum pellentesque.", + categoryId: 8, + tags: ["lifestyle", "health", "travel"], + images: null, + }, + ], + [ + "Date", + "Thu, 23 May 2024 14:39:43 GMT", + "Content-Type", + "application/json; charset=utf-8", + "Transfer-Encoding", + "chunked", + "Connection", + "close", + "Content-Range", + "0-4/5", + "CF-Ray", + "8885cc0ae9abfc5b-WAW", + "CF-Cache-Status", + "DYNAMIC", + "Content-Location", + "/posts?limit=10&offset=0&select=%2A&tags=not.cs.%7B%22lifestyle%22%2C%22personal%22%7D", + "Strict-Transport-Security", + "max-age=15552000; includeSubDomains", + "content-profile", + "public", + "preference-applied", + "count=exact", + "sb-gateway-version", + "1", + "x-envoy-upstream-service-time", + "2", + "Vary", + "Accept-Encoding", + "Server", + "cloudflare", + "alt-svc", + 'h3=":443"; ma=86400', + ], + ); + +nock("https://iwdfzvfqbtokqetmbmbp.supabase.co:443", { + encodedQueryParams: true, +}) + .get("/rest/v1/posts") + .query({ + select: "%2A", + offset: "0", + limit: "10", + or: "%28tags.cs.%7B%22technology%22%2C%22education%22%7D%2Ctags.not.cs.%7B%22lifestyle%22%2C%22personal%22%7D%29", + }) + .reply( + 200, + [ + { + id: 7, + created_at: "2024-04-24T13:20:10.200327+00:00", + title: "Sickle Island Spleenwort", + slug: "e4ab8737-a719-4fc1-900c-f72f2d0d7700", + content: + "Praesent blandit. Nam nulla. Integer pede justo, lacinia eget, tincidunt eget, tempus vel, pede.\n\nMorbi porttitor lorem id ligula. Suspendisse ornare consequat lectus. In est risus, auctor sed, tristique in, tempus sit amet, sem.\n\nFusce consequat. Nulla nisl. Nunc nisl.", + categoryId: 3, + tags: ["fashion", "beauty", "mental health"], + images: null, + }, + { + id: 8, + created_at: "2024-04-24T13:20:10.200327+00:00", + title: "Oakwoods Prairie Clover", + slug: "41ed56fc-7644-475a-9c83-ea50e1b9fc76", + content: + "Nam ultrices, libero non mattis pulvinar, nulla pede ullamcorper augue, a suscipit nulla elit ac nulla. Sed vel enim sit amet nunc viverra dapibus. Nulla suscipit ligula in lacus.", + categoryId: 5, + tags: ["travel", "food", "recipes", "personal"], + images: null, + }, + { + id: 11, + created_at: "2024-04-24T13:20:10.200327+00:00", + title: "Funck's Wart Lichen", + slug: "9d5b7d39-2c6e-40c3-ad4e-9cc46eabe1fb", + content: + "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Proin risus. Praesent lectus.\n\nVestibulum quam sapien, varius ut, blandit non, interdum in, ante. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Duis faucibus accumsan odio. Curabitur convallis.", + categoryId: 8, + tags: ["recipes", "food"], + images: null, + }, + { + id: 5, + created_at: "2024-04-24T13:20:10.200327+00:00", + title: "test", + slug: "6ca3651b-ce0b-45c7-9bd3-ece235142da5", + content: "test content", + categoryId: 12, + tags: ["technology", "education"], + images: null, + }, + { + id: 2, + created_at: "2024-04-24T13:20:10.200327+00:00", + title: "Samsung Galaxy S21", + slug: "5aecd7b0-cf28-40b4-ad48-7d4c6718837e", + content: + "In hac habitasse platea dictumst. Etiam faucibus cursus urna. Ut tellus.\n\nNulla ut erat id mauris vulputate elementum. Nullam varius. Nulla facilisi.\n\nCras non velit nec nisi vulputate nonummy. Maecenas tincidunt lacus at velit. Vivamus vel nulla eget eros elementum pellentesque.", + categoryId: 8, + tags: ["lifestyle", "health", "travel"], + images: null, + }, + { + id: 1, + created_at: "2024-05-06T11:27:01.700396+00:00", + title: "Black Psorotichia Lichen", + slug: "61a31089-c85d-48a0-a4be-d5dce5c96b6a", + content: "test content", + categoryId: 5, + tags: [ + "lifestyle", + "health", + "travel", + "food", + "recipes", + "personal", + "technology", + "fashion", + "beauty", + "skincare", + "education", + "mental health", + ], + images: "", + }, + ], + [ + "Date", + "Thu, 23 May 2024 14:53:46 GMT", + "Content-Type", + "application/json; charset=utf-8", + "Transfer-Encoding", + "chunked", + "Connection", + "close", + "Content-Range", + "0-5/6", + "CF-Ray", + "8885e09c2872b215-WAW", + "CF-Cache-Status", + "DYNAMIC", + "Content-Location", + "/posts?limit=10&offset=0&or=%28tags.cs.%7B%22technology%22%2C%22education%22%7D%2Ctags.not.cs.%7B%22lifestyle%22%2C%22personal%22%7D%29&select=%2A", + "Strict-Transport-Security", + "max-age=15552000; includeSubDomains", + "content-profile", + "public", + "preference-applied", + "count=exact", + "sb-gateway-version", + "1", + "x-envoy-upstream-service-time", + "14", + "Vary", + "Accept-Encoding", + "Server", + "cloudflare", + "alt-svc", + 'h3=":443"; ma=86400', + ], + ); diff --git a/packages/supabase/test/getList/index.spec.ts b/packages/supabase/test/getList/index.spec.ts index be421bd9c5b9..d869222b611c 100644 --- a/packages/supabase/test/getList/index.spec.ts +++ b/packages/supabase/test/getList/index.spec.ts @@ -285,6 +285,103 @@ describe("filtering", () => { expect(total).toBe(2); }); + it("ina operator should work correctly with or", async () => { + const { data, total } = await dataProvider(supabaseClient).getList({ + resource: "posts", + filters: [ + { + operator: "or", + value: [ + { + field: "id", + operator: "eq", + value: "2", + }, + { + field: "tags", + operator: "ina", + value: ["recipes", "personal", "food"], + }, + ], + }, + ], + }); + + expect(data[0]["title"]).toBe("Oakwoods Prairie Clover"); + expect(data[1]["title"]).toBe("Samsung Galaxy S21"); + expect(data[2]["title"]).toBe("Black Psorotichia Lichen"); + expect(total).toBe(3); + }); + + it("ina operator should work correctly", async () => { + const { data, total } = await dataProvider(supabaseClient).getList({ + resource: "posts", + filters: [ + { + field: "tags", + operator: "ina", + value: ["health", "travel"], + }, + ], + }); + + expect(data[0]["id"]).toBe(6); + expect(data[1]["id"]).toBe(2); + expect(data[2]["id"]).toBe(1); + expect(total).toBe(3); + }); + + it("nina operator should work correctly", async () => { + const { data, total } = await dataProvider(supabaseClient).getList({ + resource: "posts", + filters: [ + { + field: "tags", + operator: "nina", + value: ["lifestyle", "personal"], + }, + ], + }); + + expect(data[0]["id"]).toBe(7); + expect(data[1]["id"]).toBe(8); + expect(data[2]["id"]).toBe(11); + expect(data[3]["id"]).toBe(5); + expect(data[4]["id"]).toBe(2); + expect(total).toBe(5); + }); + + it("nina operator should work correctly with or", async () => { + const { data, total } = await dataProvider(supabaseClient).getList({ + resource: "posts", + filters: [ + { + operator: "or", + value: [ + { + field: "tags", + operator: "ina", + value: ["technology", "education"], + }, + { + field: "tags", + operator: "nina", + value: ["lifestyle", "personal"], + }, + ], + }, + ], + }); + + expect(data[0]["title"]).toBe("Sickle Island Spleenwort"); + expect(data[1]["title"]).toBe("Oakwoods Prairie Clover"); + expect(data[2]["title"]).toBe("Funck's Wart Lichen"); + expect(data[3]["title"]).toBe("test"); + expect(data[4]["title"]).toBe("Samsung Galaxy S21"); + expect(data[5]["title"]).toBe("Black Psorotichia Lichen"); + expect(total).toBe(6); + }); + it("should change schema", async () => { const { data } = await dataProvider(supabaseClient).getList({ resource: "posts", @@ -297,7 +394,7 @@ describe("filtering", () => { expect(data.length).toBeGreaterThan(0); const promise = dataProvider(supabaseClient).getList({ - resource: "posts", + resource: "products", meta: { schema: "private", }, diff --git a/packages/supabase/test/utils/mapOperator.spec.ts b/packages/supabase/test/utils/mapOperator.spec.ts index aef9bb70764e..38b13ab79882 100644 --- a/packages/supabase/test/utils/mapOperator.spec.ts +++ b/packages/supabase/test/utils/mapOperator.spec.ts @@ -11,7 +11,9 @@ describe("mapOperator", () => { lte: "lte", gte: "gte", in: "in", + ina: "cs", nin: "not.in", + nina: "not.cs", contains: "ilike", ncontains: "not.ilike", containss: "like",