Skip to content

Commit d5ec7c0

Browse files
authored
Merge pull request #174 from hypercerts-org/fix/search_functionality
Search functionality
2 parents cbefb22 + 8a482ef commit d5ec7c0

File tree

9 files changed

+1953
-1927
lines changed

9 files changed

+1953
-1927
lines changed

schema.graphql

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -696,7 +696,11 @@ input MetadataWhereInput {
696696
}
697697

698698
input NumberArraySearchOptions {
699+
"""Array of numbers"""
699700
contains: [BigInt!]
701+
702+
"""Array of numbers"""
703+
overlaps: [BigInt!]
700704
}
701705

702706
input NumberSearchOptions {
@@ -921,6 +925,7 @@ enum SortOrder {
921925

922926
input StringArraySearchOptions {
923927
contains: [String!]
928+
overlaps: [String!]
924929
}
925930

926931
input StringSearchOptions {

src/graphql/schemas/inputs/searchOptions.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,22 @@ export class NumberSearchOptions {
6868
export class StringArraySearchOptions {
6969
@Field(() => [String], { nullable: true })
7070
contains?: string[];
71+
72+
@Field(() => [String], { nullable: true })
73+
overlaps?: string[];
7174
}
7275

7376
@InputType()
7477
export class NumberArraySearchOptions {
75-
@Field(() => [GraphQLBigInt], { nullable: true })
76-
contains?: bigint[] | number[];
78+
@Field(() => [GraphQLBigInt], {
79+
nullable: true,
80+
description: "Array of numbers",
81+
})
82+
contains?: bigint[];
83+
84+
@Field(() => [GraphQLBigInt], {
85+
nullable: true,
86+
description: "Array of numbers",
87+
})
88+
overlaps?: bigint[];
7789
}
Lines changed: 204 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,33 @@
1-
type OperandType = string | number | bigint | string[] | bigint[];
2-
type OperatorType =
3-
| "eq"
4-
| "gt"
5-
| "gte"
6-
| "lt"
7-
| "lte"
8-
| "ilike"
9-
| "contains"
10-
| "startsWith"
11-
| "endsWith";
1+
import { sql, SqlBool } from "kysely";
2+
import {
3+
NumberSearchOptions,
4+
StringSearchOptions,
5+
StringArraySearchOptions,
6+
NumberArraySearchOptions,
7+
} from "../inputs/searchOptions.js";
8+
9+
export type OperandType = string | number | bigint | string[] | bigint[];
10+
11+
export type NumericOperatorType = "eq" | "gt" | "gte" | "lt" | "lte";
12+
export type StringOperatorType = "contains" | "startsWith" | "endsWith";
13+
export type ArrayOperatorType = "overlaps" | "contains";
14+
export type OperatorType =
15+
| NumericOperatorType
16+
| StringOperatorType
17+
| ArrayOperatorType;
1218

1319
enum OperatorSymbols {
1420
eq = "=",
1521
gt = ">",
1622
gte = ">=",
1723
lt = "<",
1824
lte = "<=",
19-
ilike = "ilike",
25+
ilike = "~*",
26+
overlaps = "&&",
27+
contains = "@>",
2028
}
2129

30+
// TODO: remove when data client is updated
2231
export const generateFilterValues = (
2332
column: string,
2433
operator: OperatorType,
@@ -36,12 +45,192 @@ export const generateFilterValues = (
3645
case "lte":
3746
return [column, OperatorSymbols.lte, operand];
3847
case "contains":
39-
return [column, OperatorSymbols.ilike, operand];
48+
return [column, OperatorSymbols.ilike, `%${operand}%`];
4049
case "startsWith":
41-
return [column, OperatorSymbols.ilike, operand];
50+
return [column, OperatorSymbols.ilike, `${operand}%`];
4251
case "endsWith":
43-
return [column, OperatorSymbols.ilike, operand];
52+
return [column, OperatorSymbols.ilike, `%${operand}`];
4453
}
4554

4655
return [];
4756
};
57+
58+
export const getTablePrefix = (column: string): string => {
59+
switch (column) {
60+
case "hypercerts":
61+
return "claims";
62+
case "contract":
63+
return "contracts";
64+
case "fractions":
65+
return "fractions_view";
66+
case "metadata":
67+
return "metadata";
68+
case "attestations":
69+
return "attestations";
70+
default:
71+
return column;
72+
}
73+
};
74+
75+
export const isFilterObject = (obj: never): boolean => {
76+
const filterKeys = [
77+
"eq",
78+
"gt",
79+
"gte",
80+
"lt",
81+
"lte",
82+
"contains",
83+
"startsWith",
84+
"endsWith",
85+
"in",
86+
"overlaps",
87+
"contains",
88+
];
89+
return Object.keys(obj).some((key) => filterKeys.includes(key));
90+
};
91+
92+
// Helper functions for building conditions
93+
const buildEqualityCondition = (
94+
column: string,
95+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
96+
value: any,
97+
tableName: string,
98+
): SqlBool => sql`${sql.raw(`"${tableName}"."${column}"`)} =
99+
${value}`;
100+
101+
const buildComparisonCondition = (
102+
column: string,
103+
operator: string,
104+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
105+
value: any,
106+
tableName: string,
107+
): SqlBool =>
108+
sql`${sql.raw(`"${tableName}"."${column}"`)}
109+
${sql.raw(operator)}
110+
${value}`;
111+
112+
const buildLikeCondition = (
113+
column: string,
114+
pattern: string,
115+
tableName: string,
116+
): SqlBool => sql`${sql.raw(`"${tableName}"."${column}"`)} ILIKE
117+
${pattern}`;
118+
119+
const buildArrayCondition = (
120+
column: string,
121+
operator: string,
122+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
123+
values: any[],
124+
tableName: string,
125+
): SqlBool =>
126+
sql`${sql.raw(`"${tableName}"."${column}"`)}
127+
${sql.raw(operator)}
128+
${sql.raw(`ARRAY[${values.map((v) => `'${v}'`).join(", ")}]`)}`;
129+
130+
const conditionBuilders = {
131+
eq: buildEqualityCondition,
132+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
133+
gt: (column: string, value: any, tableName: string) =>
134+
buildComparisonCondition(column, ">", value, tableName),
135+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
136+
gte: (column: string, value: any, tableName: string) =>
137+
buildComparisonCondition(column, ">=", value, tableName),
138+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
139+
lt: (column: string, value: any, tableName: string) =>
140+
buildComparisonCondition(column, "<", value, tableName),
141+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
142+
lte: (column: string, value: any, tableName: string) =>
143+
buildComparisonCondition(column, "<=", value, tableName),
144+
contains: (column: string, value: string, tableName: string) =>
145+
buildLikeCondition(column, `%${value}%`, tableName),
146+
startsWith: (column: string, value: string, tableName: string) =>
147+
buildLikeCondition(column, `${value}%`, tableName),
148+
endsWith: (column: string, value: string, tableName: string) =>
149+
buildLikeCondition(column, `%${value}`, tableName),
150+
};
151+
152+
export const buildCondition = (
153+
column: string,
154+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
155+
value: any,
156+
tableName: string,
157+
): SqlBool => {
158+
const conditions: SqlBool[] = [];
159+
160+
if (
161+
value instanceof StringSearchOptions ||
162+
value instanceof NumberSearchOptions
163+
) {
164+
Object.entries(value).forEach(([key, val]) => {
165+
if (key in conditionBuilders && val !== undefined) {
166+
conditions.push(conditionBuilders[key](column, val, tableName));
167+
}
168+
});
169+
} else if (
170+
value instanceof StringArraySearchOptions ||
171+
value instanceof NumberArraySearchOptions
172+
) {
173+
if (value.contains && value.contains.length > 0) {
174+
conditions.push(
175+
buildArrayCondition(column, "@>", value.contains, tableName),
176+
);
177+
}
178+
if (value.overlaps && value.overlaps.length > 0) {
179+
conditions.push(
180+
buildArrayCondition(column, "&&", value.overlaps, tableName),
181+
);
182+
}
183+
} else if (typeof value === "object" && value !== null) {
184+
Object.entries(value).forEach(([key, val]) => {
185+
if (key in conditionBuilders && val !== undefined) {
186+
conditions.push(conditionBuilders[key](column, val, tableName));
187+
} else if (key === "contains" && Array.isArray(val)) {
188+
conditions.push(buildArrayCondition(column, "@>", val, tableName));
189+
} else if (key === "overlaps" && Array.isArray(val)) {
190+
conditions.push(buildArrayCondition(column, "&&", val, tableName));
191+
}
192+
});
193+
}
194+
195+
return sql.join(conditions, sql` AND `);
196+
};
197+
198+
export const buildWhereCondition = <T extends string>(
199+
column: string,
200+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
201+
value: any,
202+
tableName: T,
203+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
204+
eb: any,
205+
): SqlBool | null => {
206+
if (!column || value === undefined) return null;
207+
208+
if (typeof value === "object" && value !== null) {
209+
if (isFilterObject(value)) {
210+
return buildCondition(column, value, tableName);
211+
}
212+
213+
const relatedTable = getTablePrefix(column);
214+
const nestedConditions: SqlBool[] = [];
215+
216+
for (const [nestedColumn, nestedValue] of Object.entries(value)) {
217+
if (!nestedColumn || nestedValue === undefined) continue;
218+
const nestedCondition = buildWhereCondition(
219+
nestedColumn,
220+
nestedValue,
221+
relatedTable,
222+
eb,
223+
);
224+
if (nestedCondition) {
225+
nestedConditions.push(nestedCondition);
226+
}
227+
}
228+
229+
return nestedConditions.length > 0
230+
? sql.join(nestedConditions, sql` AND `)
231+
: null;
232+
}
233+
234+
return sql`${sql.raw(`"${tableName}"."${column}"`)} =
235+
${value}`;
236+
};
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { expressionBuilder, Kysely, SqlBool } from "kysely";
2+
import { BaseArgs } from "../graphql/schemas/args/baseArgs.js";
3+
import { SortOrder } from "../graphql/schemas/enums/sortEnums.js";
4+
import { buildWhereCondition } from "../graphql/schemas/utils/filters-kysely.js";
5+
6+
export abstract class BaseSupabaseService<DB> {
7+
protected db: Kysely<DB>;
8+
9+
protected constructor(db: Kysely<DB>) {
10+
this.db = db;
11+
}
12+
13+
abstract getDataQuery<T extends keyof DB & string, A extends object>(
14+
tableName: T,
15+
args: BaseArgs<A>, // eslint-disable-next-line @typescript-eslint/no-explicit-any
16+
): any;
17+
18+
abstract getCountQuery<T extends keyof DB & string, A extends object>(
19+
tableName: T,
20+
args: BaseArgs<A>, // eslint-disable-next-line @typescript-eslint/no-explicit-any
21+
): any;
22+
23+
handleGetData<T extends keyof DB & string, A extends object>(
24+
tableName: T,
25+
args: BaseArgs<A> & {
26+
first?: number;
27+
offset?: number;
28+
},
29+
) {
30+
let query = this.getDataQuery(tableName, args);
31+
const { where, first, offset, sort } = args;
32+
const eb = expressionBuilder(query);
33+
34+
if (where) {
35+
query = this.applyWhereConditions(query, where, tableName, eb);
36+
}
37+
38+
if (sort?.by) {
39+
query = this.applySorting(query, sort.by);
40+
}
41+
42+
if (first) query = query.limit(first);
43+
if (offset) query = query.offset(offset);
44+
45+
return query;
46+
}
47+
48+
handleGetCount<T extends keyof DB & string, A extends object>(
49+
tableName: T,
50+
args: BaseArgs<A> & {
51+
first?: number;
52+
offset?: number;
53+
},
54+
) {
55+
let query = this.getCountQuery(tableName, args);
56+
57+
const { where } = args;
58+
const eb = expressionBuilder(query);
59+
60+
if (where) {
61+
query = this.applyWhereConditions(query, where, tableName, eb);
62+
}
63+
64+
return query;
65+
}
66+
67+
private applyWhereConditions<T extends string>(
68+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
69+
query: any,
70+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
71+
where: any,
72+
tableName: T,
73+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
74+
eb: any,
75+
) {
76+
const conditions = Object.entries(where)
77+
.map(([column, value]) =>
78+
buildWhereCondition(column, value, tableName, eb),
79+
)
80+
.filter(Boolean);
81+
82+
return conditions.reduce((q, condition) => {
83+
return q.where(condition as SqlBool);
84+
}, query);
85+
}
86+
87+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
88+
private applySorting(query: any, sortBy: any) {
89+
for (const [column, direction] of Object.entries(sortBy)) {
90+
if (!column || !direction) continue;
91+
const dir: "asc" | "desc" =
92+
direction === SortOrder.ascending ? "asc" : "desc";
93+
query = query.orderBy(column, dir);
94+
}
95+
return query;
96+
}
97+
}

0 commit comments

Comments
 (0)