Skip to content

Commit

Permalink
feat: created historical-price endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
yevhen-burkovskyi committed Apr 12, 2024
1 parent 5fbb8b4 commit 9fc9825
Show file tree
Hide file tree
Showing 25 changed files with 5,881 additions and 83 deletions.
5 changes: 5 additions & 0 deletions app/api/core/config/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const config = {
osmosis: {
url: process.env.OSMOSIS_BASE_URL,
},
};
7 changes: 7 additions & 0 deletions app/api/core/enums/http-error-message.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export enum HttpErrorMessage {
BAD_REQUEST = 'Bad Request',
UNAUTHORIZED = 'Unauthorized',
FORBIDDEN = 'Forbidden',
NOT_FOUND = 'Not Found',
INTERNAL_SERVER_ERROR = 'Internal Server Error',
}
11 changes: 11 additions & 0 deletions app/api/core/enums/http-status.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export enum HttpStatus {
OK = 200,
CREATED = 201,
ACCEPTED = 202,
BAD_REQUEST = 400,
UNAUTHORIZED = 401,
FORBIDDEN = 403,
NOT_FOUND = 404,
INTERNAL_SERVER_ERROR = 500,
NOT_IMPLEMENTED = 501,
}
3 changes: 3 additions & 0 deletions app/api/core/services/osmosis/enums/endpoints.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export enum Endpoints {
HISTORICAL_PRICE = 'tokens/v2/historical/:symbol/chart',
}
3 changes: 3 additions & 0 deletions app/api/core/services/osmosis/enums/route-param.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export enum RouteParam {
SYMBOL = ':symbol',
}
55 changes: 55 additions & 0 deletions app/api/core/services/osmosis/osmosis.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/* eslint-disable @typescript-eslint/no-namespace, @typescript-eslint/no-explicit-any */
import { GetHistoricalChartDto } from '@/app/api/historical-price/dtos/get-historical-chart.dto';
import { config } from '@core/config/config';
import { Endpoints } from '@core/services/osmosis/enums/endpoints.enum';
import { RouteParam } from '@core/services/osmosis/enums/route-param.enum';
import { FailedResponse } from '@core/services/osmosis/responses/failed.response';
import { GSFResponse } from '@core/services/osmosis/responses/generic-success-failed.response';
import { BadRequestException } from '@core/utils/bad-request-exception';
import { createUrlParams } from '@core/utils/create-url-params';
import { HttpRequester } from '@core/utils/http-requester';
import { HistoricalChartRes } from './responses/historical-chart.response';

export namespace OsmosisService {
const BASE_URL = config.osmosis.url;

function constructUrl (endpoint: string, params?: string): string {
return `${BASE_URL}/${endpoint}${params ? `?${params}` : ''}`;
}

export async function getHistoricalChart (
payload: GetHistoricalChartDto
): Promise<HistoricalChartRes> {
const endpoint = Endpoints.HISTORICAL_PRICE.replace(
RouteParam.SYMBOL,
payload.symbol
);

return errorHandleWrapper(
HttpRequester.get.bind(
null,
constructUrl(endpoint, createUrlParams({ tf: payload.range }))
)
);
}

async function errorHandleWrapper<T> (fn: any): Promise<T> {
try {
const response: GSFResponse<T> = await fn();

if (isFailedResponse(response)) {
throw new BadRequestException(response.message);
}

return response as T;
} catch (e: any) {
throw new BadRequestException(e.message);
}
}

function isFailedResponse<T> (
response: GSFResponse<T>
): response is FailedResponse {
return (response as FailedResponse).message !== undefined;
}
}
3 changes: 3 additions & 0 deletions app/api/core/services/osmosis/responses/failed.response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type FailedResponse = {
message: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { FailedResponse } from '@core/services/osmosis/responses/failed.response';

export type GSFResponse<T> = T | FailedResponse; // Generic Success / Failed Response for Osmosis
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export type HistoricalChartRes = HistoricalChartItem[];

export type HistoricalChartItem = {
time: number;
close: number;
high: number;
low: number;
open: number;
volume: number;
}
8 changes: 8 additions & 0 deletions app/api/core/utils/bad-request-exception.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { HttpStatus } from '@core/enums/http-status.enum';
import { HttpException } from '@core/utils/http-exception';

export class BadRequestException extends HttpException {
constructor (message: string) {
super(message, HttpStatus.BAD_REQUEST);
}
}
5 changes: 5 additions & 0 deletions app/api/core/utils/create-url-params.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function createUrlParams (params: any): string {
const url = new URLSearchParams(params);
return url.toString();
}
19 changes: 19 additions & 0 deletions app/api/core/utils/exception-filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { NextResponse } from 'next/server';

import { HttpErrorMessage } from '@core/enums/http-error-message.enum';
import { HttpStatus } from '@core/enums/http-status.enum';

export async function exceptionFilter (fn: any) {
try {
return await fn(); // just to catch all error at this step
} catch (e: any) {
return NextResponse.json(
{
error: e?.message || HttpErrorMessage.INTERNAL_SERVER_ERROR,
status: e?.statusCode || HttpStatus.INTERNAL_SERVER_ERROR,
},
{ status: e?.statusCode || HttpStatus.INTERNAL_SERVER_ERROR }
);
}
}
10 changes: 10 additions & 0 deletions app/api/core/utils/http-exception.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { HttpStatus } from '@core/enums/http-status.enum';

export class HttpException extends Error {
private statusCode: HttpStatus;

constructor (message: string, statusCode: HttpStatus) {
super(message);
this.statusCode = statusCode;
}
}
43 changes: 43 additions & 0 deletions app/api/core/utils/http-requester.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/* eslint-disable @typescript-eslint/no-namespace */

export namespace HttpRequester {
const enum HttpMethod {
GET = 'get',
POST = 'post',
PUT = 'put',
PATCH = 'patch',
DELETE = 'delete',
HEAD = 'head',
OPTIONS = 'options',
}

export async function get<T> (
url: string,
headers?: HeadersInit,
revalidate?: number
): Promise<T> {
const res = await fetch(url, {
method: HttpMethod.GET,
headers,
next: {
revalidate,
},
});

return res.json();
}

export async function post<T> (
url: string,
body: BodyInit,
headers?: HeadersInit
): Promise<T> {
const res = await fetch(url, {
method: HttpMethod.POST,
body,
headers,
});

return res.json();
}
}
16 changes: 16 additions & 0 deletions app/api/core/utils/parse-url-params.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { NextRequest } from 'next/server';

type QueryParams<T> = {
[key: string]: T;
}

export function parseUrlParams (request: NextRequest) {
const url = new URL(request.url);
const queryParamsObject: QueryParams<string> = {};

url.searchParams.forEach((value, key) => {
queryParamsObject[key] = value;
});

return queryParamsObject;
}
24 changes: 24 additions & 0 deletions app/api/core/utils/schema-validate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { SafeParseError, ZodSchema } from 'zod';

import { BadRequestException } from '@core/utils/bad-request-exception';

export function schemaValidate (schema: ZodSchema, value: unknown) {
const res = schema.safeParse(value);

if (res.success) {
return res.data;
}

throw new BadRequestException(errorView(res));
}

function errorView (value: SafeParseError<unknown>): string {
const flattenErrors = value.error.flatten();
let message = 'Passed fields didn\'t match current schema:';

for (const [key, value] of Object.entries(flattenErrors.fieldErrors)) {
message += ` ${key} - [${value?.toString()}]`;
}

return message;
}
4 changes: 4 additions & 0 deletions app/api/historical-price/dtos/get-historical-chart.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type GetHistoricalChartDto = {
symbol: string;
range: number;
}
29 changes: 29 additions & 0 deletions app/api/historical-price/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { NextRequest, NextResponse } from 'next/server';

import { OsmosisService } from '@core/services/osmosis/osmosis.service';
import { HistoricalChartRes } from '@core/services/osmosis/responses/historical-chart.response';
import { exceptionFilter } from '@core/utils/exception-filter';
import { parseUrlParams } from '@core/utils/parse-url-params';
import { schemaValidate } from '@core/utils/schema-validate';
import { GetHistoricalChartDto } from '@historical-price/dtos/get-historical-chart.dto';
import { getHistoricalChartSchema } from '@historical-price/schemas/get-historical-chart.schema';

export async function GET (request: NextRequest) {
return exceptionFilter(getHistoricalPrice.bind(null, request));
}

async function getHistoricalPrice (request: NextRequest) {
const dto: GetHistoricalChartDto = schemaValidate(
getHistoricalChartSchema,
parseUrlParams(request)
);
const historicalChart = await OsmosisService.getHistoricalChart(dto);
return NextResponse.json(historicalPriceView(historicalChart));
}

function historicalPriceView (res: HistoricalChartRes) {
return res.map((item) => ({
time: item.time,
price: item.close,
}));
}
12 changes: 12 additions & 0 deletions app/api/historical-price/schemas/get-historical-chart.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { z } from 'zod';

export const getHistoricalChartSchema = z.object({
symbol: z.string(),
range: z.string().transform((val) => {
const parsedNumber = parseFloat(val);
if (isNaN(parsedNumber)) {
throw new Error('Invalid number format');
}
return parsedNumber;
}),
});
56 changes: 2 additions & 54 deletions app/mock-chart-data.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,6 @@
type ChartData = {
name: string;
price: number;
time: number;
price: number;
};

const mockChartData: ChartData[] = [
{
name: '01 Apr',
price: 10.00,
},
{
name: '04 Apr',
price: 10.00,
},
{
name: '06 Apr',
price: 20.00,
},
{
name: '08 Apr',
price: 30.00,
},
{
name: '10 Apr',
price: 40.00,
},
{
name: '14 Apr',
price: 50.00,
},
{
name: '16 Apr',
price: 60.00,
},
{
name: '18 Apr',
price: 70.00,
},
{
name: '20 Apr',
price: 60.00,
},
{
name: '22 Apr',
price: 55.00,
},
{
name: '28 Apr',
price: 80.00,
},
{
name: '30 Apr',
price: 90.00,
},
];

export { mockChartData };
export type { ChartData };
20 changes: 17 additions & 3 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client';
import Link from 'next/link';
import { useEffect, useState } from 'react';
import { Text, Title } from '@/components/typography';
import AxoneAreaChart from '@/components/ui/axone-area-chart';
import Box from '@/components/ui/box';
Expand All @@ -8,10 +9,23 @@ import { Button } from '@/components/ui/button';
import Column from '@/components/ui/column';
import PageContainer from '@/components/ui/page-container';
import Row from '@/components/ui/row';
import { mockChartData } from './mock-chart-data';

import { ChartData } from './mock-chart-data';

export default function Dashboard () {
const [chartData, setChartData] = useState<ChartData[]>([]);
useEffect(() => {
fetch(
'http://localhost:3000/api/historical-price?symbol=eth&range=43800'
).then(res => {
return res.json();
})
.then(data => {
setChartData(data);
})
.catch(e => {
console.log('Error ' + e);
});
}, []);
return (
<PageContainer>
<Row className='p-6'>
Expand All @@ -29,7 +43,7 @@ export default function Dashboard () {
</Row>

<BoxInner className='h-[384px] py-5'>
<AxoneAreaChart data={mockChartData} />
<AxoneAreaChart data={chartData} />
</BoxInner>

<Row className='mt-10'>
Expand Down
Loading

0 comments on commit 9fc9825

Please sign in to comment.