Skip to content

Commit 13a68ec

Browse files
committed
feat(graphql): standardize error handling across resolvers with null returns
Implemented consistent error handling across multiple GraphQL resolvers: - Updated AllowlistRecordResolver, AttestationResolver, AttestationSchemaResolver, ContractResolver, and FractionResolver - Replaced error throwing with null returns for failed resolver queries - Added detailed console error logging for debugging - Updated corresponding test cases to expect null instead of thrown errors - Improved resilience of GraphQL endpoint by preventing query failures
1 parent 7a6f0b1 commit 13a68ec

File tree

10 files changed

+149
-106
lines changed

10 files changed

+149
-106
lines changed

src/services/graphql/resolvers/allowlistRecordResolver.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,14 @@ class AllowlistRecordResolver {
5959
*/
6060
@Query(() => GetAllowlistRecordResponse)
6161
async allowlistRecords(@Args() args: GetAllowlistRecordsArgs) {
62-
return await this.allowlistRecordService.getAllowlistRecords(args);
62+
try {
63+
return await this.allowlistRecordService.getAllowlistRecords(args);
64+
} catch (e) {
65+
console.error(
66+
`[AllowlistRecordResolver::allowlistRecords] Error fetching allowlist records: ${(e as Error).message}`,
67+
);
68+
return null;
69+
}
6370
}
6471

6572
/**
@@ -87,9 +94,16 @@ class AllowlistRecordResolver {
8794
*/
8895
@FieldResolver()
8996
async hypercert(@Root() allowlistRecord: AllowlistRecord) {
90-
return await this.hypercertsService.getHypercert({
91-
where: { hypercert_id: { eq: allowlistRecord.hypercert_id } },
92-
});
97+
try {
98+
return await this.hypercertsService.getHypercert({
99+
where: { hypercert_id: { eq: allowlistRecord.hypercert_id } },
100+
});
101+
} catch (e) {
102+
console.error(
103+
`[AllowlistRecordResolver::hypercert] Error fetching hypercert: ${(e as Error).message}`,
104+
);
105+
return null;
106+
}
93107
}
94108
}
95109

src/services/graphql/resolvers/attestationResolver.ts

Lines changed: 72 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@ import { inject, injectable } from "tsyringe";
22
import { Args, FieldResolver, Query, Resolver, Root } from "type-graphql";
33
import { getAddress, isAddress } from "viem";
44
import { z } from "zod";
5-
import { AttestationService } from "../../database/entities/AttestationEntityService.js";
6-
import { AttestationSchemaService } from "../../database/entities/AttestationSchemaEntityService.js";
7-
import { HypercertsService } from "../../database/entities/HypercertsEntityService.js";
8-
import { MetadataService } from "../../database/entities/MetadataEntityService.js";
95
import { GetAttestationsArgs } from "../../../graphql/schemas/args/attestationArgs.js";
106
import {
117
Attestation,
128
GetAttestationsResponse,
139
} from "../../../graphql/schemas/typeDefs/attestationTypeDefs.js";
10+
import { AttestationService } from "../../database/entities/AttestationEntityService.js";
11+
import { AttestationSchemaService } from "../../database/entities/AttestationSchemaEntityService.js";
12+
import { HypercertsService } from "../../database/entities/HypercertsEntityService.js";
13+
import { MetadataService } from "../../database/entities/MetadataEntityService.js";
1414

1515
/**
1616
* Schema for validating hypercert pointer data in attestations.
@@ -70,9 +70,9 @@ const HypercertPointer = z.object({
7070
* - Field resolution for metadata associated with the attested hypercert
7171
*
7272
* Error handling:
73-
* - Invalid attestation data returns undefined for related fields
73+
* - Invalid attestation data returns null for related fields
7474
* - Database errors are propagated to the GraphQL layer
75-
* - Schema validation errors result in undefined hypercert IDs
75+
* - Schema validation errors result in null hypercert IDs
7676
*
7777
* @injectable Marks the class as injectable for dependency injection with tsyringe
7878
* @resolver Marks the class as a GraphQL resolver for the Attestation type
@@ -107,7 +107,6 @@ class AttestationResolver {
107107
* @returns A promise that resolves to an object containing:
108108
* - data: Array of attestations matching the query
109109
* - count: Total number of matching attestations
110-
* @throws {Error} If the database query fails
111110
*
112111
* Filtering supports:
113112
* - Attestation fields (id, supported_schemas_id, etc.)
@@ -137,7 +136,14 @@ class AttestationResolver {
137136
*/
138137
@Query(() => GetAttestationsResponse)
139138
async attestations(@Args() args: GetAttestationsArgs) {
140-
return await this.attestationService.getAttestations(args);
139+
try {
140+
return await this.attestationService.getAttestations(args);
141+
} catch (e) {
142+
console.error(
143+
`[AttestationResolver::attestations] Error fetching attestations: ${(e as Error).message}`,
144+
);
145+
return null;
146+
}
141147
}
142148

143149
/**
@@ -152,7 +158,6 @@ class AttestationResolver {
152158
* - attestation.data is null/undefined
153159
* - hypercert ID cannot be extracted from data
154160
* - no matching hypercert is found
155-
* @throws {Error} If the hypercert service query fails
156161
*
157162
* @example
158163
* Query with hypercert field:
@@ -173,19 +178,26 @@ class AttestationResolver {
173178
*/
174179
@FieldResolver()
175180
async hypercert(@Root() attestation: Attestation) {
176-
if (!attestation.data) return;
181+
try {
182+
if (!attestation.data) return null;
177183

178-
const attested_hypercert_id = this.getHypercertIdFromAttestationData(
179-
attestation.data,
180-
);
184+
const attested_hypercert_id = this.getHypercertIdFromAttestationData(
185+
attestation.data,
186+
);
181187

182-
if (!attested_hypercert_id) return;
188+
if (!attested_hypercert_id) return null;
183189

184-
return await this.hypercertService.getHypercert({
185-
where: {
186-
hypercert_id: { eq: attested_hypercert_id },
187-
},
188-
});
190+
return await this.hypercertService.getHypercert({
191+
where: {
192+
hypercert_id: { eq: attested_hypercert_id },
193+
},
194+
});
195+
} catch (e) {
196+
console.error(
197+
`[AttestationResolver::hypercert] Error fetching hypercert: ${(e as Error).message}`,
198+
);
199+
return null;
200+
}
189201
}
190202

191203
/**
@@ -196,7 +208,6 @@ class AttestationResolver {
196208
* @returns A promise that resolves to:
197209
* - The associated schema data if found
198210
* - undefined if no schema ID is present
199-
* @throws {Error} If the schema service query fails
200211
*
201212
* @example
202213
* Query with schema field:
@@ -219,13 +230,20 @@ class AttestationResolver {
219230
*/
220231
@FieldResolver()
221232
async eas_schema(@Root() attestation: Attestation) {
222-
if (!attestation.supported_schemas_id) return;
233+
try {
234+
if (!attestation.supported_schemas_id) return null;
223235

224-
return await this.attestationSchemaService.getAttestationSchema({
225-
where: {
226-
id: { eq: attestation.supported_schemas_id },
227-
},
228-
});
236+
return await this.attestationSchemaService.getAttestationSchema({
237+
where: {
238+
id: { eq: attestation.supported_schemas_id },
239+
},
240+
});
241+
} catch (e) {
242+
console.error(
243+
`[AttestationResolver::eas_schema] Error fetching eas_schema: ${(e as Error).message}`,
244+
);
245+
return null;
246+
}
229247
}
230248

231249
/**
@@ -263,17 +281,24 @@ class AttestationResolver {
263281
//TODO: Should this be part of the resolved hypercert data?
264282
@FieldResolver()
265283
async metadata(@Root() attestation: Attestation) {
266-
if (!attestation.data) return;
284+
try {
285+
if (!attestation.data) return null;
267286

268-
const attested_hypercert_id = this.getHypercertIdFromAttestationData(
269-
attestation.data,
270-
);
287+
const attested_hypercert_id = this.getHypercertIdFromAttestationData(
288+
attestation.data,
289+
);
271290

272-
if (!attested_hypercert_id) return;
291+
if (!attested_hypercert_id) return null;
273292

274-
return await this.metadataService.getMetadataSingle({
275-
where: { hypercerts: { hypercert_id: { eq: attested_hypercert_id } } },
276-
});
293+
return await this.metadataService.getMetadataSingle({
294+
where: { hypercerts: { hypercert_id: { eq: attested_hypercert_id } } },
295+
});
296+
} catch (e) {
297+
console.error(
298+
`[AttestationResolver::metadata] Error fetching metadata: ${(e as Error).message}`,
299+
);
300+
return null;
301+
}
277302
}
278303

279304
/**
@@ -299,17 +324,22 @@ class AttestationResolver {
299324
* getHypercertIdFromAttestationData(null) // returns undefined
300325
* ```
301326
*/
302-
getHypercertIdFromAttestationData(
303-
attestationData: unknown,
304-
): string | undefined {
305-
if (!attestationData) return;
327+
getHypercertIdFromAttestationData(attestationData: unknown): string | null {
328+
try {
329+
if (!attestationData) return null;
306330

307-
const parseResult = HypercertPointer.safeParse(attestationData);
331+
const parseResult = HypercertPointer.safeParse(attestationData);
308332

309-
if (!parseResult.success) return;
333+
if (!parseResult.success) return null;
310334

311-
const { chain_id, contract_address, token_id } = parseResult.data;
312-
return `${chain_id.toString()}-${getAddress(contract_address)}-${token_id.toString()}`;
335+
const { chain_id, contract_address, token_id } = parseResult.data;
336+
return `${chain_id.toString()}-${getAddress(contract_address)}-${token_id.toString()}`;
337+
} catch (e) {
338+
console.error(
339+
`[AttestationResolver::getHypercertIdFromAttestationData] Error parsing hypercert ID: ${(e as Error).message}`,
340+
);
341+
return null;
342+
}
313343
}
314344
}
315345

src/services/graphql/resolvers/attestationSchemaResolver.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { inject, injectable } from "tsyringe";
22
import { Args, FieldResolver, Query, Resolver, Root } from "type-graphql";
3-
import { AttestationService } from "../../../services/database/entities/AttestationEntityService.js";
4-
import { AttestationSchemaService } from "../../../services/database/entities/AttestationSchemaEntityService.js";
53
import { GetAttestationSchemasArgs } from "../../../graphql/schemas/args/attestationSchemaArgs.js";
6-
import { GetAttestationsResponse } from "../../../graphql/schemas/typeDefs/attestationTypeDefs.js";
74
import {
85
AttestationSchema,
96
GetAttestationsSchemaResponse,
107
} from "../../../graphql/schemas/typeDefs/attestationSchemaTypeDefs.js";
8+
import { GetAttestationsResponse } from "../../../graphql/schemas/typeDefs/attestationTypeDefs.js";
9+
import { AttestationService } from "../../../services/database/entities/AttestationEntityService.js";
10+
import { AttestationSchemaService } from "../../../services/database/entities/AttestationSchemaEntityService.js";
1111

1212
/**
1313
* GraphQL resolver for AttestationSchema operations.
@@ -44,7 +44,6 @@ class AttestationSchemaResolver {
4444
* @returns A promise that resolves to an object containing:
4545
* - data: Array of attestation schemas matching the query
4646
* - count: Total number of matching schemas
47-
* @throws {Error} If the schema service query fails
4847
*
4948
* @example
5049
* Query with filtering:
@@ -70,7 +69,14 @@ class AttestationSchemaResolver {
7069
*/
7170
@Query(() => GetAttestationsSchemaResponse)
7271
async attestationSchemas(@Args() args: GetAttestationSchemasArgs) {
73-
return await this.attestationSchemaService.getAttestationSchemas(args);
72+
try {
73+
return await this.attestationSchemaService.getAttestationSchemas(args);
74+
} catch (e) {
75+
console.error(
76+
`[AttestationSchemaResolver::attestationSchemas] Error fetching attestation schemas: ${(e as Error).message}`,
77+
);
78+
return null;
79+
}
7480
}
7581

7682
/**
@@ -107,9 +113,16 @@ class AttestationSchemaResolver {
107113
*/
108114
@FieldResolver(() => GetAttestationsResponse, { nullable: true })
109115
async attestations(@Root() schema: Partial<AttestationSchema>) {
110-
return await this.attestationService.getAttestations({
111-
where: { supported_schemas_id: { eq: schema.id } },
112-
});
116+
try {
117+
return await this.attestationService.getAttestations({
118+
where: { supported_schemas_id: { eq: schema.id } },
119+
});
120+
} catch (e) {
121+
console.error(
122+
`[AttestationSchemaResolver::attestations] Error fetching attestations: ${(e as Error).message}`,
123+
);
124+
return null;
125+
}
113126
}
114127
}
115128

src/services/graphql/resolvers/contractResolver.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { inject, injectable } from "tsyringe";
22
import { Args, Query, Resolver } from "type-graphql";
3-
import { ContractService } from "../../database/entities/ContractEntityService.js";
43
import { GetContractsArgs } from "../../../graphql/schemas/args/contractArgs.js";
54
import {
65
Contract,
76
GetContractsResponse,
87
} from "../../../graphql/schemas/typeDefs/contractTypeDefs.js";
8+
import { ContractService } from "../../database/entities/ContractEntityService.js";
99

1010
/**
1111
* GraphQL resolver for Contract operations.
@@ -45,7 +45,6 @@ class ContractResolver {
4545
* @returns A promise that resolves to an object containing:
4646
* - data: Array of contracts matching the query
4747
* - count: Total number of matching contracts
48-
* @throws {Error} If the contract service query fails
4948
*
5049
* @example
5150
* Query with filtering:
@@ -70,7 +69,14 @@ class ContractResolver {
7069
*/
7170
@Query(() => GetContractsResponse)
7271
async contracts(@Args() args: GetContractsArgs) {
73-
return this.contractService.getContracts(args);
72+
try {
73+
return await this.contractService.getContracts(args);
74+
} catch (e) {
75+
console.error(
76+
`[ContractResolver::contracts] Error fetching contracts: ${(e as Error).message}`,
77+
);
78+
return null;
79+
}
7480
}
7581
}
7682

src/services/graphql/resolvers/fractionResolver.ts

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,7 @@ class FractionResolver {
9494
console.error(
9595
`[FractionResolver::fractions] Error fetching fractions: ${(e as Error).message}`,
9696
);
97-
// Return empty result instead of throwing
98-
return {
99-
data: [],
100-
count: 0,
101-
};
97+
return null;
10298
}
10399
}
104100

@@ -208,10 +204,7 @@ class FractionResolver {
208204
`[FractionResolver::orders] Error fetching orders for fraction ${fraction.id}: ${(e as Error).message}`,
209205
);
210206
// Return empty result instead of throwing
211-
return {
212-
data: [],
213-
count: 0,
214-
};
207+
return null;
215208
}
216209
}
217210

@@ -275,11 +268,7 @@ class FractionResolver {
275268
console.error(
276269
`[FractionResolver::sales] Error fetching sales for fraction ${fraction.id}: ${(e as Error).message}`,
277270
);
278-
// Return empty result instead of throwing
279-
return {
280-
data: [],
281-
count: 0,
282-
};
271+
return null;
283272
}
284273
}
285274
}

test/services/graphql/resolvers/allowlistRecordResolver.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ describe("AllowlistRecordResolver", () => {
8080
mockAllowlistRecordService.getAllowlistRecords.mockRejectedValue(error);
8181

8282
// Act & Assert
83-
await expect(resolver.allowlistRecords(args)).rejects.toThrow(error);
83+
await expect(resolver.allowlistRecords(args)).resolves.toBeNull();
8484
});
8585
});
8686

@@ -135,7 +135,7 @@ describe("AllowlistRecordResolver", () => {
135135
mockHypercertsService.getHypercert.mockRejectedValue(error);
136136

137137
// Act & Assert
138-
await expect(resolver.hypercert(allowlistRecord)).rejects.toThrow(error);
138+
await expect(resolver.hypercert(allowlistRecord)).resolves.toBeNull();
139139
});
140140
});
141141
});

0 commit comments

Comments
 (0)