Skip to content

Commit

Permalink
Merge pull request #2 from angelxmoreno/feat/api-class
Browse files Browse the repository at this point in the history
Feat/api class
  • Loading branch information
angelxmoreno authored Feb 2, 2024
2 parents 24b2f41 + 6bde90d commit cfb8bb0
Show file tree
Hide file tree
Showing 8 changed files with 327 additions and 1 deletion.
37 changes: 37 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"@commitlint/cli": "^18.6.0",
"@commitlint/config-conventional": "^18.6.0",
"@types/jest": "^29.5.12",
"axios-mock-adapter": "^1.22.0",
"eslint": "^8.56.0",
"eslint-config-universe": "^12.0.0",
"husky": "^8.0.0",
Expand Down
138 changes: 138 additions & 0 deletions src/TheSneakerDatabaseClient.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';

import { TheSneakerDatabaseClient } from './TheSneakerDatabaseClient';
import { GetSneakersOptions, GetSneakersResponse, SearchOptions, SearchResponse, Sneaker } from './interfaces';

describe('TheSneakerDatabaseClient', () => {
let client: TheSneakerDatabaseClient;
let mockAxios: MockAdapter;
const sneakerData: Sneaker = {
id: '5338a798-ac8b-442f-a8b2-71d3a79311a5',
brand: 'Jordan',
colorway: 'White/Black/Dark Mocha',
estimatedMarketValue: 120,
gender: 'youth',
image: {
original: '',
small: '',
thumbnail: '',
},
links: {
stockX: '',
goat: 'https://goat.com/sneakers/air-jordan-1-retro-low-og-gs-mocha-cz0858-102',
flightClub: 'https://flightclub.com/air-jordan-1-retro-low-og-gs-mocha-cz0858-102',
stadiumGoods: '',
},
name: "Air Jordan 1 Retro Low OG GS 'Mocha'",
releaseDate: new Date('2024-08-21'),
releaseYear: 2024,
retailPrice: 120,
silhouette: 'Air Jordan 1',
sku: 'CZ0858-102',
story: 'The Air Jordan 1 Retro Low OG GS ‘Mocha’ showcases...',
};
beforeEach(() => {
mockAxios = new MockAdapter(axios);
client = new TheSneakerDatabaseClient('your-api-key', {});
});

afterEach(() => {
mockAxios.reset();
});
it('should handle getSneakers request', async () => {
const options: GetSneakersOptions = {
limit: 5,
name: 'Air Jordan 1 Retro Low OG GS',
brand: 'Jordan',
colorway: 'White/Black/Dark Mocha',
gender: 'youth',
page: 1,
releaseDate: '2024-08-21',
sku: 'CZ0858-102',
releaseYear: '2024',
sort: 'release_date',
silhouette: 'Air Jordan 1',
};
const responseObj: GetSneakersResponse = {
count: 1,
results: [sneakerData],
};
mockAxios.onGet('/sneakers', { params: options }).reply(200, responseObj);
const response = await client.getSneakers(options);

expect(response.error).toBeUndefined();
expect(response.response).toBeDefined();
expect(mockAxios.history.get.length).toBe(1);
expect(mockAxios.history.get[0].url).toBe('/sneakers');
expect(mockAxios.history.get[0].params).toEqual(options);
});

it('should handle getSneakers request with an error', async () => {
mockAxios.onGet('/sneakers', { params: { limit: 5 } }).reply(500, 'Internal Server Error');
const response = await client.getSneakers({ limit: 5 });
expect(response.response).toBeUndefined();
expect(response.error).toBeDefined();
});

it('should handle getSneakerById request', async () => {
const sneakerId = '5338a798-ac8b-442f-a8b2-71d3a79311a5';
const responseObj: Sneaker = sneakerData;
mockAxios.onGet(`/sneakers/${sneakerId}`).reply(200, responseObj);
const response = await client.getSneakerById(sneakerId);

expect(response.error).toBeUndefined();
expect(response.response).toBeDefined();
expect(response.response).toEqual(responseObj);
expect(mockAxios.history.get.length).toBe(1);
expect(mockAxios.history.get[0].url).toBe(`/sneakers/${sneakerId}`);
});

it('should handle getSneakerById request with an error', async () => {
const sneakerId = 'nonexistent-id';
mockAxios.onGet(`/sneakers/${sneakerId}`).reply(404, 'Not Found');
const response = await client.getSneakerById(sneakerId);

expect(response.response).toBeUndefined();
expect(response.error).toBeDefined();
expect(mockAxios.history.get.length).toBe(1);
expect(mockAxios.history.get[0].url).toBe(`/sneakers/${sneakerId}`);
});

it('should handle search request', async () => {
const searchOptions: SearchOptions = {
query: 'Air Jordan 1',
page: 1,
limit: 5,
};
const responseObj: SearchResponse = {
count: 1,
totalPages: 1,
results: [sneakerData],
};
mockAxios.onGet('/search', { params: searchOptions }).reply(200, responseObj);
const response = await client.search(searchOptions);

expect(response.error).toBeUndefined();
expect(response.response).toBeDefined();
expect(response.response).toEqual(responseObj);
expect(mockAxios.history.get.length).toBe(1);
expect(mockAxios.history.get[0].url).toBe('/search');
expect(mockAxios.history.get[0].params).toEqual(searchOptions);
});

it('should handle search request with an error', async () => {
const searchOptions: SearchOptions = {
limit: 5,
query: 'Nonexistent Sneaker',
};
mockAxios.onGet('/search', { params: searchOptions }).reply(404, 'Not Found');
const response = await client.search(searchOptions);

expect(response.response).toBeUndefined();
expect(response.error).toBeDefined();
expect(mockAxios.history.get.length).toBe(1);
expect(mockAxios.history.get[0].url).toBe('/search');
expect(mockAxios.history.get[0].params).toEqual(searchOptions);
});
});
66 changes: 66 additions & 0 deletions src/TheSneakerDatabaseClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import axios, { AxiosInstance, CreateAxiosDefaults } from 'axios';
import { addAxiosDateTransformer, createAxiosDateTransformer } from 'axios-date-transformer';

import {
GetSneakersOptions,
GetSneakersResponse,
MethodResponse,
SearchOptions,
SearchResponse,
Sneaker,
} from './interfaces';
import { handleAxiosError } from './utils';

export class TheSneakerDatabaseClient {
protected client: AxiosInstance;

constructor(rapidApiKey: string, axiosParam: AxiosInstance | CreateAxiosDefaults) {
this.client = this.configureAxiosInstance(rapidApiKey, axiosParam);
}

protected configureAxiosInstance(
rapidApiKey: string,
axiosParam: AxiosInstance | CreateAxiosDefaults,
): AxiosInstance {
const instance =
axiosParam instanceof axios
? addAxiosDateTransformer(axiosParam as AxiosInstance)
: createAxiosDateTransformer(axiosParam as CreateAxiosDefaults);

instance.defaults.baseURL = 'https://the-sneaker-database.p.rapidapi.com/';
instance.defaults.headers.common['X-RapidAPI-Host'] = 'the-sneaker-database.p.rapidapi.com';
instance.defaults.headers.common['X-RapidAPI-Key'] = rapidApiKey;
instance.defaults.headers.common['Content-Type'] = 'application/json';

return instance;
}

protected async handleRequest<T, K = undefined>(uri: string, params?: K): Promise<MethodResponse<T>> {
try {
const { data } = await this.client.get<T>(uri, { params });
return { response: data };
} catch (error) {
return { error: handleAxiosError(error) };
}
}

getSneakers(options: GetSneakersOptions): Promise<MethodResponse<GetSneakersResponse>> {
return this.handleRequest<GetSneakersResponse, GetSneakersOptions>('/sneakers', options);
}

getSneakerById(sneakerId: string): Promise<MethodResponse<Sneaker[]>> {
return this.handleRequest<Sneaker[]>(`/sneakers/${sneakerId}`);
}

getBrands(): Promise<MethodResponse<string[]>> {
return this.handleRequest<string[]>(`/brands`);
}

getGenders(): Promise<MethodResponse<string[]>> {
return this.handleRequest<string[]>(`/genders`);
}

search(options: SearchOptions): Promise<MethodResponse<SearchResponse>> {
return this.handleRequest<SearchResponse, SearchOptions>(`/search`, options);
}
}
11 changes: 10 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,10 @@
console.log('fun');
export { TheSneakerDatabaseClient } from './TheSneakerDatabaseClient';

export {
GetSneakersOptions,
GetSneakersResponse,
MethodResponse,
SearchOptions,
SearchResponse,
Sneaker,
} from './interfaces';
62 changes: 62 additions & 0 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
export interface Image {
original: string;
small: string;
thumbnail: string;
}

export interface Links {
stockX: string;
goat: string;
flightClub: string;
stadiumGoods: string;
}

export interface Sneaker {
id: string;
sku: string;
brand: string;
name: string;
colorway: string;
gender: string;
silhouette: string;
retailPrice: number;
releaseDate: Date;
releaseYear: number;
estimatedMarketValue: number;
links: Links;
image: Image;
story: string;
}

export type MethodResponse<T> = { error?: Error; response?: T };

export interface GetSneakersOptions {
limit: number;
gender?: string;
silhouette?: string;
colorway?: string;
releaseYear?: string;
page?: number;
releaseDate?: Date | string;
sku?: string;
sort?: string;
name?: string;
brand?: string;
}

export interface GetSneakersResponse {
count: number;
results: Sneaker[];
}

export interface SearchOptions {
limit: number;
page?: number;
query?: string;
}

export interface SearchResponse {
count: number;
totalPages: number;
results: Sneaker[];
}
11 changes: 11 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import axios from 'axios';

export const handleAxiosError = (error: any) => {
if (axios.isAxiosError(error)) {
// If the error is an AxiosError, return the response data
return error.response?.data as Error;
} else {
// If the error is not an AxiosError, return the error as is
return error as Error;
}
};
2 changes: 2 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"strictNullChecks": true,
"allowSyntheticDefaultImports": true,
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "**/*.test.ts"],
Expand Down

0 comments on commit cfb8bb0

Please sign in to comment.