Skip to content

Commit 0eca3f6

Browse files
committed
feat(db): enhance EntityServiceFactory with comprehensive documentation and testing
Added robust documentation and test coverage for the EntityServiceFactory: - Expanded JSDoc comments explaining EntityService interface and createEntityService function - Improved type safety by leveraging BaseQueryArgsType - Created comprehensive test suite for EntityServiceFactory using pg-mem - Verified core functionality including single entity retrieval, multiple entity queries, and error handling
1 parent cc841f8 commit 0eca3f6

2 files changed

Lines changed: 123 additions & 9 deletions

File tree

src/services/database/entities/EntityServiceFactory.ts

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,31 +5,60 @@ import {
55
createStandardQueryModifier,
66
QueryModifier,
77
} from "../../../lib/db/queryModifiers/queryModifiers.js";
8-
import { QueryStrategyFactory } from "../strategies/QueryStrategyFactory.js";
8+
import { BaseQueryArgsType } from "../../../lib/graphql/BaseQueryArgs.js";
99
import {
1010
QueryStrategy,
1111
SupportedDatabases,
1212
} from "../strategies/QueryStrategy.js";
13+
import { QueryStrategyFactory } from "../strategies/QueryStrategyFactory.js";
1314

15+
/**
16+
* Interface defining the core functionality of an entity service
17+
* @template TEntity - The entity type this service manages
18+
* @template TArgs - The arguments type for queries
19+
*/
1420
export interface EntityService<TEntity, TArgs> {
21+
/**
22+
* Retrieves a single entity based on the provided arguments
23+
* @param args - Query arguments
24+
* @returns Promise resolving to the entity or undefined if not found
25+
*/
1526
getSingle(args: TArgs): Promise<Selectable<TEntity> | undefined>;
27+
28+
/**
29+
* Retrieves multiple entities based on the provided arguments
30+
* @param args - Query arguments
31+
* @returns Promise resolving to an object containing the data and total count of all matching entities
32+
*/
1633
getMany(args: TArgs): Promise<{ data: Selectable<TEntity>[]; count: number }>;
1734
}
1835

36+
/**
37+
* Creates an entity service for a specific database table
38+
* @template DB - The database schema type
39+
* @template T - The table name type
40+
* @template Args - The arguments type for queries
41+
* @param tableName - Name of the table this service will manage
42+
* @param ServiceName - Name to be assigned to the generated service class
43+
* @param dbConnection - Database connection instance
44+
* @returns An instance of EntityService for the specified table
45+
* @throws {Error} If the strategy for the table cannot be found
46+
*/
1947
export function createEntityService<
2048
DB extends SupportedDatabases,
2149
T extends keyof DB & string,
22-
Args extends {
23-
first?: number;
24-
offset?: number;
25-
where?: Record<string, unknown>;
26-
sortBy?: { [K in keyof DB[T]]?: SortOrder | null };
27-
},
50+
Args extends BaseQueryArgsType<
51+
Record<string, unknown>,
52+
{ [K in keyof DB[T]]?: SortOrder | null }
53+
>,
2854
>(
2955
tableName: T,
3056
ServiceName: string,
3157
dbConnection: Kysely<DB>,
3258
): EntityService<DB[T], Args> {
59+
/**
60+
* Internal service class generated for the specific entity
61+
*/
3362
class GeneratedEntityService implements EntityService<DB[T], Args> {
3463
private readonly strategy: QueryStrategy<DB, T, Args>;
3564
private readonly db: Kysely<DB>;
@@ -45,7 +74,10 @@ export function createEntityService<
4574
);
4675
}
4776

48-
async getSingle(args: Args) {
77+
/**
78+
* @inheritdoc
79+
*/
80+
async getSingle(args: Args): Promise<Selectable<DB[T]> | undefined> {
4981
const query = this.applyQueryModifiers(
5082
this.strategy.buildDataQuery(this.db, args),
5183
args,
@@ -54,7 +86,12 @@ export function createEntityService<
5486
return await query.executeTakeFirst();
5587
}
5688

57-
async getMany(args: Args) {
89+
/**
90+
* @inheritdoc
91+
*/
92+
async getMany(
93+
args: Args,
94+
): Promise<{ data: Selectable<DB[T]>[]; count: number }> {
5895
const dataQuery = this.applyQueryModifiers(
5996
this.strategy.buildDataQuery(this.db, args),
6097
args,
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { Kysely } from "kysely";
2+
import { IMemoryDb, newDb } from "pg-mem";
3+
import { beforeEach, describe, expect, it } from "vitest";
4+
import { GetUsersArgs } from "../../../../src/graphql/schemas/args/userArgs.js";
5+
import {
6+
createEntityService,
7+
EntityService,
8+
} from "../../../../src/services/database/entities/EntityServiceFactory.js";
9+
import { DataDatabase } from "../../../../src/types/kyselySupabaseData.js";
10+
11+
type TestDatabase = DataDatabase;
12+
13+
describe("EntityServiceFactory", () => {
14+
let db: Kysely<TestDatabase>;
15+
let mem: IMemoryDb;
16+
let entityService: EntityService<DataDatabase["users"], GetUsersArgs>;
17+
18+
beforeEach(() => {
19+
mem = newDb();
20+
db = mem.adapters.createKysely();
21+
22+
// Create test table
23+
mem.public.none(`
24+
CREATE TABLE users (
25+
id SERIAL PRIMARY KEY,
26+
display_name TEXT NOT NULL,
27+
avatar TEXT NOT NULL
28+
);
29+
`);
30+
31+
// Insert some test data
32+
mem.public.none(`
33+
INSERT INTO users (display_name, avatar) VALUES
34+
('Alice', 'https://example.com/alice.jpg'),
35+
('Bob', 'https://example.com/bob.jpg'),
36+
('Charlie', 'https://example.com/charlie.jpg');
37+
`);
38+
39+
entityService = createEntityService("users", "TestEntityService", db);
40+
});
41+
42+
describe("Basic Functionality", () => {
43+
it("should retrieve a single entity", async () => {
44+
const result = await entityService.getSingle({
45+
where: { id: { eq: "1" } },
46+
});
47+
expect(result).toBeDefined();
48+
expect(result?.id).toBe(1);
49+
expect(result?.display_name).toBe("Alice");
50+
expect(result?.avatar).toBe("https://example.com/alice.jpg");
51+
});
52+
53+
it("should retrieve multiple entities", async () => {
54+
const result = await entityService.getMany({});
55+
expect(result).toBeDefined();
56+
expect(Array.isArray(result.data)).toBe(true);
57+
expect(result.count).toBe(3); // Alice, Bob, and Charlie
58+
});
59+
});
60+
61+
describe("Error Handling", () => {
62+
it("should return undefined for non-existent entity", async () => {
63+
const result = await entityService.getSingle({
64+
where: { id: { eq: "999" } },
65+
});
66+
expect(result).toBeUndefined();
67+
});
68+
});
69+
70+
describe("Instance Uniqueness", () => {
71+
it("should return unique instances for each service", () => {
72+
const service1 = createEntityService("users", "TestEntityService1", db);
73+
const service2 = createEntityService("users", "TestEntityService2", db);
74+
expect(service1).not.toBe(service2); // Should be different instances
75+
});
76+
});
77+
});

0 commit comments

Comments
 (0)