Skip to content

Commit 22da606

Browse files
committed
refactor(sales): implement comprehensive sales resolver and services
Restructured sales-related components to improve code organization and maintainability: - Moved SalesResolver from graphql to services directory - Updated import paths in composed resolver - Enhanced SalesEntityService with comprehensive documentation - Added detailed JSDoc comments for SalesService and SalesQueryStrategy - Introduced comprehensive test coverage for SalesService, SalesQueryStrategy, and SalesResolver - Improved error handling to return null for failed resolver queries - Standardized code structure with other similar resolvers
1 parent e98d7b0 commit 22da606

File tree

8 files changed

+482
-40
lines changed

8 files changed

+482
-40
lines changed

src/graphql/schemas/resolvers/composed.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { AttestationSchemaResolver } from "../../../services/graphql/resolvers/a
77
import { OrderResolver } from "./orderResolver.js";
88
import { HyperboardResolver } from "./hyperboardResolver.js";
99
import { AllowlistRecordResolver } from "../../../services/graphql/resolvers/allowlistRecordResolver.js";
10-
import { SalesResolver } from "./salesResolver.js";
10+
import { SalesResolver } from "../../../services/graphql/resolvers/salesResolver.js";
1111
import { UserResolver } from "./userResolver.js";
1212
import { BlueprintResolver } from "./blueprintResolver.js";
1313
import { SignatureRequestResolver } from "./signatureRequestResolver.js";

src/graphql/schemas/resolvers/salesResolver.ts

Lines changed: 0 additions & 39 deletions
This file was deleted.

src/services/database/entities/SalesEntityService.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ import type { EntityService } from "./EntityServiceFactory.js";
77
import { createEntityService } from "./EntityServiceFactory.js";
88

99
export type SaleSelect = Selectable<CachingDatabase["sales"]>;
10+
11+
/**
12+
* Service for handling sales-related database operations.
13+
* This service provides functionality to:
14+
* 1. Query multiple sales with filtering and pagination
15+
* 2. Query a single sale by ID
16+
*/
1017
@injectable()
1118
export class SalesService {
1219
private entityService: EntityService<CachingDatabase["sales"], GetSalesArgs>;
@@ -19,10 +26,26 @@ export class SalesService {
1926
>("sales", "SalesEntityService", kyselyCaching);
2027
}
2128

29+
/**
30+
* Retrieves multiple sales based on the provided query arguments.
31+
*
32+
* @param args - Query arguments including where conditions, sorting, and pagination
33+
* @returns A promise resolving to an object containing:
34+
* - data: Array of sales matching the query criteria
35+
* - count: Total number of matching sales
36+
* @throws {Error} If the database query fails
37+
*/
2238
async getSales(args: GetSalesArgs) {
2339
return this.entityService.getMany(args);
2440
}
2541

42+
/**
43+
* Retrieves a single sale based on the provided query arguments.
44+
*
45+
* @param args - Query arguments including where conditions to identify the sale
46+
* @returns A promise resolving to the matching sale
47+
* @throws {Error} If the database query fails
48+
*/
2649
async getSale(args: GetSalesArgs) {
2750
return this.entityService.getSingle(args);
2851
}

src/services/database/strategies/SalesQueryStrategy.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,37 @@ import { Kysely } from "kysely";
22
import { CachingDatabase } from "../../../types/kyselySupabaseCaching.js";
33
import { QueryStrategy } from "./QueryStrategy.js";
44

5+
/**
6+
* Query strategy for handling sales-related database operations.
7+
* This strategy provides functionality to:
8+
* 1. Build queries for fetching sales data
9+
* 2. Build queries for counting sales
10+
*
11+
* The strategy is used by the SalesService to construct and execute database queries.
12+
* It extends the base QueryStrategy class to provide sales-specific query building.
13+
*/
514
export class SalesQueryStrategy extends QueryStrategy<
615
CachingDatabase,
716
"sales"
817
> {
918
protected readonly tableName = "sales" as const;
1019

20+
/**
21+
* Builds a query to fetch sales data.
22+
*
23+
* @param db - The Kysely database instance
24+
* @returns A query builder configured to select all fields from the sales table
25+
*/
1126
buildDataQuery(db: Kysely<CachingDatabase>) {
1227
return db.selectFrom(this.tableName).selectAll();
1328
}
1429

30+
/**
31+
* Builds a query to count sales.
32+
*
33+
* @param db - The Kysely database instance
34+
* @returns A query builder configured to count all rows in the sales table
35+
*/
1536
buildCountQuery(db: Kysely<CachingDatabase>) {
1637
return db.selectFrom(this.tableName).select((eb) => {
1738
return eb.fn.countAll().as("count");
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { inject, injectable } from "tsyringe";
2+
import { Args, FieldResolver, Query, Resolver, Root } from "type-graphql";
3+
import { HypercertsService } from "../../database/entities/HypercertsEntityService.js";
4+
import { SalesService } from "../../database/entities/SalesEntityService.js";
5+
import { GetSalesArgs } from "../../../graphql/schemas/args/salesArgs.js";
6+
import {
7+
Sale,
8+
GetSalesResponse,
9+
} from "../../../graphql/schemas/typeDefs/salesTypeDefs.js";
10+
11+
/**
12+
* Resolver for handling sales-related GraphQL queries and field resolvers.
13+
* This resolver provides functionality to:
14+
* 1. Query sales with filtering and pagination
15+
* 2. Resolve the associated hypercert for a sale
16+
*/
17+
@injectable()
18+
@Resolver(() => Sale)
19+
class SalesResolver {
20+
constructor(
21+
@inject(SalesService)
22+
private salesService: SalesService,
23+
@inject(HypercertsService)
24+
private hypercertsService: HypercertsService,
25+
) {}
26+
27+
/**
28+
* Query resolver for fetching sales with optional filtering and pagination.
29+
*
30+
* @param args - Query arguments including where conditions, sorting, and pagination
31+
* @returns A promise resolving to:
32+
* - Object containing sales data and count if successful
33+
* - null if an error occurs during retrieval
34+
*
35+
* @example
36+
* ```graphql
37+
* query {
38+
* sales(
39+
* where: { hypercert_id: { eq: "123" } }
40+
* first: 10
41+
* offset: 0
42+
* ) {
43+
* data {
44+
* id
45+
* buyer
46+
* seller
47+
* hypercert {
48+
* id
49+
* }
50+
* }
51+
* count
52+
* }
53+
* }
54+
* ```
55+
*/
56+
@Query(() => GetSalesResponse)
57+
async sales(@Args() args: GetSalesArgs) {
58+
try {
59+
return await this.salesService.getSales(args);
60+
} catch (e) {
61+
console.error(
62+
`[SalesResolver::sales] Error fetching sales: ${(e as Error).message}`,
63+
);
64+
return null;
65+
}
66+
}
67+
68+
/**
69+
* Field resolver for the hypercert associated with a sale.
70+
* This resolver is called automatically when the hypercert field is requested in a query.
71+
*
72+
* @param sale - The sale for which to resolve the associated hypercert
73+
* @returns A promise resolving to:
74+
* - The associated hypercert if found
75+
* - null if:
76+
* - No hypercert_id is available
77+
* - The hypercert is not found
78+
* - An error occurs during retrieval
79+
*
80+
* @example
81+
* ```graphql
82+
* query {
83+
* sales {
84+
* data {
85+
* id
86+
* hypercert {
87+
* id
88+
* hypercert_id
89+
* }
90+
* }
91+
* }
92+
* }
93+
* ```
94+
*/
95+
@FieldResolver({ nullable: true })
96+
async hypercert(@Root() sale: Sale) {
97+
if (!sale.hypercert_id) {
98+
console.warn(`[SalesResolver::hypercert_id] Missing hypercert_id`);
99+
return null;
100+
}
101+
102+
try {
103+
return await this.hypercertsService.getHypercert({
104+
where: {
105+
hypercert_id: {
106+
eq: sale.hypercert_id,
107+
},
108+
},
109+
});
110+
} catch (e) {
111+
console.error(
112+
`[SalesResolver::hypercert] Error fetching hypercert: ${(e as Error).message}`,
113+
);
114+
return null;
115+
}
116+
}
117+
}
118+
119+
export { SalesResolver };
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { faker } from "@faker-js/faker";
2+
import { beforeEach, describe, expect, it, vi } from "vitest";
3+
import type { GetSalesArgs } from "../../../../src/graphql/schemas/args/salesArgs.js";
4+
import type { Sale } from "../../../../src/graphql/schemas/typeDefs/salesTypeDefs.js";
5+
import { SalesService } from "../../../../src/services/database/entities/SalesEntityService.js";
6+
import { generateHypercertId } from "../../../utils/testUtils.js";
7+
8+
const mockEntityService = {
9+
getMany: vi.fn(),
10+
getSingle: vi.fn(),
11+
};
12+
13+
// Mock the createEntityService function
14+
vi.mock(
15+
"../../../../src/services/database/entities/EntityServiceFactory.js",
16+
() => ({
17+
createEntityService: () => mockEntityService,
18+
}),
19+
);
20+
21+
describe("SalesService", () => {
22+
let service: SalesService;
23+
24+
beforeEach(() => {
25+
service = new SalesService();
26+
});
27+
28+
describe("getSales", () => {
29+
it("should return sales for given arguments", async () => {
30+
// Arrange
31+
const args: GetSalesArgs = {
32+
where: {
33+
hypercert_id: { eq: generateHypercertId() },
34+
},
35+
};
36+
const expectedResult = {
37+
data: [
38+
{
39+
id: faker.string.uuid(),
40+
hypercert_id: generateHypercertId(),
41+
buyer: faker.string.alphanumeric(42),
42+
seller: faker.string.alphanumeric(42),
43+
currency: faker.string.alphanumeric(42),
44+
collection: faker.string.alphanumeric(42),
45+
transaction_hash: faker.string.alphanumeric(66),
46+
} as Sale,
47+
],
48+
count: 1,
49+
};
50+
mockEntityService.getMany.mockResolvedValue(expectedResult);
51+
52+
// Act
53+
const result = await service.getSales(args);
54+
55+
// Assert
56+
expect(mockEntityService.getMany).toHaveBeenCalledWith(args);
57+
expect(result).toEqual(expectedResult);
58+
});
59+
60+
it("should handle errors from entity service", async () => {
61+
// Arrange
62+
const args: GetSalesArgs = {};
63+
const error = new Error("Entity service error");
64+
mockEntityService.getMany.mockRejectedValue(error);
65+
66+
// Act & Assert
67+
await expect(service.getSales(args)).rejects.toThrow(error);
68+
});
69+
});
70+
71+
describe("getSale", () => {
72+
it("should return a single sale for given arguments", async () => {
73+
// Arrange
74+
const args: GetSalesArgs = {
75+
where: {
76+
id: { eq: faker.string.uuid() },
77+
},
78+
};
79+
const expectedResult = {
80+
id: faker.string.uuid(),
81+
hypercert_id: generateHypercertId(),
82+
buyer: faker.string.alphanumeric(42),
83+
seller: faker.string.alphanumeric(42),
84+
currency: faker.string.alphanumeric(42),
85+
collection: faker.string.alphanumeric(42),
86+
transaction_hash: faker.string.alphanumeric(66),
87+
} as Sale;
88+
mockEntityService.getSingle.mockResolvedValue(expectedResult);
89+
90+
// Act
91+
const result = await service.getSale(args);
92+
93+
// Assert
94+
expect(mockEntityService.getSingle).toHaveBeenCalledWith(args);
95+
expect(result).toEqual(expectedResult);
96+
});
97+
98+
it("should handle errors from entity service", async () => {
99+
// Arrange
100+
const args: GetSalesArgs = {};
101+
const error = new Error("Entity service error");
102+
mockEntityService.getSingle.mockRejectedValue(error);
103+
104+
// Act & Assert
105+
await expect(service.getSale(args)).rejects.toThrow(error);
106+
});
107+
});
108+
});

0 commit comments

Comments
 (0)