Skip to content
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

feat(appwrite): add support to conditional filters and missing logical filters #6256

Merged
5 changes: 5 additions & 0 deletions .changeset/wicked-rats-breathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@refinedev/appwrite": patch
---

Add Support to `and`, `or`, `between`, `null`, `nnull`, `startswith` and `endswith` operators
45 changes: 44 additions & 1 deletion packages/appwrite/src/utils/generateFilter.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
import type { CrudFilter } from "@refinedev/core";
import { Query } from "appwrite";

export const generateFilter = (filter: CrudFilter) => {
/**
* Generate a filter string for Appwrite from Refine's filter
* @param filter Refine's filter
* @param deep Max deep of the filter
* @returns Appwrite's filter string
*/
export const generateFilter = (filter: CrudFilter, deep = 10): string => {
const nextDeep = deep - 1;

if (nextDeep < 0) {
throw new Error("Max deep reached");
}

switch (filter.operator) {
// Logical operators
case "eq":
return Query.equal(filter.field, filter.value);
case "ne":
Expand All @@ -17,6 +30,36 @@ export const generateFilter = (filter: CrudFilter) => {
return Query.lessThanEqual(filter.field, filter.value);
case "contains":
return Query.search(filter.field, `%${filter.value}%`);
case "between":
if (!Array.isArray(filter.value) || filter.value.length !== 2) {
throw new Error(
`Value array must contain exactly two elements for "between" operator`,
);
}
return Query.between(filter.field, filter.value[0], filter.value[1]);
case "null":
return Query.isNull(filter.field);
case "nnull":
return Query.isNotNull(filter.field);
case "startswith":
return Query.startsWith(filter.field, filter.value);
case "endswith":
return Query.endsWith(filter.field, filter.value);

// Conditional operators
case "or":
if (filter.value.length === 1 && filter.value[0]) {
//? "OR" queries require at least two queries in Appwrite
return generateFilter(filter.value[0], nextDeep);
}
return Query.or(filter.value.map((f) => generateFilter(f, nextDeep)));
case "and":
if (filter.value.length === 1 && filter.value[0]) {
//? "AND" queries require at least two queries in Appwrite
return generateFilter(filter.value[0], nextDeep);
}
return Query.and(filter.value.map((f) => generateFilter(f, nextDeep)));

default:
throw new Error(`Operator ${filter.operator} is not supported`);
}
Expand Down
16 changes: 2 additions & 14 deletions packages/appwrite/src/utils/getAppwriteFilters.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,14 @@
import type { CrudFilters } from "@refinedev/core";
import { generateFilter } from "./generateFilter";
import { replaceIdWithAppwriteId } from "./replaceIdWithAppwriteId";

type GetAppwriteFiltersType = (filters?: CrudFilters) => string[];

export const getAppwriteFilters: GetAppwriteFiltersType = (filters) => {
const appwriteFilters: string[] = [];

for (const filter of filters ?? []) {
if (
filter.operator !== "or" &&
filter.operator !== "and" &&
"field" in filter
) {
const filterField = filter.field === "id" ? "$id" : filter.field;

appwriteFilters.push(
generateFilter({
...filter,
field: filterField,
}),
);
}
appwriteFilters.push(generateFilter(replaceIdWithAppwriteId(filter)));
}

return appwriteFilters;
Expand Down
20 changes: 20 additions & 0 deletions packages/appwrite/src/utils/replaceIdWithAppwriteId.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { CrudFilter } from "@refinedev/core";

/**
* Replace ID("id") With Appwrite ID("$id") recursively
* @param filter Filter to replace
* @returns Filter with replaced ID
*/
export const replaceIdWithAppwriteId = (filter: CrudFilter): CrudFilter => {
if ("field" in filter && filter.field === "id") {
filter.field = "$id";
}

if (Array.isArray(filter.value)) {
return {
...filter,
value: filter.value.map(replaceIdWithAppwriteId),
};
}
return filter;
};
79 changes: 79 additions & 0 deletions packages/appwrite/test/utils/generateFilter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,73 @@ describe("generateFilter", () => {
filter: { operator: "contains", field: "name", value: "John" },
expected: Query.search("name", "%John%"),
},
{
filter: { operator: "between", field: "age", value: [0, 64] },
expected: Query.between("age", 0, 64),
},
{
filter: { operator: "null", field: "name", value: undefined },
expected: Query.isNull("name"),
},
{
filter: { operator: "nnull", field: "name", value: undefined },
expected: Query.isNotNull("name"),
},
{
filter: { operator: "startswith", field: "name", value: "John" },
expected: Query.startsWith("name", "John"),
},
{
filter: { operator: "endswith", field: "name", value: "John" },
expected: Query.endsWith("name", "John"),
},
{
filter: {
operator: "or",
value: [
{
operator: "eq",
field: "name",
value: "John",
},
{
operator: "lt",
field: "age",
value: 30,
},
],
},
expected: Query.or([
Query.equal("name", "John"),
Query.lessThan("age", 30),
]),
},
{
filter: {
operator: "or",
value: [
{
operator: "eq",
field: "name",
value: "John",
},
],
},
expected: Query.equal("name", "John"),
},
{
filter: {
operator: "and",
value: [
{
operator: "eq",
field: "name",
value: "John",
},
],
},
expected: Query.equal("name", "John"),
},
];

testCases.forEach(({ filter, expected }) => {
Expand All @@ -41,6 +108,18 @@ describe("generateFilter", () => {
});
});

it("should throw an error when value array has only one element for 'between' operator", () => {
const filter = {
operator: "between",
field: "age",
value: [0],
} as CrudFilter;

expect(() => generateFilter(filter)).toThrowError(
'Value array must contain exactly two elements for "between" operator',
);
});

it("should throw an error for unsupported operator", () => {
const filter = {
operator: "unsupported",
Expand Down
66 changes: 62 additions & 4 deletions packages/appwrite/test/utils/getAppwriteFilters.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,73 @@ describe("getAppwriteFilters", () => {
});
});

it('should not generate Appwrite filters for "or" and "and" operators', () => {
it('should generate Appwrite filters for "or" and "and" operators', () => {
const filters = [
{ operator: "or", filters: [] },
{ operator: "and", filters: [] },
{
operator: "or",
filters: [
{
operator: "eq",
field: "name",
value: "John",
},
{
operator: "lt",
field: "age",
value: 30,
},
],
},
{
operator: "and",
filters: [
{
operator: "eq",
field: "name",
value: "John",
},
{
operator: "lt",
field: "age",
value: 30,
},
],
},
];

getAppwriteFilters(filters as any);

expect(generateFilter).toHaveBeenCalledTimes(0);
expect(generateFilter).toHaveBeenCalledTimes(2);
expect(generateFilter).toHaveBeenNthCalledWith(1, {
operator: "or",
filters: [
{
operator: "eq",
field: "name",
value: "John",
},
{
operator: "lt",
field: "age",
value: 30,
},
],
});
expect(generateFilter).toHaveBeenNthCalledWith(2, {
operator: "and",
filters: [
{
operator: "eq",
field: "name",
value: "John",
},
{
operator: "lt",
field: "age",
value: 30,
},
],
});
});

it('should replace "id" field with "$id"', () => {
Expand Down
55 changes: 55 additions & 0 deletions packages/appwrite/test/utils/replaceIdWithAppwriteId.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { CrudFilter } from "@refinedev/core";
import { replaceIdWithAppwriteId } from "../../src/utils/replaceIdWithAppwriteId";

describe("replaceIdWithAppwriteId", () => {
it("should replace the id with appwrite id", () => {
const result = replaceIdWithAppwriteId({
field: "id",
operator: "eq",
value: "John Doe",
} satisfies CrudFilter);
expect(result).toStrictEqual({
field: "$id",
operator: "eq",
value: "John Doe",
});
});

it("should replace the id with appwrite id with complex filter", () => {
const result = replaceIdWithAppwriteId({
field: "id",
operator: "eq",
value: [
{
field: "id",
operator: "eq",
value: "John Doe",
},
],
} satisfies CrudFilter);
expect(result).toStrictEqual({
field: "$id",
operator: "eq",
value: [
{
field: "$id",
operator: "eq",
value: "John Doe",
},
],
});
});

it("should not replace the other field value with appwrite id", () => {
const result = replaceIdWithAppwriteId({
field: "name",
operator: "eq",
value: "John Doe",
} satisfies CrudFilter);
expect(result).toStrictEqual({
field: "name",
operator: "eq",
value: "John Doe",
});
});
});
Loading