chore: migrate services and utils
This commit is contained in:
parent
d142e3f5b8
commit
f87c41a053
|
|
@ -4,3 +4,4 @@ dist
|
||||||
*.local
|
*.local
|
||||||
.idea
|
.idea
|
||||||
*.iml
|
*.iml
|
||||||
|
cypress/videos
|
||||||
|
|
|
||||||
13
index.html
13
index.html
|
|
@ -2,12 +2,17 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<title>Conduit</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Vite App</title>
|
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
<link href="//code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css" rel="stylesheet" type="text/css">
|
||||||
|
<link href="//fonts.googleapis.com/css?family=Titillium+Web:700|Source+Serif+Pro:400,700|Merriweather+Sans:400,700|Source+Sans+Pro:400,300,600,700,300italic,400italic,600italic,700italic" rel="stylesheet" type="text/css">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="//demo.productionready.io/main.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script type="module" src="/src/main.ts"></script>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
36
package.json
36
package.json
|
|
@ -6,19 +6,27 @@
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"lint": "eslint --ext .ts,.vue src",
|
"lint": "eslint --ext .ts,.vue src",
|
||||||
"test:e2e": "cypress run"
|
"test:unit": "jest",
|
||||||
|
"test:e2e": "cypress run",
|
||||||
|
"test": "yarn tsc && yarn lint && yarn test:unit && yarn test:e2e"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"vue": "^3.0.0"
|
"vue": "^3.0.0",
|
||||||
|
"vue-router": "^4.0.0-beta.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/jest": "^26.0.14",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^4.3.0",
|
||||||
"@typescript-eslint/parser": "^4.2.0",
|
"@typescript-eslint/parser": "^4.2.0",
|
||||||
"@vue/compiler-sfc": "^3.0.0-rc.1",
|
"@vue/compiler-sfc": "^3.0.0-rc.1",
|
||||||
"cypress": "^5.3.0",
|
"cypress": "^5.3.0",
|
||||||
"eslint": "^7.10.0",
|
"eslint": "^7.10.0",
|
||||||
"eslint-plugin-vue": "^7.0.0-beta.4",
|
"eslint-plugin-vue": "^7.0.0-beta.4",
|
||||||
"husky": "^4.3.0",
|
"husky": "^4.3.0",
|
||||||
|
"jest": "^26.4.2",
|
||||||
|
"jsdom": "^16.4.0",
|
||||||
"lint-staged": "^10.4.0",
|
"lint-staged": "^10.4.0",
|
||||||
|
"ts-jest": "^26.4.1",
|
||||||
"typescript": "^4.0.3",
|
"typescript": "^4.0.3",
|
||||||
"vite": "^1.0.0-rc.1"
|
"vite": "^1.0.0-rc.1"
|
||||||
},
|
},
|
||||||
|
|
@ -34,10 +42,14 @@
|
||||||
"parser": "vue-eslint-parser",
|
"parser": "vue-eslint-parser",
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"parser": "@typescript-eslint/parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"parserOptions": {
|
||||||
|
"project": "./tsconfig.json"
|
||||||
|
},
|
||||||
"sourceType": "module"
|
"sourceType": "module"
|
||||||
},
|
},
|
||||||
"extends": [
|
"extends": [
|
||||||
"eslint:recommended",
|
"plugin:@typescript-eslint/eslint-recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
"plugin:vue/vue3-recommended"
|
"plugin:vue/vue3-recommended"
|
||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
|
|
@ -48,7 +60,23 @@
|
||||||
"quotes": [
|
"quotes": [
|
||||||
"error",
|
"error",
|
||||||
"single"
|
"single"
|
||||||
]
|
],
|
||||||
|
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||||
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
"@typescript-eslint/ban-ts-comment": "off"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"preset": "ts-jest",
|
||||||
|
"testEnvironment": "jsdom",
|
||||||
|
"testMatch": [
|
||||||
|
"<rootDir>/src/**/*.spec.ts"
|
||||||
|
],
|
||||||
|
"modulePaths": [
|
||||||
|
"<rootDir>"
|
||||||
|
],
|
||||||
|
"setupFilesAfterEnv": [
|
||||||
|
"<rootDir>/src/setup-test.ts"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
import FetchRequest from './utils/request'
|
||||||
|
import parseStorageGet from './utils/parse-storage-get'
|
||||||
|
|
||||||
|
export const limit = 10
|
||||||
|
|
||||||
|
export const request = new FetchRequest({
|
||||||
|
prefix: `${process.env.API_HOST}/api`,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Token ${parseStorageGet('user')?.token}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export interface PostLoginForm {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function postLogin(form: PostLoginForm) {
|
||||||
|
return request.post<UserResponse>('/users/login', { user: form }).then(res => res.user)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PostRegisterForm extends PostLoginForm {
|
||||||
|
username: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function postRegister(form: PostRegisterForm) {
|
||||||
|
return request.post<UserResponse>('/users', { user: form }).then(res => res.user)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAllTags() {
|
||||||
|
return request.get<TagsResponse>('/tags').then(res => res.tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PostArticleForm {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
body: string;
|
||||||
|
tagList: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function postArticle(form: PostArticleForm) {
|
||||||
|
return request.post<ArticleResponse>('/articles', { article: form })
|
||||||
|
.then(res => res.article)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getArticle(slug: string) {
|
||||||
|
return request.get<ArticleResponse>(`/articles/${slug}`).then(res => res.article)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function putArticle(slug: string, form: PostArticleForm) {
|
||||||
|
return request.put<ArticleResponse>(`/articles/${slug}`, { article: form })
|
||||||
|
.then(res => res.article)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getArticles(page = 1) {
|
||||||
|
const params = { limit, offset: (page - 1) * limit }
|
||||||
|
return request.get<ArticlesResponse>('/articles', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getFeeds(page = 1) {
|
||||||
|
const params = { limit, offset: (page - 1) * limit }
|
||||||
|
return request.get<ArticlesResponse>('/articles/feed', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getArticlesByTag(tagName: string, page = 1) {
|
||||||
|
const params = { tag: tagName, limit, offset: (page - 1) * limit }
|
||||||
|
return request.get<ArticlesResponse>('/articles', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getProfileArticles(username: string, page = 1) {
|
||||||
|
const params = { limit, offset: (page - 1) * limit, author: username }
|
||||||
|
return request.get<ArticlesResponse>('/articles', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getFavoritedArticles(username: string, page = 1) {
|
||||||
|
const params = { limit, offset: (page - 1) * limit, favorited: username }
|
||||||
|
return request.get<ArticlesResponse>('/articles', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCommentsByArticle(slug: string) {
|
||||||
|
return request.get<CommentsResponse>(`/articles/${slug}/comments`).then(res => res.comments)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteComment(slug: string, commentId: number) {
|
||||||
|
return request.delete(`/articles/${slug}/comments/${commentId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function postComment(slug: string, body: string) {
|
||||||
|
return request.post<CommentResponse>(`/articles/${slug}/comments`, { comment: { body } })
|
||||||
|
.then(res => res.comment)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function postFavoriteArticle(slug: string) {
|
||||||
|
return request.post<ArticleResponse>(`/articles/${slug}/favorite`).then(res => res.article)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteFavoriteArticle(slug: string) {
|
||||||
|
return request.delete<ArticleResponse>(`/articles/${slug}/favorite`).then(res => res.article)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getProfile(username: string) {
|
||||||
|
return request.get<ProfileResponse>(`/profiles/${username}`).then(res => res.profile)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function putProfile(form: Partial<Profile>) {
|
||||||
|
return request.put<ProfileResponse>('/user', form).then(res => res.profile)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function postFollowProfile(username: string) {
|
||||||
|
return request.post<ProfileResponse>(`/profiles/${username}/follow`).then(res => res.profile)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteFollowProfile(username: string) {
|
||||||
|
return request.delete<ProfileResponse>(`/profiles/${username}/follow`).then(res => res.profile)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
import 'jest'
|
||||||
|
|
||||||
|
jest.spyOn(window.Storage.prototype, 'getItem').mockReturnValue('')
|
||||||
|
jest.spyOn(window.Storage.prototype, 'setItem').mockImplementation()
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
declare module '*.vue' {
|
||||||
|
import Vue from 'vue'
|
||||||
|
|
||||||
|
export default Vue
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
declare interface Article {
|
||||||
|
title: string;
|
||||||
|
slug: string;
|
||||||
|
body: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
tagList: string[];
|
||||||
|
description: string;
|
||||||
|
author: Profile;
|
||||||
|
favorited: boolean;
|
||||||
|
favoritesCount: number;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
declare interface ArticleComment {
|
||||||
|
id: number;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
body: string;
|
||||||
|
author: Profile;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
declare interface ResponseError {
|
||||||
|
[field: string]: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
declare interface Response {
|
||||||
|
errors: ResponseError;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare interface UserResponse {
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare interface TagsResponse {
|
||||||
|
tags: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
declare interface ProfileResponse {
|
||||||
|
profile: Profile;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare interface ArticleResponse {
|
||||||
|
article: Article;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare interface ArticlesResponse {
|
||||||
|
articles: Article[];
|
||||||
|
articlesCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare interface CommentResponse {
|
||||||
|
comment: ArticleComment;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare interface CommentsResponse {
|
||||||
|
comments: ArticleComment[];
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
interface RootState {
|
||||||
|
user: User | undefined;
|
||||||
|
errors: ResponseError;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Action = ActionSetErrors | ActionCleanErrors | ActionUpdateUser | { type: '' }
|
||||||
|
|
||||||
|
interface ActionSetErrors {
|
||||||
|
type: 'SET_ERRORS';
|
||||||
|
errors: ResponseError;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ActionCleanErrors {
|
||||||
|
type: 'CLEAN_ERRORS';
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ActionUpdateUser {
|
||||||
|
type: 'UPDATE_USER';
|
||||||
|
user?: Profile | User;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
declare interface Profile {
|
||||||
|
username: string;
|
||||||
|
bio: string;
|
||||||
|
image: string;
|
||||||
|
following: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare interface User {
|
||||||
|
id: number;
|
||||||
|
email: string;
|
||||||
|
username: string;
|
||||||
|
bio: string | null;
|
||||||
|
image: string | null;
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { dateFilter } from 'src/utils/filters'
|
||||||
|
|
||||||
|
describe('# Date filters', function() {
|
||||||
|
it('should format date correctly', function() {
|
||||||
|
const dateString = '2019-01-01 00:00:00'
|
||||||
|
const result = dateFilter(dateString)
|
||||||
|
|
||||||
|
expect(result).toMatchInlineSnapshot('"January 1"')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
export const dateFilter = (dateString: string) => {
|
||||||
|
const date = new Date(dateString)
|
||||||
|
return date.toLocaleDateString('en-US', {
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import parseStorageGet from 'src/utils/parse-storage-get'
|
||||||
|
|
||||||
|
describe('# parse storage get', function () {
|
||||||
|
|
||||||
|
it('should get an object given valid local storage item', function () {
|
||||||
|
jest.spyOn(global.localStorage, 'getItem')
|
||||||
|
.mockReturnValue(JSON.stringify({ foo: 'bar' }))
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
const result = parseStorageGet('key')
|
||||||
|
expect(result).toMatchObject({ foo: 'bar' })
|
||||||
|
}).not.toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should get null given invalid storage item', function () {
|
||||||
|
jest.spyOn(global.localStorage, 'getItem')
|
||||||
|
.mockReturnValue('bar')
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
const result = parseStorageGet('key')
|
||||||
|
expect(result).toBeNull()
|
||||||
|
}).not.toThrow()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
export default function parseStorageGet (key: string) {
|
||||||
|
try {
|
||||||
|
const value = localStorage.getItem(key) || ''
|
||||||
|
return JSON.parse(value)
|
||||||
|
} catch (e) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,210 @@
|
||||||
|
import FetchRequest from 'src/utils/request'
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
global.fetch = jest.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
async json() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('# Request GET', function () {
|
||||||
|
|
||||||
|
it('should implement GET method', async function () {
|
||||||
|
const request = new FetchRequest()
|
||||||
|
await request.get('/path')
|
||||||
|
|
||||||
|
expect(global.fetch).toBeCalledTimes(1)
|
||||||
|
expect(global.fetch).toBeCalledWith('/path', expect.objectContaining({
|
||||||
|
method: 'GET',
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should can set prefix of request url with global', async function () {
|
||||||
|
const request = new FetchRequest({ prefix: '/prefix' })
|
||||||
|
await request.get('/path')
|
||||||
|
|
||||||
|
expect(global.fetch).toBeCalledWith('/prefix/path', expect.any(Object))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should can be set prefix of request url with single request', async function () {
|
||||||
|
const request = new FetchRequest()
|
||||||
|
await request.get('/path', { prefix: '/prefix' })
|
||||||
|
|
||||||
|
expect(global.fetch).toBeCalledWith('/prefix/path', expect.any(Object))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can be convert query object to query string in request url', async function () {
|
||||||
|
const request = new FetchRequest()
|
||||||
|
await request.get('/path', { params: { foo: 'bar' } })
|
||||||
|
|
||||||
|
expect(global.fetch).toBeCalledWith('/path?foo=bar', expect.any(Object))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should converted response body to json', async function () {
|
||||||
|
global.fetch = jest.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
async json() {
|
||||||
|
return {
|
||||||
|
foo: 'bar',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const request = new FetchRequest()
|
||||||
|
const response = await request.get('/path')
|
||||||
|
|
||||||
|
expect(response).toMatchObject({ foo: 'bar' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw Error with response when request status code is not 2xx', async function () {
|
||||||
|
global.fetch = jest.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
status: 400,
|
||||||
|
statusText: 'Bad request',
|
||||||
|
async json() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const request = new FetchRequest()
|
||||||
|
|
||||||
|
await expect(request.post('/path')).rejects.toThrow('Bad request')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('# Request POST', function () {
|
||||||
|
|
||||||
|
it('should implement POST method', async function () {
|
||||||
|
const request = new FetchRequest()
|
||||||
|
await request.post('/path')
|
||||||
|
|
||||||
|
expect(global.fetch).toBeCalledTimes(1)
|
||||||
|
expect(global.fetch).toBeCalledWith('/path', expect.objectContaining({
|
||||||
|
method: 'POST',
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should can set prefix of request url with global', async function () {
|
||||||
|
const request = new FetchRequest({ prefix: '/prefix' })
|
||||||
|
await request.post('/path')
|
||||||
|
|
||||||
|
expect(global.fetch).toBeCalledWith('/prefix/path', expect.any(Object))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should can be set prefix of request url with single request', async function () {
|
||||||
|
const request = new FetchRequest()
|
||||||
|
await request.post('/path', {}, { prefix: '/prefix' })
|
||||||
|
|
||||||
|
expect(global.fetch).toBeCalledWith('/prefix/path', expect.any(Object))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should can be send json data in request body', async function () {
|
||||||
|
const request = new FetchRequest()
|
||||||
|
await request.post('/path', { foo: 'bar' })
|
||||||
|
|
||||||
|
expect(global.fetch).toBeCalledWith('/path', expect.objectContaining({
|
||||||
|
body: JSON.stringify({ foo: 'bar' }),
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can be convert query object to query string in request url', async function () {
|
||||||
|
const request = new FetchRequest()
|
||||||
|
await request.post('/path', {}, { params: { foo: 'bar' } })
|
||||||
|
|
||||||
|
expect(global.fetch).toBeCalledWith('/path?foo=bar', expect.any(Object))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should converted response body to json', async function () {
|
||||||
|
global.fetch = jest.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
async json() {
|
||||||
|
return {
|
||||||
|
foo: 'bar',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const request = new FetchRequest()
|
||||||
|
const response = await request.post('/path')
|
||||||
|
|
||||||
|
expect(response).toMatchObject({ foo: 'bar' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw Error with response when request status code is not 2xx', async function () {
|
||||||
|
global.fetch = jest.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
status: 400,
|
||||||
|
statusText: 'Bad request',
|
||||||
|
async json() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const request = new FetchRequest()
|
||||||
|
|
||||||
|
await expect(request.post('/path')).rejects.toThrow('Bad request')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw Error with 4xx code', function () {
|
||||||
|
global.fetch = jest.fn().mockReturnValue({
|
||||||
|
ok: true,
|
||||||
|
status: 422,
|
||||||
|
async json() {
|
||||||
|
return {
|
||||||
|
errors: {
|
||||||
|
some: [ 'error' ],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const request = new FetchRequest()
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
request.get('/')
|
||||||
|
}).toThrow()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('# Request DELETE', function () {
|
||||||
|
it('should implement DELETE method', async function () {
|
||||||
|
const request = new FetchRequest()
|
||||||
|
await request.delete('/path')
|
||||||
|
|
||||||
|
expect(global.fetch).toBeCalledTimes(1)
|
||||||
|
expect(global.fetch).toBeCalledWith('/path', expect.objectContaining({
|
||||||
|
method: 'DELETE',
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('# Request PUT', function () {
|
||||||
|
it('should implement PUT method', async function () {
|
||||||
|
const request = new FetchRequest()
|
||||||
|
await request.put('/path')
|
||||||
|
|
||||||
|
expect(global.fetch).toBeCalledTimes(1)
|
||||||
|
expect(global.fetch).toBeCalledWith('/path', expect.objectContaining({
|
||||||
|
method: 'PUT',
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('# Request PATCH', function () {
|
||||||
|
it('should implement PATCH method', async function () {
|
||||||
|
const request = new FetchRequest()
|
||||||
|
await request.patch('/path')
|
||||||
|
|
||||||
|
expect(global.fetch).toBeCalledTimes(1)
|
||||||
|
expect(global.fetch).toBeCalledWith('/path', expect.objectContaining({
|
||||||
|
method: 'PATCH',
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
interface FetchRequestOptions {
|
||||||
|
prefix: string;
|
||||||
|
headers: Record<string, any>;
|
||||||
|
params: Record<string, any>;
|
||||||
|
responseInterceptor: (response: Response) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class FetchRequest {
|
||||||
|
defaultOptions: FetchRequestOptions = {
|
||||||
|
prefix: '',
|
||||||
|
headers: {},
|
||||||
|
params: {},
|
||||||
|
responseInterceptor: (response) => void response,
|
||||||
|
}
|
||||||
|
public options: FetchRequestOptions
|
||||||
|
|
||||||
|
constructor(options: Partial<FetchRequestOptions> = {}) {
|
||||||
|
this.options = Object.assign({}, this.defaultOptions, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateFinalUrl = (url: string, options: Partial<FetchRequestOptions> = {}) => {
|
||||||
|
const prefix = options.prefix || this.options.prefix || ''
|
||||||
|
const params = options.params || {}
|
||||||
|
|
||||||
|
let finalUrl = `${prefix}${url}`
|
||||||
|
if (Object.keys(params).length) {
|
||||||
|
const queryString = Object.keys(params).map(key => `${key}=${params[key]}`).join('&')
|
||||||
|
finalUrl += `?${queryString}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleResponse = (response: Response) => {
|
||||||
|
this.options.responseInterceptor(response)
|
||||||
|
return response.json()
|
||||||
|
.then(json => {
|
||||||
|
if (response.status >= 200 && response.status < 300) {
|
||||||
|
return json
|
||||||
|
}
|
||||||
|
const error = new Error(response.statusText)
|
||||||
|
Object.assign(error, json, {
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
})
|
||||||
|
throw error
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get<T = any>(url: string, options: Partial<FetchRequestOptions> = {}): Promise<T> {
|
||||||
|
const finalUrl = this.generateFinalUrl(url, options)
|
||||||
|
return fetch(finalUrl, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: this.options.headers,
|
||||||
|
})
|
||||||
|
.then(this.handleResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
post<T = any>(url: string, data: Record<string, any> = {}, options: Partial<FetchRequestOptions> = {}): Promise<T> {
|
||||||
|
const finalUrl = this.generateFinalUrl(url, options)
|
||||||
|
|
||||||
|
return fetch(finalUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
headers: this.options.headers,
|
||||||
|
})
|
||||||
|
.then(this.handleResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
delete<T = any>(url: string, options: Partial<FetchRequestOptions> = {}): Promise<T> {
|
||||||
|
const finalUrl = this.generateFinalUrl(url, options)
|
||||||
|
|
||||||
|
return fetch(finalUrl, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: this.options.headers,
|
||||||
|
})
|
||||||
|
.then(this.handleResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
put<T = any>(url: string, data: Record<string, any> = {}, options: Partial<FetchRequestOptions> = {}): Promise<T> {
|
||||||
|
const finalUrl = this.generateFinalUrl(url, options)
|
||||||
|
|
||||||
|
return fetch(finalUrl, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
headers: this.options.headers,
|
||||||
|
})
|
||||||
|
.then(this.handleResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
patch<T = any>(url: string, data: Record<string, any> = {}, options: Partial<FetchRequestOptions> = {}): Promise<T> {
|
||||||
|
const finalUrl = this.generateFinalUrl(url, options)
|
||||||
|
|
||||||
|
return fetch(finalUrl, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
headers: this.options.headers,
|
||||||
|
})
|
||||||
|
.then(this.handleResponse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -63,5 +63,6 @@
|
||||||
/* Advanced Options */
|
/* Advanced Options */
|
||||||
"skipLibCheck": true, /* Skip type checking of declaration files. */
|
"skipLibCheck": true, /* Skip type checking of declaration files. */
|
||||||
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
|
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
|
||||||
}
|
},
|
||||||
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue