Skip to content

Commit c661f9c

Browse files
committed
fix: feedback
1 parent f7411fb commit c661f9c

File tree

3 files changed

+205
-145
lines changed

3 files changed

+205
-145
lines changed

pages/api/SGhoService.ts

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import {
2+
ApiResponse,
3+
GraphQLResponse,
4+
SGhoApyQueryOptions,
5+
SGhoRatesData,
6+
TransformedDailyData,
7+
} from './SGhoService.types';
8+
9+
const sghoConfig = {
10+
graphqlEndpoint: 'https://tokenlogic-data.ddn.hasura.app/graphql',
11+
apiKey: process.env.TOKENLOGIC_API_KEY,
12+
defaultLimit: 100,
13+
} as const;
14+
15+
/**
16+
* GraphQL queries for sGHO APY data
17+
*/
18+
export const sghoQueries = {
19+
/**
20+
* Query for sGHO APY data with date range filtering
21+
*/
22+
getApyHistoryDateRange: `
23+
query GetSGhoApyHistoryDateRange($startDate: timestamptz!, $endDate: timestamptz!, $limit: Int!) {
24+
aaveV3RatesSgho(
25+
limit: $limit,
26+
where: {
27+
blockHour: {
28+
_gte: $startDate,
29+
_lte: $endDate
30+
}
31+
},
32+
order_by: { blockHour: asc }
33+
) {
34+
blockHour
35+
apr
36+
}
37+
}
38+
`,
39+
40+
/**
41+
* Query for recent sGHO APY data
42+
*/
43+
getApyHistory: `
44+
query GetSGhoApyHistory($limit: Int!) {
45+
aaveV3RatesSgho(
46+
limit: $limit,
47+
order_by: { blockHour: asc }
48+
) {
49+
blockHour
50+
apr
51+
}
52+
}
53+
`,
54+
} as const;
55+
56+
/**
57+
* Transform GraphQL data to the format expected by the frontend
58+
* Aggregates multiple hourly entries per day to a single daily entry
59+
*/
60+
export const transformGraphQLData = (graphqlData: SGhoRatesData[]): TransformedDailyData[] => {
61+
const dailyData = new Map<string, { timestamp: Date; merit_apy: number }>();
62+
63+
graphqlData.forEach((item) => {
64+
const timestamp = new Date(item.blockHour);
65+
const dateString = timestamp.toISOString().split('T')[0];
66+
67+
// Keep the latest entry for each day (or first if no existing entry)
68+
const existing = dailyData.get(dateString);
69+
if (!existing || timestamp > existing.timestamp) {
70+
dailyData.set(dateString, {
71+
timestamp,
72+
merit_apy: item.apr,
73+
});
74+
}
75+
});
76+
77+
return Array.from(dailyData.entries()).map(([dateString, { merit_apy }]) => ({
78+
day: {
79+
value: dateString,
80+
},
81+
merit_apy,
82+
}));
83+
};
84+
85+
/**
86+
* Execute GraphQL query against the TokenLogic API
87+
*/
88+
export const executeGraphQLQuery = async (
89+
query: string,
90+
variables: Record<string, string | number>
91+
): Promise<GraphQLResponse> => {
92+
if (!sghoConfig.apiKey) {
93+
throw new Error('TOKENLOGIC_API_KEY environment variable not set');
94+
}
95+
96+
const response = await fetch(sghoConfig.graphqlEndpoint, {
97+
method: 'POST',
98+
headers: {
99+
'Content-Type': 'application/json',
100+
'x-api-key': sghoConfig.apiKey,
101+
},
102+
body: JSON.stringify({
103+
query,
104+
variables,
105+
}),
106+
});
107+
108+
if (!response.ok) {
109+
throw new Error(`GraphQL HTTP error: ${response.status} - ${response.statusText}`);
110+
}
111+
112+
const result: GraphQLResponse = await response.json();
113+
114+
if (result.errors && result.errors.length > 0) {
115+
throw new Error(`GraphQL error: ${result.errors.map((e) => e.message).join(', ')}`);
116+
}
117+
118+
if (!result.data?.aaveV3RatesSgho || !Array.isArray(result.data.aaveV3RatesSgho)) {
119+
throw new Error('Invalid response format from data source');
120+
}
121+
122+
return result;
123+
};
124+
125+
/**
126+
* Fetch and transform sGHO APY data
127+
*/
128+
export const fetchSGhoApyData = async (options: SGhoApyQueryOptions): Promise<ApiResponse> => {
129+
const { limit = sghoConfig.defaultLimit, startDate, endDate } = options;
130+
131+
// Determine which query to use and prepare variables
132+
let query: string;
133+
let variables: Record<string, string | number>;
134+
135+
if (startDate && endDate) {
136+
query = sghoQueries.getApyHistoryDateRange;
137+
variables = { startDate, endDate, limit };
138+
} else {
139+
query = sghoQueries.getApyHistory;
140+
variables = { limit };
141+
}
142+
143+
const result = await executeGraphQLQuery(query, variables);
144+
145+
// Transform the data (data is already sorted by blockHour from the GraphQL query)
146+
const transformedData = transformGraphQLData(result.data!.aaveV3RatesSgho);
147+
148+
return { data: transformedData };
149+
};

pages/api/SGhoService.types.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* Types for sGHO APY data service
3+
*/
4+
5+
export type SGhoRatesData = {
6+
blockHour: string;
7+
apr: number;
8+
};
9+
10+
export type GraphQLResponse = {
11+
data?: {
12+
aaveV3RatesSgho: SGhoRatesData[];
13+
};
14+
errors?: Array<{
15+
message: string;
16+
locations?: Array<{ line: number; column: number }>;
17+
path?: string[];
18+
}>;
19+
};
20+
21+
export type ApiResponse = {
22+
data?: Array<{
23+
day: { value: string };
24+
merit_apy: number;
25+
}>;
26+
error?: string;
27+
};
28+
29+
export type SGhoApyQueryOptions = {
30+
limit?: number;
31+
startDate?: string;
32+
endDate?: string;
33+
};
34+
35+
export type TransformedDailyData = {
36+
day: { value: string };
37+
merit_apy: number;
38+
};

pages/api/sgho-apy.ts

Lines changed: 18 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,6 @@
11
import type { NextApiRequest, NextApiResponse } from 'next';
2-
3-
const GRAPHQL_ENDPOINT = 'https://tokenlogic-data.ddn.hasura.app/graphql';
4-
const API_KEY = process.env.TOKENLOGIC_API_KEY;
5-
6-
type SGhoRatesData = {
7-
blockHour: string;
8-
apr: number;
9-
};
10-
11-
type GraphQLResponse = {
12-
data?: {
13-
aaveV3RatesSgho: SGhoRatesData[];
14-
};
15-
errors?: Array<{
16-
message: string;
17-
locations?: Array<{ line: number; column: number }>;
18-
path?: string[];
19-
}>;
20-
};
21-
22-
type ApiResponse = {
23-
data?: Array<{
24-
day: { value: string };
25-
merit_apy: number;
26-
}>;
27-
error?: string;
28-
};
29-
30-
/**
31-
* Transform GraphQL data to the format expected by the frontend
32-
* Aggregates multiple hourly entries per day to a single daily entry
33-
*/
34-
const transformGraphQLData = (graphqlData: SGhoRatesData[]) => {
35-
const dailyData = new Map<string, { timestamp: Date; merit_apy: number }>();
36-
37-
graphqlData.forEach((item) => {
38-
const timestamp = new Date(item.blockHour);
39-
const dateString = timestamp.toISOString().split('T')[0];
40-
41-
// Keep the latest entry for each day (or first if no existing entry)
42-
const existing = dailyData.get(dateString);
43-
if (!existing || timestamp > existing.timestamp) {
44-
dailyData.set(dateString, {
45-
timestamp,
46-
merit_apy: item.apr,
47-
});
48-
}
49-
});
50-
51-
return Array.from(dailyData.entries()).map(([dateString, { merit_apy }]) => ({
52-
day: {
53-
value: dateString,
54-
},
55-
merit_apy,
56-
}));
57-
};
2+
import { fetchSGhoApyData } from 'pages/api/SGhoService';
3+
import { ApiResponse } from 'pages/api/SGhoService.types';
584

595
/**
606
* Next.js API route to fetch sGHO APY data from TokenLogic GraphQL API
@@ -72,107 +18,34 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
7218
}
7319

7420
try {
75-
// Check if API key is configured
76-
if (!API_KEY) {
77-
console.error('TOKENLOGIC_API_KEY environment variable not set');
78-
return res.status(500).json({
79-
error: 'Server configuration error',
80-
});
81-
}
82-
83-
// Parse query parameters
8421
const limit = parseInt(req.query.limit as string) || 100;
8522
const startDate = req.query.startDate as string;
8623
const endDate = req.query.endDate as string;
8724

88-
// Build GraphQL query based on parameters
89-
let graphqlQuery: string;
90-
let variables: Record<string, string | number>;
91-
92-
if (startDate && endDate) {
93-
// Query with date range
94-
graphqlQuery = `
95-
query GetSGhoApyHistoryDateRange($startDate: timestamptz!, $endDate: timestamptz!, $limit: Int!) {
96-
aaveV3RatesSgho(
97-
limit: $limit,
98-
where: {
99-
blockHour: {
100-
_gte: $startDate,
101-
_lte: $endDate
102-
}
103-
}
104-
) {
105-
blockHour
106-
apr
107-
}
108-
}
109-
`;
110-
variables = { startDate, endDate, limit };
111-
} else {
112-
// Query for recent data
113-
graphqlQuery = `
114-
query GetSGhoApyHistory($limit: Int!) {
115-
aaveV3RatesSgho(limit: $limit) {
116-
blockHour
117-
apr
118-
}
119-
}
120-
`;
121-
variables = { limit };
122-
}
123-
124-
// Make GraphQL request
125-
const response = await fetch(GRAPHQL_ENDPOINT, {
126-
method: 'POST',
127-
headers: {
128-
'Content-Type': 'application/json',
129-
'x-api-key': API_KEY,
130-
},
131-
body: JSON.stringify({
132-
query: graphqlQuery,
133-
variables,
134-
}),
25+
const result = await fetchSGhoApyData({
26+
limit,
27+
startDate,
28+
endDate,
13529
});
13630

137-
if (!response.ok) {
138-
console.error(`GraphQL HTTP error: ${response.status}`);
139-
return res.status(response.status).json({
140-
error: `Failed to fetch data: ${response.statusText}`,
141-
});
142-
}
143-
144-
const result: GraphQLResponse = await response.json();
31+
res.status(200).json(result);
32+
} catch (error) {
33+
console.error('API route error:', error);
14534

146-
// Check for GraphQL errors
147-
if (result.errors && result.errors.length > 0) {
148-
console.error('GraphQL errors:', result.errors);
35+
if (error.message.includes('GraphQL error')) {
14936
return res.status(400).json({
150-
error: `GraphQL error: ${result.errors.map((e) => e.message).join(', ')}`,
37+
error: error.message,
15138
});
15239
}
15340

154-
// Validate response structure
155-
if (!result.data?.aaveV3RatesSgho || !Array.isArray(result.data.aaveV3RatesSgho)) {
156-
console.error('Invalid GraphQL response format:', result);
157-
return res.status(500).json({
158-
error: 'Invalid response format from data source',
41+
if (error.message.includes('HTTP error')) {
42+
return res.status(502).json({
43+
error: 'Failed to fetch data from external service',
15944
});
16045
}
161-
162-
const transformedData = transformGraphQLData(result.data.aaveV3RatesSgho);
163-
164-
const sortedData = transformedData.sort((a, b) => {
165-
const dateA = new Date(a.day.value);
166-
const dateB = new Date(b.day.value);
167-
return dateA.getTime() - dateB.getTime();
168-
});
169-
170-
// Return successful response
171-
res.status(200).json({ data: sortedData });
172-
} catch (error) {
173-
console.error('API route error:', error);
174-
res.status(500).json({
175-
error: 'Internal server error',
176-
});
17746
}
47+
48+
res.status(500).json({
49+
error: 'Internal server error',
50+
});
17851
}

0 commit comments

Comments
 (0)