Skip to content

Commit 7119317

Browse files
committed
feat(db): enhance applySort with comprehensive documentation and robust testing
Improved the applySort utility with: - Detailed JSDoc documentation explaining function purpose, type parameters, and usage - Comprehensive test suite covering various sorting scenarios, query builder integration, and data validation - Enhanced testing using pg-mem for in-memory database simulation - Removed error handling try-catch block to simplify implementation
1 parent 5df0b8b commit 7119317

2 files changed

Lines changed: 237 additions & 112 deletions

File tree

src/lib/db/queryModifiers/applySort.ts

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,36 @@ import { SortOrder } from "../../../graphql/schemas/enums/sortEnums.js";
33
import { SupportedDatabases } from "../../../services/database/strategies/QueryStrategy.js";
44

55
/**
6-
* Applies sorting to a query based on the provided arguments
7-
* @param query The query to apply sorting to
8-
* @param args The arguments containing sort conditions
6+
* Applies sorting to a query based on the provided arguments.
7+
* This function processes each sort condition and applies them in sequence to the query.
8+
*
9+
* @typeParam DB - The database type extending SupportedDatabases
10+
* @typeParam T - The table name type (must be a key of DB and a string)
11+
* @typeParam Args - The arguments type containing optional sortBy property
12+
*
13+
* @param query - The Kysely SelectQueryBuilder instance to apply sorting to
14+
* @param args - The arguments containing sort conditions
15+
*
916
* @returns The modified query with sorting applied
17+
*
18+
* @remarks
19+
* - If no sort conditions are provided (args.sortBy is undefined), returns the original query
20+
* - Null or undefined sort directions are filtered out
21+
* - Sort conditions are applied in sequence, maintaining the order specified
22+
* - TypeScript type checking should prevent invalid field names at compile time
23+
* - SortOrder.ascending maps to 'asc', SortOrder.descending maps to 'desc'
24+
*
25+
* @example
26+
* ```typescript
27+
* const query = db.selectFrom('users');
28+
* const args = {
29+
* sortBy: {
30+
* name: SortOrder.ascending,
31+
* created_at: SortOrder.descending
32+
* }
33+
* };
34+
* const result = applySort(query, args);
35+
* ```
1036
*/
1137
export function applySort<
1238
DB extends SupportedDatabases,
@@ -38,15 +64,10 @@ export function applySort<
3864

3965
for (const [field, direction] of sortEntries) {
4066
const orderDirection = direction === SortOrder.ascending ? "asc" : "desc";
41-
42-
try {
43-
modifiedQuery = modifiedQuery.orderBy(
44-
field as keyof DB[T] & string,
45-
orderDirection,
46-
);
47-
} catch (error) {
48-
// Silently ignore invalid sort fields
49-
}
67+
modifiedQuery = modifiedQuery.orderBy(
68+
field as keyof DB[T] & string,
69+
orderDirection,
70+
);
5071
}
5172

5273
return modifiedQuery;
Lines changed: 204 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,122 +1,226 @@
1-
import { beforeEach, describe, expect, it, vi } from "vitest";
2-
import { SortOrder } from "../../../../src/graphql/schemas/enums/sortEnums.js";
1+
import { describe, it, expect, beforeEach } from "vitest";
2+
import { Kysely } from "kysely";
3+
import { IMemoryDb, newDb } from "pg-mem";
34
import { applySort } from "../../../../src/lib/db/queryModifiers/applySort.js";
5+
import { SortOrder } from "../../../../src/graphql/schemas/enums/sortEnums.js";
6+
import { DataDatabase } from "../../../../src/types/kyselySupabaseData.js";
7+
8+
type TestDatabase = DataDatabase & {
9+
test_users: {
10+
id: number;
11+
name: string;
12+
age: number;
13+
active: boolean;
14+
created_at: Date;
15+
score: number;
16+
};
17+
};
418

519
describe("applySort", () => {
6-
// Create a mock query with orderBy method
7-
const mockQuery = {
8-
orderBy: vi.fn().mockReturnThis(),
9-
};
20+
let db: Kysely<TestDatabase>;
21+
let mem: IMemoryDb;
1022

11-
// Reset mocks before each test
1223
beforeEach(() => {
13-
vi.clearAllMocks();
14-
// Reset console.debug to avoid polluting test output
15-
vi.spyOn(console, "debug").mockImplementation(() => {});
24+
mem = newDb();
25+
db = mem.adapters.createKysely();
26+
27+
// Create test table
28+
mem.public.none(`
29+
CREATE TABLE test_users (
30+
id SERIAL PRIMARY KEY,
31+
name TEXT NOT NULL,
32+
age INTEGER NOT NULL,
33+
active BOOLEAN NOT NULL DEFAULT true,
34+
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
35+
score NUMERIC NOT NULL DEFAULT 0
36+
);
37+
`);
38+
39+
// Insert some test data
40+
mem.public.none(`
41+
INSERT INTO test_users (name, age, score, created_at) VALUES
42+
('Alice', 25, 100, '2024-01-01'),
43+
('Bob', 30, 85, '2024-01-02'),
44+
('Charlie', 20, 95, '2024-01-03');
45+
`);
1646
});
1747

18-
it("should return the original query if sortBy is not provided", () => {
19-
const args = { first: 10, offset: 0 };
20-
const result = applySort(mockQuery as any, args);
48+
describe("basic functionality", () => {
49+
it("should return original query when no sort is provided", () => {
50+
const baseQuery = db.selectFrom("test_users").selectAll() as any;
51+
const result = applySort<TestDatabase, "test_users", any>(baseQuery, {});
2152

22-
expect(result).toBe(mockQuery);
23-
expect(mockQuery.orderBy).not.toHaveBeenCalled();
24-
expect(console.debug).toHaveBeenCalledWith("No sort arguments provided");
25-
});
53+
const { sql, parameters } = result.compile();
54+
expect(sql).not.toContain("order by");
55+
expect(parameters).toEqual([]);
56+
});
2657

27-
it("should return the original query if sortBy has no non-null values", () => {
28-
const args = {
29-
first: 10,
30-
offset: 0,
31-
sortBy: {
32-
name: null,
33-
age: undefined,
34-
},
35-
};
36-
37-
const result = applySort(mockQuery as any, args);
38-
39-
expect(result).toBe(mockQuery);
40-
expect(mockQuery.orderBy).not.toHaveBeenCalled();
41-
expect(console.debug).toHaveBeenCalledWith("No non-null sort fields found");
42-
});
58+
it("should apply single ascending sort", () => {
59+
const baseQuery = db.selectFrom("test_users").selectAll() as any;
60+
const result = applySort<TestDatabase, "test_users", any>(baseQuery, {
61+
sortBy: { name: SortOrder.ascending },
62+
});
4363

44-
it("should apply orderBy for each non-null sort field with ascending order", () => {
45-
const args = {
46-
first: 10,
47-
offset: 0,
48-
sortBy: {
49-
name: SortOrder.ascending,
50-
age: SortOrder.ascending,
51-
},
52-
};
53-
54-
const result = applySort(mockQuery as any, args);
55-
56-
expect(result).toBe(mockQuery);
57-
expect(mockQuery.orderBy).toHaveBeenCalledTimes(2);
58-
expect(mockQuery.orderBy).toHaveBeenCalledWith("name", "asc");
59-
expect(mockQuery.orderBy).toHaveBeenCalledWith("age", "asc");
64+
const { sql } = result.compile();
65+
expect(sql).toMatch(/order by.*"name".*asc/i);
66+
});
67+
68+
it("should apply single descending sort", () => {
69+
const baseQuery = db.selectFrom("test_users").selectAll() as any;
70+
const result = applySort<TestDatabase, "test_users", any>(baseQuery, {
71+
sortBy: { age: SortOrder.descending },
72+
});
73+
74+
const { sql } = result.compile();
75+
expect(sql).toMatch(/order by.*"age".*desc/i);
76+
});
6077
});
6178

62-
it("should apply orderBy for each non-null sort field with descending order", () => {
63-
const args = {
64-
first: 10,
65-
offset: 0,
66-
sortBy: {
67-
name: SortOrder.descending,
68-
age: SortOrder.descending,
69-
},
70-
};
71-
72-
const result = applySort(mockQuery as any, args);
73-
74-
expect(result).toBe(mockQuery);
75-
expect(mockQuery.orderBy).toHaveBeenCalledTimes(2);
76-
expect(mockQuery.orderBy).toHaveBeenCalledWith("name", "desc");
77-
expect(mockQuery.orderBy).toHaveBeenCalledWith("age", "desc");
79+
describe("multiple sort conditions", () => {
80+
it("should apply multiple sort conditions in order", () => {
81+
const baseQuery = db.selectFrom("test_users").selectAll() as any;
82+
const result = applySort<TestDatabase, "test_users", any>(baseQuery, {
83+
sortBy: {
84+
score: SortOrder.descending,
85+
name: SortOrder.ascending,
86+
},
87+
});
88+
89+
const { sql } = result.compile();
90+
expect(sql).toMatch(/order by.*"score".*desc.*"name".*asc/i);
91+
});
92+
93+
it("should handle mixed sort directions", () => {
94+
const baseQuery = db.selectFrom("test_users").selectAll() as any;
95+
const result = applySort<TestDatabase, "test_users", any>(baseQuery, {
96+
sortBy: {
97+
age: SortOrder.ascending,
98+
score: SortOrder.descending,
99+
name: SortOrder.ascending,
100+
},
101+
});
102+
103+
const { sql } = result.compile();
104+
expect(sql).toMatch(/order by.*"age".*asc.*"score".*desc.*"name".*asc/i);
105+
});
78106
});
79107

80-
it("should handle mixed sort directions", () => {
81-
const args = {
82-
first: 10,
83-
offset: 0,
84-
sortBy: {
85-
name: SortOrder.ascending,
86-
age: SortOrder.descending,
87-
created_at: null, // Should be ignored
88-
},
89-
};
90-
91-
const result = applySort(mockQuery as any, args);
92-
93-
expect(result).toBe(mockQuery);
94-
expect(mockQuery.orderBy).toHaveBeenCalledTimes(2);
95-
expect(mockQuery.orderBy).toHaveBeenCalledWith("name", "asc");
96-
expect(mockQuery.orderBy).toHaveBeenCalledWith("age", "desc");
108+
describe("edge cases", () => {
109+
it("should ignore null and undefined sort values", () => {
110+
const baseQuery = db.selectFrom("test_users").selectAll() as any;
111+
const result = applySort<TestDatabase, "test_users", any>(baseQuery, {
112+
sortBy: {
113+
name: null,
114+
age: undefined,
115+
score: SortOrder.ascending,
116+
},
117+
});
118+
119+
const { sql } = result.compile();
120+
expect(sql).toMatch(/order by.*"score".*asc/i);
121+
expect(sql).not.toMatch(/"test_users"."name"/);
122+
expect(sql).not.toMatch(/"test_users"."age"/);
123+
});
124+
125+
it("should return original query when all sort values are null/undefined", () => {
126+
const baseQuery = db.selectFrom("test_users").selectAll() as any;
127+
const result = applySort<TestDatabase, "test_users", any>(baseQuery, {
128+
sortBy: {
129+
name: null,
130+
age: undefined,
131+
},
132+
});
133+
134+
const { sql } = result.compile();
135+
expect(sql).not.toContain("order by");
136+
});
97137
});
98138

99-
it("should silently ignore errors when applying orderBy", () => {
100-
// Mock orderBy to throw an error on the second call
101-
mockQuery.orderBy
102-
.mockImplementationOnce(() => mockQuery)
103-
.mockImplementationOnce(() => {
104-
throw new Error("Invalid field");
139+
describe("query builder integration", () => {
140+
it("should work with existing where conditions", () => {
141+
const baseQuery = db
142+
.selectFrom("test_users")
143+
.selectAll()
144+
.where("active", "=", true) as any;
145+
146+
const result = applySort<TestDatabase, "test_users", any>(baseQuery, {
147+
sortBy: { name: SortOrder.ascending },
105148
});
106149

107-
const args = {
108-
first: 10,
109-
offset: 0,
110-
sortBy: {
111-
name: SortOrder.ascending,
112-
invalid_field: SortOrder.descending,
113-
},
114-
};
150+
const { sql } = result.compile();
151+
expect(sql).toContain("where");
152+
expect(sql).toMatch(/order by.*"name".*asc/i);
153+
});
154+
155+
it("should preserve existing order by clauses", () => {
156+
const baseQuery = db
157+
.selectFrom("test_users")
158+
.selectAll()
159+
.orderBy("id", "asc") as any;
115160

116-
// This should not throw an error
117-
const result = applySort(mockQuery as any, args);
161+
const result = applySort<TestDatabase, "test_users", any>(baseQuery, {
162+
sortBy: { name: SortOrder.ascending },
163+
});
164+
165+
const { sql } = result.compile();
166+
expect(sql).toMatch(/order by.*"id".*asc.*"name".*asc/i);
167+
});
168+
169+
it("should work with limit and offset", () => {
170+
const baseQuery = db
171+
.selectFrom("test_users")
172+
.selectAll()
173+
.limit(10)
174+
.offset(20) as any;
175+
176+
const result = applySort<TestDatabase, "test_users", any>(baseQuery, {
177+
sortBy: { name: SortOrder.ascending },
178+
});
179+
180+
const { sql, parameters } = result.compile();
181+
expect(sql).toMatch(/order by.*"name".*asc/i);
182+
expect(sql).toContain("limit");
183+
expect(sql).toContain("offset");
184+
expect(parameters).toContain(10);
185+
expect(parameters).toContain(20);
186+
});
187+
});
118188

119-
expect(result).toBe(mockQuery);
120-
expect(mockQuery.orderBy).toHaveBeenCalledTimes(2);
189+
describe("data validation", () => {
190+
it("should correctly sort numeric values", async () => {
191+
const result = await db
192+
.selectFrom("test_users")
193+
.selectAll()
194+
.orderBy("score", "desc")
195+
.execute();
196+
197+
expect(result[0].score).toBe(100);
198+
expect(result[1].score).toBe(95);
199+
expect(result[2].score).toBe(85);
200+
});
201+
202+
it("should correctly sort text values", async () => {
203+
const result = await db
204+
.selectFrom("test_users")
205+
.selectAll()
206+
.orderBy("name", "asc")
207+
.execute();
208+
209+
expect(result[0].name).toBe("Alice");
210+
expect(result[1].name).toBe("Bob");
211+
expect(result[2].name).toBe("Charlie");
212+
});
213+
214+
it("should correctly sort dates", async () => {
215+
const result = await db
216+
.selectFrom("test_users")
217+
.selectAll()
218+
.orderBy("created_at", "asc")
219+
.execute();
220+
221+
expect(result[0].name).toBe("Alice"); // 2024-01-01
222+
expect(result[1].name).toBe("Bob"); // 2024-01-02
223+
expect(result[2].name).toBe("Charlie"); // 2024-01-03
224+
});
121225
});
122226
});

0 commit comments

Comments
 (0)