Skip to content

Commit 5df0b8b

Browse files
committed
feat(db): enhance applyWhere with comprehensive documentation and robust testing
Improved the applyWhere utility with: - Detailed JSDoc documentation explaining function purpose, type parameters, and usage - Comprehensive test suite covering various filtering scenarios, comparison operators, and query builder integration - Enhanced testing using pg-mem for in-memory database simulation
1 parent 8976be3 commit 5df0b8b

3 files changed

Lines changed: 233 additions & 73 deletions

File tree

src/lib/db/queryModifiers/applyWhere.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,36 @@ import {
88
} from "../../../lib/graphql/buildWhereCondition.js";
99

1010
/**
11-
* Applies where conditions to a query based on the provided arguments
12-
* @param tableName The name of the table to query
13-
* @param query The query to apply the where conditions to
14-
* @param args The arguments containing where conditions
11+
* Applies where conditions to a query based on the provided arguments.
12+
* This function processes each condition in the where clause and applies them to the query.
13+
*
14+
* @typeParam DB - The database type extending SupportedDatabases
15+
* @typeParam T - The table name type (must be a key of DB and a string)
16+
* @typeParam Args - The arguments type extending BaseQueryArgsType
17+
*
18+
* @param tableName - The name of the table to query
19+
* @param query - The Kysely SelectQueryBuilder instance to apply where conditions to
20+
* @param args - The arguments containing where conditions
21+
*
1522
* @returns The modified query with where conditions applied
23+
*
24+
* @remarks
25+
* - If no where conditions are provided (args.where is undefined), returns the original query
26+
* - Each property in the where object is processed independently
27+
* - Invalid conditions (those that return undefined from buildWhereCondition) are skipped
28+
* - The conditions are applied in sequence using AND logic
29+
*
30+
* @example
31+
* ```typescript
32+
* const query = db.selectFrom('users');
33+
* const args = {
34+
* where: {
35+
* name: { eq: "John" },
36+
* age: { gt: 18 }
37+
* }
38+
* };
39+
* const result = applyWhere('users', query, args);
40+
* ```
1641
*/
1742
export function applyWhere<
1843
DB extends SupportedDatabases,

src/lib/graphql/buildWhereCondition.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ const isNestedFilter = (value: FilterValue): value is NestedFilterValue =>
102102
*
103103
* @type {Record<FilterOperator, FilterBuilder>}
104104
*/
105+
// TODO: add support for negated filters
105106
const filterBuilders: Record<FilterOperator, FilterBuilder> = {
106107
eq: (tableName, column, value) =>
107108
sql<SqlBool>`${sql.raw(`"${tableName}"."${column}"`)} = ${value}`,
Lines changed: 203 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,220 @@
1-
import { sql } from "kysely";
2-
import { beforeEach, describe, expect, it, vi } from "vitest";
1+
import { describe, it, expect, beforeEach } from "vitest";
2+
import { Kysely } from "kysely";
3+
import { IMemoryDb, newDb } from "pg-mem";
34
import { applyWhere } from "../../../../src/lib/db/queryModifiers/applyWhere.js";
4-
import { FilterValue } from "../../../../src/lib/graphql/buildWhereCondition.js";
5+
import { DataDatabase } from "../../../../src/types/kyselySupabaseData.js";
56

6-
// Mock the buildWhereCondition function
7-
vi.mock("../../../../src/lib/graphql/buildWhereCondition.js", () => ({
8-
buildWhereCondition: vi.fn(),
9-
FilterValue: {},
10-
}));
11-
12-
// Import the mocked module
13-
import { buildWhereCondition } from "../../../../src/lib/graphql/buildWhereCondition.js";
7+
type TestDatabase = DataDatabase & {
8+
test_users: {
9+
id: number;
10+
name: string;
11+
age: number;
12+
active: boolean;
13+
created_at: Date;
14+
tags: string[];
15+
};
16+
};
1417

1518
describe("applyWhere", () => {
16-
const mockQuery = {
17-
where: vi.fn().mockReturnThis(),
18-
};
19+
let db: Kysely<TestDatabase>;
20+
let mem: IMemoryDb;
1921

20-
// Reset mocks before each test
2122
beforeEach(() => {
22-
vi.clearAllMocks();
23+
mem = newDb();
24+
db = mem.adapters.createKysely();
25+
26+
// Create test table
27+
mem.public.none(`
28+
CREATE TABLE test_users (
29+
id SERIAL PRIMARY KEY,
30+
name TEXT NOT NULL,
31+
age INTEGER NOT NULL,
32+
active BOOLEAN NOT NULL DEFAULT true,
33+
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
34+
tags TEXT[] NOT NULL DEFAULT '{}'
35+
);
36+
`);
37+
});
38+
39+
describe("basic functionality", () => {
40+
it("should return original query when no where clause is provided", () => {
41+
const baseQuery = db.selectFrom("test_users").selectAll() as any;
42+
const result = applyWhere<TestDatabase, "test_users", any>(
43+
"test_users",
44+
baseQuery,
45+
{},
46+
);
47+
48+
const { sql, parameters } = result.compile();
49+
expect(sql).not.toContain("where");
50+
expect(parameters).toEqual([]);
51+
});
52+
53+
it("should apply simple equality condition", () => {
54+
const baseQuery = db.selectFrom("test_users").selectAll() as any;
55+
const result = applyWhere<TestDatabase, "test_users", any>(
56+
"test_users",
57+
baseQuery,
58+
{
59+
where: { name: { eq: "John" } },
60+
},
61+
);
62+
63+
const { sql, parameters } = result.compile();
64+
expect(sql).toMatch(/where.*"test_users"."name".*=.*\$1/i);
65+
expect(parameters).toEqual(["John"]);
66+
});
67+
68+
it("should apply multiple conditions with AND", () => {
69+
const baseQuery = db.selectFrom("test_users").selectAll() as any;
70+
const result = applyWhere<TestDatabase, "test_users", any>(
71+
"test_users",
72+
baseQuery,
73+
{
74+
where: {
75+
name: { eq: "John" },
76+
age: { gt: 18 },
77+
},
78+
},
79+
);
80+
81+
const { sql, parameters } = result.compile();
82+
expect(sql).toMatch(
83+
/where.*"test_users"."name".*=.*\$1.*and.*"test_users"."age".*>.*\$2/i,
84+
);
85+
expect(parameters).toEqual(["John", 18]);
86+
});
87+
});
88+
89+
describe("comparison operators", () => {
90+
it("should handle greater than condition", () => {
91+
const baseQuery = db.selectFrom("test_users").selectAll() as any;
92+
const result = applyWhere<TestDatabase, "test_users", any>(
93+
"test_users",
94+
baseQuery,
95+
{
96+
where: { age: { gt: 18 } },
97+
},
98+
);
2399

24-
// Default implementation for buildWhereCondition
25-
vi.mocked(buildWhereCondition).mockImplementation((tableName, where) => {
26-
// Simple mock implementation that returns a SQL condition for testing
27-
const column = Object.keys(where)[0];
28-
return sql`${sql.raw(`"${tableName}"."${column}"`)} = 'test'`;
100+
const { sql, parameters } = result.compile();
101+
expect(sql).toMatch(/where.*"test_users"."age".*>.*\$1/i);
102+
expect(parameters).toEqual([18]);
103+
});
104+
105+
it("should handle less than or equal condition", () => {
106+
const baseQuery = db.selectFrom("test_users").selectAll() as any;
107+
const result = applyWhere<TestDatabase, "test_users", any>(
108+
"test_users",
109+
baseQuery,
110+
{
111+
where: { age: { lte: 65 } },
112+
},
113+
);
114+
115+
const { sql, parameters } = result.compile();
116+
expect(sql).toMatch(/where.*"test_users"."age".*<=.*\$1/i);
117+
expect(parameters).toEqual([65]);
29118
});
30119
});
31120

32-
it("should return the original query if where is not provided", () => {
33-
const args = { first: 10, offset: 0 };
34-
const result = applyWhere<any, any, any>(
35-
"test_table" as any,
36-
mockQuery as any,
37-
args,
38-
);
121+
describe("text search conditions", () => {
122+
it("should handle contains condition", () => {
123+
const baseQuery = db.selectFrom("test_users").selectAll() as any;
124+
const result = applyWhere<TestDatabase, "test_users", any>(
125+
"test_users",
126+
baseQuery,
127+
{
128+
where: { name: { contains: "oh" } },
129+
},
130+
);
131+
132+
const { sql, parameters } = result.compile();
133+
expect(sql).toMatch(
134+
/where.*lower.*"test_users"."name".*like.*lower.*\$1/i,
135+
);
136+
expect(parameters).toEqual(["%oh%"]);
137+
});
138+
139+
it("should handle startsWith condition", () => {
140+
const baseQuery = db.selectFrom("test_users").selectAll() as any;
141+
const result = applyWhere<TestDatabase, "test_users", any>(
142+
"test_users",
143+
baseQuery,
144+
{
145+
where: { name: { startsWith: "Jo" } },
146+
},
147+
);
39148

40-
expect(result).toBe(mockQuery);
41-
expect(mockQuery.where).not.toHaveBeenCalled();
149+
const { sql, parameters } = result.compile();
150+
expect(sql).toMatch(
151+
/where.*lower.*"test_users"."name".*like.*lower.*\$1/i,
152+
);
153+
expect(parameters).toEqual(["Jo%"]);
154+
});
42155
});
43156

44-
it("should apply where conditions for each property in the where object", () => {
45-
const args = {
46-
first: 10,
47-
offset: 0,
48-
where: {
49-
name: { eq: "test" } as FilterValue,
50-
age: { gt: 18 } as FilterValue,
51-
},
52-
};
53-
54-
const result = applyWhere<any, any, any>(
55-
"test_table" as any,
56-
mockQuery as any,
57-
args,
58-
);
59-
60-
expect(result).toBe(mockQuery);
61-
expect(mockQuery.where).toHaveBeenCalledTimes(2);
157+
describe("array conditions", () => {
158+
it("should handle array contains condition", () => {
159+
const baseQuery = db.selectFrom("test_users").selectAll() as any;
160+
const result = applyWhere<TestDatabase, "test_users", any>(
161+
"test_users",
162+
baseQuery,
163+
{
164+
where: { tags: { arrayContains: ["tag1", "tag2"] } },
165+
},
166+
);
167+
168+
const { sql, parameters } = result.compile();
169+
expect(sql).toMatch(/where.*"test_users"."tags".*@>.*array\[\$1, \$2\]/i);
170+
expect(parameters).toEqual(["tag1", "tag2"]);
171+
});
62172
});
63173

64-
it("should skip properties that don't generate a valid condition", () => {
65-
// Mock buildWhereCondition to return undefined for the first call
66-
vi.mocked(buildWhereCondition).mockImplementationOnce(() => undefined);
67-
68-
const args = {
69-
first: 10,
70-
offset: 0,
71-
where: {
72-
invalid: { eq: "test" } as FilterValue,
73-
valid: { eq: "test" } as FilterValue,
74-
},
75-
};
76-
77-
const result = applyWhere<any, any, any>(
78-
"test_table" as any,
79-
mockQuery as any,
80-
args,
81-
);
82-
83-
expect(result).toBe(mockQuery);
84-
expect(mockQuery.where).toHaveBeenCalledTimes(1);
174+
describe("query builder integration", () => {
175+
it("should work with complex queries", () => {
176+
const baseQuery = db
177+
.selectFrom("test_users")
178+
.selectAll()
179+
.orderBy("created_at") as any;
180+
181+
const result = applyWhere<TestDatabase, "test_users", any>(
182+
"test_users",
183+
baseQuery,
184+
{
185+
where: {
186+
active: { eq: true },
187+
age: { gt: 18 },
188+
},
189+
},
190+
);
191+
192+
const { sql, parameters } = result.compile();
193+
expect(sql).toContain("where");
194+
expect(sql).toContain("order by");
195+
expect(parameters).toEqual([true, 18]);
196+
});
197+
198+
it("should preserve existing query modifiers", () => {
199+
const baseQuery = db
200+
.selectFrom("test_users")
201+
.selectAll()
202+
.orderBy("created_at")
203+
.limit(10) as any;
204+
205+
const result = applyWhere<TestDatabase, "test_users", any>(
206+
"test_users",
207+
baseQuery,
208+
{
209+
where: { active: { eq: true } },
210+
},
211+
);
212+
213+
const { sql, parameters } = result.compile();
214+
expect(sql).toContain("where");
215+
expect(sql).toContain("order by");
216+
expect(sql).toContain("limit");
217+
expect(parameters).toEqual([true, 10]);
218+
});
85219
});
86220
});

0 commit comments

Comments
 (0)