feat: move api-client as core's monorepo
Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
@@ -1 +1,15 @@
|
||||
assets/types/type.declare.ts
|
||||
|
||||
node_modules
|
||||
dist
|
||||
out
|
||||
packages/*/node_modules
|
||||
packages/*/dist
|
||||
packages/*/out
|
||||
packages/*/lib
|
||||
packages/*/build
|
||||
packages/*/coverage
|
||||
packages/*/test
|
||||
packages/*/tests
|
||||
packages/*/esm
|
||||
packages/*/types
|
||||
|
||||
45
.github/workflows/api-client.yml
vendored
Normal file
45
.github/workflows/api-client.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||
|
||||
name: Build @mx-space/api-client
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'package.json'
|
||||
- 'packages/api-client/**'
|
||||
pull_request:
|
||||
branches: [master, main]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [14.x, 16.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Cache pnpm modules
|
||||
uses: actions/cache@v3
|
||||
env:
|
||||
cache-name: cache-pnpm-modules
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ matrix.node-version }}-${{ hashFiles('**/package.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-${{ env.cache-name }}-${{ matrix.node-version }}-${{ hashFiles('**/package.json') }}
|
||||
|
||||
- uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 7.x.x
|
||||
run_install: true
|
||||
- run: pnpm run test
|
||||
- run: pnpm run package
|
||||
env:
|
||||
CI: true
|
||||
13
.prettierignore
Normal file
13
.prettierignore
Normal file
@@ -0,0 +1,13 @@
|
||||
node_modules
|
||||
dist
|
||||
out
|
||||
packages/*/node_modules
|
||||
packages/*/dist
|
||||
packages/*/out
|
||||
packages/*/lib
|
||||
packages/*/build
|
||||
packages/*/coverage
|
||||
packages/*/test
|
||||
packages/*/tests
|
||||
packages/*/esm
|
||||
packages/*/types
|
||||
@@ -33,12 +33,12 @@
|
||||
"dev": "npm run start",
|
||||
"repl": "npm run start -- --entryFile repl",
|
||||
"bundle": "rimraf out && npm run build && cd dist/src && npx ncc build main.js -o ../../out -m -t && cd ../.. && chmod +x out/index.js",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\" \"packages/**/*.ts\"",
|
||||
"start": "cross-env NODE_ENV=development nest start -w --path tsconfig.json",
|
||||
"start:debug": "cross-env NODE_ENV=development nest start --debug --watch",
|
||||
"start:cluster": "cross-env NODE_ENV=development nest start --watch -- --cluster --workers 2",
|
||||
"start:prod": "cross-env NODE_ENV=production node dist/src/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"lint": "eslint \"{src,apps,libs,test,packages}/**/*.ts\" --fix",
|
||||
"prod": "cross-env NODE_ENV=production pm2-runtime start ecosystem.config.js",
|
||||
"prod:pm2": "cross-env NODE_ENV=production pm2 restart ecosystem.config.js",
|
||||
"prod:stop": "pm2 stop ecosystem.config.js",
|
||||
|
||||
6
packages/api-client/.gitignore
vendored
Normal file
6
packages/api-client/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
esm
|
||||
lib
|
||||
dist
|
||||
build
|
||||
|
||||
types
|
||||
3
packages/api-client/.npmrc
Normal file
3
packages/api-client/.npmrc
Normal file
@@ -0,0 +1,3 @@
|
||||
registry=https://registry.npmjs.org
|
||||
|
||||
strict-peer-dependencies=false
|
||||
7
packages/api-client/__tests__/adaptors/axios.spec.ts
Normal file
7
packages/api-client/__tests__/adaptors/axios.spec.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { axiosAdaptor } from '~/adaptors/axios'
|
||||
|
||||
import { testAdaptor } from '../helpers/adaptor-test'
|
||||
|
||||
describe('test axios adaptor', () => {
|
||||
testAdaptor(axiosAdaptor)
|
||||
})
|
||||
9
packages/api-client/__tests__/adaptors/ky.spec.ts
Normal file
9
packages/api-client/__tests__/adaptors/ky.spec.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import '../helpers/global-fetch'
|
||||
|
||||
import { defaultKyAdaptor } from '~/adaptors/ky'
|
||||
|
||||
import { testAdaptor } from '../helpers/adaptor-test'
|
||||
|
||||
describe('test ky adaptor', () => {
|
||||
testAdaptor(defaultKyAdaptor)
|
||||
})
|
||||
@@ -0,0 +1,7 @@
|
||||
import { umiAdaptor } from '~/adaptors/umi-request'
|
||||
|
||||
import { testAdaptor } from '../helpers/adaptor-test'
|
||||
|
||||
describe('test umi-request adaptor', () => {
|
||||
testAdaptor(umiAdaptor)
|
||||
})
|
||||
389
packages/api-client/__tests__/contronllers/aggregate.test.ts
Normal file
389
packages/api-client/__tests__/contronllers/aggregate.test.ts
Normal file
@@ -0,0 +1,389 @@
|
||||
import camelcaseKeys from 'camelcase-keys'
|
||||
|
||||
import { mockRequestInstance } from '~/__tests__/helpers/instance'
|
||||
import { mockResponse } from '~/__tests__/helpers/response'
|
||||
import { AggregateController } from '~/controllers'
|
||||
import { TimelineType } from '~/models/aggregate'
|
||||
|
||||
describe('test aggregate client', () => {
|
||||
const client = mockRequestInstance(AggregateController)
|
||||
test('GET /aggregate', async () => {
|
||||
const mocked = mockResponse(
|
||||
'/aggregate',
|
||||
// https://api.innei.ren/v2/aggregate
|
||||
|
||||
{
|
||||
user: {
|
||||
id: '5ea4fe632507ba128f4c938c',
|
||||
introduce: '这是我的小世界呀',
|
||||
mail: 'i@innei.ren',
|
||||
url: 'https://innei.ren',
|
||||
name: 'Innei',
|
||||
social_ids: {
|
||||
bili_id: 26578164,
|
||||
netease_id: 84302804,
|
||||
github: 'Innei',
|
||||
},
|
||||
username: 'Innei',
|
||||
created: '2020-04-26T03:22:11.784Z',
|
||||
modified: '2020-11-13T09:38:49.014Z',
|
||||
last_login_time: '2021-11-10T04:47:09.329Z',
|
||||
avatar: 'https://cdn.innei.ren/avatar.png',
|
||||
},
|
||||
seo: {
|
||||
title: '静かな森',
|
||||
description: '致虚极,守静笃。',
|
||||
keywords: ['blog', 'mx-space', 'space', '静かな森'],
|
||||
},
|
||||
categories: [
|
||||
{
|
||||
id: '5eb2c62a613a5ab0642f1f7a',
|
||||
type: 0,
|
||||
count: 34,
|
||||
name: '编程',
|
||||
slug: 'programming',
|
||||
created: '2020-05-06T14:14:02.339Z',
|
||||
},
|
||||
{
|
||||
id: '5eb2c62a613a5ab0642f1f7b',
|
||||
type: 0,
|
||||
count: 19,
|
||||
name: '折腾',
|
||||
slug: 'Z-Turn',
|
||||
created: '2020-05-06T14:14:02.356Z',
|
||||
},
|
||||
{
|
||||
id: '5eb2c62a613a5ab0642f1f7c',
|
||||
type: 0,
|
||||
count: 18,
|
||||
name: '学习',
|
||||
slug: 'learning-process',
|
||||
created: '2020-05-06T14:14:02.364Z',
|
||||
},
|
||||
{
|
||||
id: '5eb2c62a613a5ab0642f1f7e',
|
||||
type: 0,
|
||||
count: 11,
|
||||
name: '技术',
|
||||
slug: 'technology',
|
||||
created: '2020-05-06T14:14:02.375Z',
|
||||
},
|
||||
{
|
||||
id: '5ed09730a0a8f94af569c96c',
|
||||
type: 0,
|
||||
count: 9,
|
||||
slug: 'website',
|
||||
name: '站点日志',
|
||||
created: '2020-05-29T05:01:36.315Z',
|
||||
},
|
||||
{
|
||||
id: '5ed5be418f3d6b6cb9ab7700',
|
||||
type: 0,
|
||||
count: 2,
|
||||
slug: 'reprinta',
|
||||
name: '转载',
|
||||
created: '2020-06-02T02:49:37.424Z',
|
||||
},
|
||||
],
|
||||
page_meta: [
|
||||
{
|
||||
id: '5e0318319332d06503619337',
|
||||
title: '自述',
|
||||
slug: 'about',
|
||||
order: 1,
|
||||
},
|
||||
{
|
||||
id: '5ea52aafa27a8a01dee55f53',
|
||||
order: 1,
|
||||
title: '栈',
|
||||
slug: 'stack',
|
||||
},
|
||||
{
|
||||
id: '5eb3b6e032c759467b0ad71e',
|
||||
order: 0,
|
||||
title: '历史',
|
||||
slug: 'history',
|
||||
},
|
||||
{
|
||||
id: '5eb54fc06c9cc86c3692349f',
|
||||
order: 0,
|
||||
title: '留言',
|
||||
slug: 'message',
|
||||
},
|
||||
{
|
||||
id: '5f0aaeeaddf2006d12773b12',
|
||||
order: 0,
|
||||
title: '此站点',
|
||||
slug: 'about-site',
|
||||
},
|
||||
{
|
||||
id: '601bce41a0630165aa48b9d0',
|
||||
order: 0,
|
||||
title: '迭代',
|
||||
slug: 'sprint',
|
||||
},
|
||||
],
|
||||
url: {
|
||||
ws_url: 'https://api.innei.ren',
|
||||
server_url: 'https://api.innei.ren/v2',
|
||||
web_url: 'https://innei.ren',
|
||||
},
|
||||
},
|
||||
)
|
||||
const data = await client.aggregate.getAggregateData()
|
||||
expect(data.$raw.data).toEqual(mocked)
|
||||
expect(data.user.name).toEqual(mocked.user.name)
|
||||
expect(data.url.webUrl).toEqual(mocked.url.web_url)
|
||||
})
|
||||
|
||||
test('GET /aggregate/top', async () => {
|
||||
const mocked = mockResponse(
|
||||
'/aggregate/top', // 20211114224602
|
||||
// https://api.innei.ren/v2/aggregate/top
|
||||
|
||||
{
|
||||
notes: [
|
||||
{
|
||||
id: '618e689174afb47066ab4548',
|
||||
title: '结束了,秋招',
|
||||
created: '2021-11-12T13:13:53.769Z',
|
||||
nid: 104,
|
||||
},
|
||||
{
|
||||
id: '6166c860035bf29e2c32ec40',
|
||||
title: '致逝去的青春',
|
||||
created: '2021-10-13T11:52:00.327Z',
|
||||
nid: 103,
|
||||
},
|
||||
{
|
||||
id: '61586b2f769f07b6852f3bf0',
|
||||
title: '论就业压力',
|
||||
created: '2021-10-02T14:22:39.934Z',
|
||||
nid: 102,
|
||||
},
|
||||
{
|
||||
id: '6149403ac0209bf8c57dcd15',
|
||||
title: '关于开源',
|
||||
created: '2021-09-21T02:15:22.161Z',
|
||||
nid: 101,
|
||||
},
|
||||
{
|
||||
id: '614206d4685e0c58294b1177',
|
||||
title: '最近这段日子',
|
||||
created: '2021-09-15T14:44:36.061Z',
|
||||
nid: 99,
|
||||
},
|
||||
{
|
||||
id: '612db5905c2f6f4d3ba136d2',
|
||||
title: '告别',
|
||||
created: '2021-08-31T04:52:32.865Z',
|
||||
nid: 97,
|
||||
},
|
||||
],
|
||||
posts: [
|
||||
{
|
||||
id: '61586f7e769f07b6852f3da0',
|
||||
slug: 'host-an-entire-Mix-Space-using-Docker',
|
||||
title: '终于可以使用 Docker 托管整个 Mix Space 了',
|
||||
created: '2021-10-02T14:41:02.742Z',
|
||||
category: {
|
||||
name: '站点日志',
|
||||
slug: 'website',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '614c539cfdf566c5d93a383f',
|
||||
slug: 'docker-node-ncc',
|
||||
title: '再遇 Docker,容器化 Node 应用',
|
||||
created: '2021-09-23T10:14:52.491Z',
|
||||
category: {
|
||||
name: '技术',
|
||||
slug: 'technology',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '613c91d0326cfffc61923ea2',
|
||||
slug: 'github-ci-cd',
|
||||
title: '使用 GitHub CI 云构建和自动部署',
|
||||
created: '2021-09-11T11:24:00.424Z',
|
||||
category: {
|
||||
name: '技术',
|
||||
slug: 'technology',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '611748895c2f6f4d3ba0d9b3',
|
||||
title: 'pageproxy,为 spa 提供初始数据注入',
|
||||
slug: 'pageproxy-spa-inject',
|
||||
created: '2021-08-14T04:37:29.880Z',
|
||||
category: {
|
||||
name: '编程',
|
||||
slug: 'programming',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '60cffff50ec52e0349cbb29f',
|
||||
title: '曲折的 Vue 3 重构后台之路',
|
||||
slug: 'mx-space-vue-3',
|
||||
created: '2021-06-21T02:56:53.126Z',
|
||||
category: {
|
||||
name: '编程',
|
||||
slug: 'programming',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '60b0a9852e75e2d635406879',
|
||||
title: '2021 年了,你不还来试试 TailwindCSS 吗',
|
||||
slug: 'tailwind-2021',
|
||||
created: '2021-05-28T08:27:49.346Z',
|
||||
category: {
|
||||
name: '编程',
|
||||
slug: 'programming',
|
||||
},
|
||||
},
|
||||
],
|
||||
says: [
|
||||
{
|
||||
id: '5eb52a73505ad56acfd25c94',
|
||||
source: '网络',
|
||||
text: '找不到路,就自己走一条出来。',
|
||||
author: '魅影陌客',
|
||||
created: '2020-05-08T09:46:27.694Z',
|
||||
},
|
||||
{
|
||||
id: '5eb52a94505ad56acfd25c95',
|
||||
source: '网络',
|
||||
text: '生活中若没有朋友,就像生活中没有阳光一样。',
|
||||
author: '能美',
|
||||
created: '2020-05-08T09:47:00.436Z',
|
||||
},
|
||||
{
|
||||
id: '5eb52aa7505ad56acfd25c97',
|
||||
source: '古城荆棘王',
|
||||
text: '没有期盼就不会出现奇迹。',
|
||||
author: 'M崽',
|
||||
created: '2020-05-08T09:47:19.285Z',
|
||||
},
|
||||
{
|
||||
id: '5eb9672de2369f53ff02a73c',
|
||||
source: '生活',
|
||||
text: '别让生活蹂躏了你眉间的温柔。',
|
||||
author: '结局',
|
||||
created: '2020-05-11T14:54:37.319Z',
|
||||
},
|
||||
{
|
||||
id: '5ebc8c4a5f0af03c7db9d56f',
|
||||
source: '佛教禅语',
|
||||
text: '忌妒别人,不会给自己增加任何的好处。忌妒别人,也不可能减少别人的成就。',
|
||||
author: 'hitokoto',
|
||||
created: '2020-05-14T00:09:46.926Z',
|
||||
},
|
||||
{
|
||||
id: '5ec0a02165d4d8495bc2e9f2',
|
||||
source: '凪的新生活',
|
||||
text: '你还是和原来一样带着面具生活,真是令人作呕。',
|
||||
created: '2020-05-17T02:23:29.570Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
const data = await client.aggregate.getTop()
|
||||
expect(data.$raw.data).toEqual(mocked)
|
||||
expect(data.posts[0].title).toEqual(
|
||||
'终于可以使用 Docker 托管整个 Mix Space 了',
|
||||
)
|
||||
expect(data.notes).toBeDefined()
|
||||
})
|
||||
|
||||
it('should filter undefined value in url query, get `/top`', async () => {
|
||||
mockResponse('/aggregate/top?size=1', { notes: [{ title: '1 ' }] })
|
||||
const data = await client.aggregate.getTop(1)
|
||||
expect(data.notes.length).toEqual(1)
|
||||
})
|
||||
|
||||
test('GET /timeline', async () => {
|
||||
const mocked = mockResponse('/aggregate/timeline', {
|
||||
data: {
|
||||
posts: [
|
||||
{
|
||||
id: '5eb2c62a613a5ab0642f1fb8',
|
||||
title: '如何配置zsh',
|
||||
slug: 'zshrc',
|
||||
created: '2018-09-04T10:34:00.000Z',
|
||||
modified: '2020-11-13T21:41:43.774Z',
|
||||
category: {
|
||||
id: '5eb2c62a613a5ab0642f1f7b',
|
||||
type: 0,
|
||||
count: 22,
|
||||
name: '折腾',
|
||||
slug: 'Z-Turn',
|
||||
created: '2020-05-06T14:14:02.356Z',
|
||||
},
|
||||
url: '/posts/Z-Turn/zshrc',
|
||||
},
|
||||
],
|
||||
notes: [
|
||||
{
|
||||
id: '5eb35d6f5ae43bbd0c90b8c0',
|
||||
title: '回顾快要逝去的寒假',
|
||||
created: '2019-02-19T11:59:00.000Z',
|
||||
modified: '2020-11-15T09:43:33.199Z',
|
||||
nid: 11,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const data = await client.aggregate.getTimeline()
|
||||
expect(data.$raw.data).toEqual(mocked)
|
||||
expect(data.data.posts?.[0].url).toEqual(mocked.data.posts[0].url)
|
||||
expect(data.data.notes).toBeDefined()
|
||||
})
|
||||
|
||||
test('GET /timeline', async () => {
|
||||
const mocked = mockResponse('/aggregate/timeline?type=1', {
|
||||
data: {
|
||||
notes: [
|
||||
{
|
||||
id: '5eb35d6f5ae43bbd0c90b8c0',
|
||||
title: '回顾快要逝去的寒假',
|
||||
created: '2019-02-19T11:59:00.000Z',
|
||||
modified: '2020-11-15T09:43:33.199Z',
|
||||
nid: 11,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const data = await client.aggregate.getTimeline({ type: TimelineType.Note })
|
||||
expect(data.$raw.data).toEqual(mocked)
|
||||
expect(data.data.notes?.[0]).toEqual(mocked.data.notes[0])
|
||||
expect(data.data.posts).toBeUndefined()
|
||||
})
|
||||
|
||||
test('GET /stat', async () => {
|
||||
const mocked = mockResponse('/aggregate/stat', {
|
||||
all_comments: 464,
|
||||
categories: 6,
|
||||
comments: 260,
|
||||
link_apply: 0,
|
||||
links: 43,
|
||||
notes: 89,
|
||||
pages: 6,
|
||||
posts: 93,
|
||||
says: 26,
|
||||
recently: 19,
|
||||
unread_comments: 0,
|
||||
online: 0,
|
||||
today_max_online: '3',
|
||||
today_online_total: '2565',
|
||||
call_time: 1054126,
|
||||
uv: 67733,
|
||||
today_ip_access_count: 138,
|
||||
})
|
||||
const data = await client.aggregate.getStat()
|
||||
expect(data.$raw.data).toEqual(mocked)
|
||||
expect(data).toEqual(camelcaseKeys(mocked))
|
||||
})
|
||||
})
|
||||
284
packages/api-client/__tests__/contronllers/category.test.ts
Normal file
284
packages/api-client/__tests__/contronllers/category.test.ts
Normal file
@@ -0,0 +1,284 @@
|
||||
import camelcaseKeys from 'camelcase-keys'
|
||||
|
||||
import { mockRequestInstance } from '~/__tests__/helpers/instance'
|
||||
import { mockResponse } from '~/__tests__/helpers/response'
|
||||
import { CategoryController } from '~/controllers'
|
||||
|
||||
describe('test Category client', () => {
|
||||
const client = mockRequestInstance(CategoryController)
|
||||
|
||||
test('GET /categories', async () => {
|
||||
const mocked = mockResponse('/categories', {
|
||||
data: [
|
||||
{
|
||||
id: '5eb2c62a613a5ab0642f1f7a',
|
||||
type: 0,
|
||||
count: 34,
|
||||
name: '编程',
|
||||
slug: 'programming',
|
||||
created: '2020-05-06T14:14:02.339Z',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const data = await client.category.getAllCategories()
|
||||
expect(data.$raw.data).toEqual(mocked)
|
||||
expect(data.data).toEqual(mocked.data)
|
||||
})
|
||||
|
||||
describe('GET /categories/:id', () => {
|
||||
test('get by slug', async () => {
|
||||
const mocked = mockResponse('/categories/programming', {
|
||||
data: {
|
||||
id: '5eb2c62a613a5ab0642f1f7a',
|
||||
type: 0,
|
||||
count: 2,
|
||||
name: '编程',
|
||||
slug: 'programming',
|
||||
created: '2020-05-06T14:14:02.339Z',
|
||||
children: [
|
||||
{
|
||||
id: '611748895c2f6f4d3ba0d9b3',
|
||||
title: 'pageproxy,为 spa 提供初始数据注入',
|
||||
slug: 'pageproxy-spa-inject',
|
||||
created: '2021-08-14T04:37:29.880Z',
|
||||
},
|
||||
{
|
||||
id: '60cffff50ec52e0349cbb29f',
|
||||
title: '曲折的 Vue 3 重构后台之路',
|
||||
slug: 'mx-space-vue-3',
|
||||
created: '2021-06-21T02:56:53.126Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const data = await client.category.getCategoryByIdOrSlug('programming')
|
||||
expect(data).toEqual(mocked.data)
|
||||
expect(data.count).toEqual(mocked.data.count)
|
||||
})
|
||||
|
||||
test('get by id', async () => {
|
||||
const mocked = mockResponse('/categories/5eb2c62a613a5ab0642f1f7a', {
|
||||
data: {
|
||||
id: '5eb2c62a613a5ab0642f1f7a',
|
||||
type: 0,
|
||||
count: 2,
|
||||
name: '编程',
|
||||
slug: 'programming',
|
||||
created: '2020-05-06T14:14:02.339Z',
|
||||
children: [
|
||||
{
|
||||
id: '611748895c2f6f4d3ba0d9b3',
|
||||
title: 'pageproxy,为 spa 提供初始数据注入',
|
||||
slug: 'pageproxy-spa-inject',
|
||||
created: '2021-08-14T04:37:29.880Z',
|
||||
},
|
||||
{
|
||||
id: '60cffff50ec52e0349cbb29f',
|
||||
title: '曲折的 Vue 3 重构后台之路',
|
||||
slug: 'mx-space-vue-3',
|
||||
created: '2021-06-21T02:56:53.126Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const data = await client.category.getCategoryByIdOrSlug(
|
||||
'5eb2c62a613a5ab0642f1f7a',
|
||||
)
|
||||
expect(data).toEqual(mocked.data)
|
||||
expect(data.count).toEqual(mocked.data.count)
|
||||
})
|
||||
})
|
||||
|
||||
test('GET /categories/:tagName', async () => {
|
||||
const mocked = mockResponse('/categories/react?tag=1', {
|
||||
tag: 'react',
|
||||
data: [
|
||||
{
|
||||
id: '607bfcedc98328a0d941a409',
|
||||
title: '虚拟列表与 Scroll Restoration',
|
||||
slug: 'visualize-list-scroll-restoration',
|
||||
category: {
|
||||
id: '5eb2c62a613a5ab0642f1f7a',
|
||||
type: 0,
|
||||
name: '编程',
|
||||
slug: 'programming',
|
||||
},
|
||||
created: '2021-04-18T09:33:33.271Z',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const data = await client.category.getTagByName('react')
|
||||
expect(data.tag).toEqual('react')
|
||||
expect(data.data).toEqual(mocked.data)
|
||||
})
|
||||
|
||||
test('GET /categories?type=0', async () => {
|
||||
const mocked = mockResponse('/categories?type=0', {
|
||||
data: [
|
||||
{
|
||||
id: '5eb2c62a613a5ab0642f1f7a',
|
||||
type: 0,
|
||||
count: 34,
|
||||
name: '编程',
|
||||
slug: 'programming',
|
||||
created: '2020-05-06T14:14:02.339Z',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const data = await client.category.getAllCategories()
|
||||
expect(data.data).toEqual(mocked.data)
|
||||
expect(data.$raw.data).toEqual(mocked)
|
||||
})
|
||||
|
||||
test('GET /categories?type=1', async () => {
|
||||
const mocked = mockResponse('/categories?type=1', {
|
||||
data: [
|
||||
{
|
||||
count: 2,
|
||||
name: 'docker',
|
||||
},
|
||||
{
|
||||
count: 1,
|
||||
name: 'react',
|
||||
},
|
||||
],
|
||||
})
|
||||
const data = await client.category.getAllTags()
|
||||
expect(data.data).toEqual(mocked.data)
|
||||
expect(data.$raw.data).toEqual(mocked)
|
||||
})
|
||||
|
||||
describe('GET /categories?ids=', () => {
|
||||
it('should get with ids array', async () => {
|
||||
const mocked = mockResponse(
|
||||
'/categories?ids=5ed5be418f3d6b6cb9ab7700,5eb2c62a613a5ab0642f1f7b',
|
||||
{
|
||||
entries: {
|
||||
'5ed5be418f3d6b6cb9ab7700': {
|
||||
id: '5ed5be418f3d6b6cb9ab7700',
|
||||
type: 0,
|
||||
count: 2,
|
||||
slug: 'reprint',
|
||||
name: '转载',
|
||||
created: '2020-06-02T02:49:37.424Z',
|
||||
children: [
|
||||
{
|
||||
id: '6005562e6b14b33be8afc1c3',
|
||||
allow_comment: true,
|
||||
copyright: false,
|
||||
tags: [],
|
||||
count: {
|
||||
read: 221,
|
||||
like: 3,
|
||||
},
|
||||
title: '[reprint] Your Own Time Zone',
|
||||
slug: 'your-own-time-zone',
|
||||
category_id: '5ed5be418f3d6b6cb9ab7700',
|
||||
modified: '2021-01-18T09:41:37.380Z',
|
||||
created: '2021-01-18T09:34:38.550Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
'5eb2c62a613a5ab0642f1f7b': {
|
||||
id: '5eb2c62a613a5ab0642f1f7b',
|
||||
type: 0,
|
||||
count: 19,
|
||||
name: '折腾',
|
||||
slug: 'Z-Turn',
|
||||
created: '2020-05-06T14:14:02.356Z',
|
||||
children: [
|
||||
{
|
||||
id: '5eb2c62a613a5ab0642f1f95',
|
||||
title: '从零开始的 Redux',
|
||||
slug: 'learn-redux',
|
||||
created: '2020-01-08T08:24:00.000Z',
|
||||
modified: '2020-11-14T06:50:19.164Z',
|
||||
category_id: '5eb2c62a613a5ab0642f1f7b',
|
||||
copyright: true,
|
||||
count: {
|
||||
read: 309,
|
||||
like: 2,
|
||||
},
|
||||
allow_comment: true,
|
||||
tags: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
const data = await client.category.getCategoryDetail([
|
||||
'5ed5be418f3d6b6cb9ab7700',
|
||||
'5eb2c62a613a5ab0642f1f7b',
|
||||
])
|
||||
|
||||
expect(data).toEqual(
|
||||
new Map([
|
||||
[
|
||||
'5ed5be418f3d6b6cb9ab7700',
|
||||
camelcaseKeys(mocked.entries['5ed5be418f3d6b6cb9ab7700'], {
|
||||
deep: true,
|
||||
}),
|
||||
],
|
||||
[
|
||||
'5eb2c62a613a5ab0642f1f7b',
|
||||
camelcaseKeys(mocked.entries['5eb2c62a613a5ab0642f1f7b'], {
|
||||
deep: true,
|
||||
}),
|
||||
],
|
||||
]),
|
||||
)
|
||||
|
||||
expect(data.$raw.data).toEqual(mocked)
|
||||
})
|
||||
|
||||
it('should get with single id', async () => {
|
||||
const mocked = mockResponse('/categories?ids=5ed5be418f3d6b6cb9ab7700', {
|
||||
entries: {
|
||||
'5ed5be418f3d6b6cb9ab7700': {
|
||||
id: '5ed5be418f3d6b6cb9ab7700',
|
||||
type: 0,
|
||||
count: 2,
|
||||
slug: 'reprint',
|
||||
name: '转载',
|
||||
created: '2020-06-02T02:49:37.424Z',
|
||||
children: [
|
||||
{
|
||||
id: '6005562e6b14b33be8afc1c3',
|
||||
allow_comment: true,
|
||||
copyright: false,
|
||||
tags: [],
|
||||
count: {
|
||||
read: 221,
|
||||
like: 3,
|
||||
},
|
||||
title: '[reprint] Your Own Time Zone',
|
||||
slug: 'your-own-time-zone',
|
||||
category_id: '5ed5be418f3d6b6cb9ab7700',
|
||||
modified: '2021-01-18T09:41:37.380Z',
|
||||
created: '2021-01-18T09:34:38.550Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const data = await client.category.getCategoryDetail(
|
||||
'5ed5be418f3d6b6cb9ab7700',
|
||||
)
|
||||
expect(data).toEqual(
|
||||
camelcaseKeys(mocked.entries['5ed5be418f3d6b6cb9ab7700'], {
|
||||
deep: true,
|
||||
}),
|
||||
)
|
||||
|
||||
expect(data.$raw.data).toEqual(mocked)
|
||||
})
|
||||
})
|
||||
})
|
||||
113
packages/api-client/__tests__/contronllers/comment.test.ts
Normal file
113
packages/api-client/__tests__/contronllers/comment.test.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import camelcaseKeys from 'camelcase-keys'
|
||||
|
||||
import { mockRequestInstance } from '~/__tests__/helpers/instance'
|
||||
import { mockResponse } from '~/__tests__/helpers/response'
|
||||
import { CommentController } from '~/controllers/comment'
|
||||
|
||||
describe('test note client', () => {
|
||||
const client = mockRequestInstance(CommentController)
|
||||
|
||||
test('get comment by id', async () => {
|
||||
mockResponse('/comments/11111', {
|
||||
ref_type: 'Page',
|
||||
state: 1,
|
||||
children: [],
|
||||
comments_index: 1,
|
||||
id: '6188b80b6290547080c9e1f3',
|
||||
author: 'yss',
|
||||
text: '做的框架模板不错. (•౪• ) ',
|
||||
url: 'https://gitee.com/kmyss/',
|
||||
key: '#26',
|
||||
ref: '5e0318319332d06503619337',
|
||||
created: '2021-11-08T05:39:23.010Z',
|
||||
avatar:
|
||||
'https://sdn.geekzu.org/avatar/8675fa376c044b0d93a23374549c4248?d=retro',
|
||||
})
|
||||
|
||||
const data = await client.comment.getById('11111')
|
||||
expect(data.children).toEqual([])
|
||||
expect(data.text).toBeDefined()
|
||||
})
|
||||
|
||||
test('get comment by ref id', async () => {
|
||||
const comments = [
|
||||
{
|
||||
ref_type: 'Page',
|
||||
state: 1,
|
||||
children: [],
|
||||
comments_index: 1,
|
||||
id: '6188b80b6290547080c9e1f3',
|
||||
author: 'yss',
|
||||
text: '做的框架模板不错. (•౪• ) ',
|
||||
url: 'https://gitee.com/kmyss/',
|
||||
key: '#26',
|
||||
ref: '5e0318319332d06503619337',
|
||||
created: '2021-11-08T05:39:23.010Z',
|
||||
avatar:
|
||||
'https://sdn.geekzu.org/avatar/8675fa376c044b0d93a23374549c4248?d=retro',
|
||||
},
|
||||
]
|
||||
mockResponse(
|
||||
'/comments/ref/5e0318319332d06503619337',
|
||||
{
|
||||
data: comments,
|
||||
pagination: {
|
||||
total: 23,
|
||||
current_page: 1,
|
||||
total_page: 3,
|
||||
size: 10,
|
||||
has_next_page: true,
|
||||
has_prev_page: false,
|
||||
},
|
||||
},
|
||||
'get',
|
||||
)
|
||||
|
||||
const data = await client.comment.getByRefId('5e0318319332d06503619337')
|
||||
|
||||
expect(data.data).toEqual(camelcaseKeys(comments, { deep: true }))
|
||||
expect(data.pagination.total).toEqual(23)
|
||||
expect(data.pagination.hasNextPage).toEqual(true)
|
||||
})
|
||||
|
||||
it('should comment successfully', async () => {
|
||||
mockResponse(
|
||||
'/comments/1',
|
||||
{
|
||||
id: '1',
|
||||
text: 'bar',
|
||||
},
|
||||
'post',
|
||||
)
|
||||
|
||||
const data = await client.comment.comment('1', {
|
||||
author: 'foo',
|
||||
text: 'bar',
|
||||
mail: 'xx@aa.com',
|
||||
})
|
||||
|
||||
expect(data).toEqual({
|
||||
id: '1',
|
||||
text: 'bar',
|
||||
})
|
||||
})
|
||||
|
||||
it('should reply comment successfully', async () => {
|
||||
mockResponse(
|
||||
'/comments/reply/1',
|
||||
{
|
||||
id: '1',
|
||||
text: 'bar',
|
||||
},
|
||||
'post',
|
||||
)
|
||||
|
||||
const data = await client.comment.reply('1', {
|
||||
author: 'f',
|
||||
text: 'bar',
|
||||
mail: 'a@q.com',
|
||||
})
|
||||
|
||||
expect(data).toEqual({ id: '1', text: 'bar' })
|
||||
})
|
||||
})
|
||||
95
packages/api-client/__tests__/contronllers/link.test.ts
Normal file
95
packages/api-client/__tests__/contronllers/link.test.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import camelcaseKeys from 'camelcase-keys'
|
||||
|
||||
import { mockRequestInstance } from '~/__tests__/helpers/instance'
|
||||
import { mockResponse } from '~/__tests__/helpers/response'
|
||||
import { LinkController } from '~/controllers'
|
||||
|
||||
describe('test link client, /links', () => {
|
||||
const client = mockRequestInstance(LinkController)
|
||||
|
||||
test('GET /', async () => {
|
||||
const mocked = mockResponse('/links?size=10&page=1', {
|
||||
data: [
|
||||
{
|
||||
type: 0,
|
||||
state: 0,
|
||||
id: '615c191ed5db15a1000e3ca6',
|
||||
url: 'https://barry-flynn.github.io/',
|
||||
avatar: 'https://i.loli.net/2021/09/09/5belKgmrkjN8ZQ7.jpg',
|
||||
description: '星河滚烫,无问西东。',
|
||||
name: '百里飞洋の博客',
|
||||
created: '2021-10-05T09:21:34.257Z',
|
||||
hide: false,
|
||||
},
|
||||
// ...
|
||||
],
|
||||
pagination: {
|
||||
total: 43,
|
||||
current_page: 1,
|
||||
total_page: 5,
|
||||
size: 10,
|
||||
has_next_page: true,
|
||||
has_prev_page: false,
|
||||
},
|
||||
})
|
||||
|
||||
const data = await client.link.getAllPaginated(1, 10)
|
||||
expect(data.$raw.data).toEqual(mocked)
|
||||
expect(data).toEqual(camelcaseKeys(mocked, { deep: true }))
|
||||
})
|
||||
|
||||
it('should `friend` == `link`', () => {
|
||||
expect(client.link).toEqual(client.friend)
|
||||
})
|
||||
|
||||
test('GET /all', async () => {
|
||||
const mocked = mockResponse('/links/all', {
|
||||
data: [],
|
||||
})
|
||||
|
||||
const data = await client.link.getAll()
|
||||
expect(data.$raw.data).toEqual(mocked)
|
||||
expect(data).toEqual(camelcaseKeys(mocked, { deep: true }))
|
||||
})
|
||||
|
||||
test('GET /:id', async () => {
|
||||
const mocked = mockResponse('/links/5eaabe10cd5bca719652179d', {
|
||||
id: '5eaabe10cd5bca719652179d',
|
||||
name: '静かな森',
|
||||
url: 'https://innei.ren',
|
||||
avatar: 'https://cdn.innei.ren/avatar.png',
|
||||
created: '2020-04-30T12:01:20.738Z',
|
||||
type: 0,
|
||||
description: '致虚极,守静笃。',
|
||||
state: 0,
|
||||
})
|
||||
const data = await client.link.getById('5eaabe10cd5bca719652179d')
|
||||
expect(data.$raw.data).toEqual(mocked)
|
||||
expect(data).toEqual(camelcaseKeys(mocked, { deep: true }))
|
||||
})
|
||||
|
||||
test('GET /audit', async () => {
|
||||
const mocked = mockResponse('/links/audit', {
|
||||
can: true,
|
||||
})
|
||||
|
||||
const allowed = await client.link.canApplyLink()
|
||||
|
||||
expect(allowed).toEqual(mocked.can)
|
||||
})
|
||||
|
||||
test('POST /audit', async () => {
|
||||
const payload = {
|
||||
author: '',
|
||||
avatar: '',
|
||||
name: '',
|
||||
url: '',
|
||||
description: '',
|
||||
email: '',
|
||||
}
|
||||
mockResponse('/links/audit', 'OK', 'post', payload)
|
||||
const res = await client.link.applyLink(payload)
|
||||
|
||||
expect(res).toEqual('OK')
|
||||
})
|
||||
})
|
||||
120
packages/api-client/__tests__/contronllers/note.test.ts
Normal file
120
packages/api-client/__tests__/contronllers/note.test.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { vi } from 'vitest'
|
||||
|
||||
import { mockRequestInstance } from '~/__tests__/helpers/instance'
|
||||
import { mockResponse } from '~/__tests__/helpers/response'
|
||||
import { axiosAdaptor } from '~/adaptors/axios'
|
||||
import { NoteController } from '~/controllers'
|
||||
import { RequestError } from '~/core'
|
||||
|
||||
const { spyOn } = vi
|
||||
|
||||
describe('test note client', () => {
|
||||
const client = mockRequestInstance(NoteController)
|
||||
|
||||
it('should get note list', async () => {
|
||||
mockResponse('/notes', {
|
||||
data: [],
|
||||
pagination: {},
|
||||
})
|
||||
|
||||
const data = await client.note.getList()
|
||||
expect(data).toEqual({ data: [], pagination: {} })
|
||||
})
|
||||
|
||||
it('should get post list filter filed', async () => {
|
||||
const mocked = mockResponse('/notes?page=1&size=1&select=created+title', {
|
||||
data: [{}],
|
||||
})
|
||||
|
||||
const data = await client.note.getList(1, 1, {
|
||||
select: ['created', 'title'],
|
||||
})
|
||||
expect(data).toEqual(mocked)
|
||||
})
|
||||
|
||||
it('should get latest note', async () => {
|
||||
mockResponse('/notes/latest', { data: { title: '1' } })
|
||||
const data = await client.note.getLatest()
|
||||
expect(data.data.title).toBe('1')
|
||||
})
|
||||
|
||||
it('should get middle list of note', async () => {
|
||||
mockResponse('/notes/list/1', {
|
||||
data: [
|
||||
{
|
||||
id: '1',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
},
|
||||
],
|
||||
size: 2,
|
||||
})
|
||||
const data = await client.note.getMiddleList('1')
|
||||
expect(data).toEqual({
|
||||
data: [
|
||||
{
|
||||
id: '1',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
},
|
||||
],
|
||||
size: 2,
|
||||
})
|
||||
})
|
||||
|
||||
it('should get single note', async () => {
|
||||
mockResponse('/notes/1', { title: '1' })
|
||||
|
||||
const data = await client.note.getNoteById('1')
|
||||
|
||||
expect(data).toStrictEqual({ title: '1' })
|
||||
expect(data.$raw).toBeDefined()
|
||||
})
|
||||
|
||||
it('should get note by nid', async () => {
|
||||
mockResponse('/notes/nid/1', { data: { title: '1' } })
|
||||
|
||||
const data = await client.note.getNoteById(1)
|
||||
expect(data.data.title).toBe('1')
|
||||
})
|
||||
|
||||
it('should get note by nid query single result', async () => {
|
||||
mockResponse('/notes/nid/1', { title: '1' })
|
||||
|
||||
const data = await client.note.getNoteById(1, undefined, true)
|
||||
expect(data.title).toBe('1')
|
||||
})
|
||||
|
||||
it('should like note', async () => {
|
||||
mockResponse('/notes/like/1', null)
|
||||
|
||||
const data = await client.note.likeIt('1')
|
||||
|
||||
expect(data).toBeNull()
|
||||
})
|
||||
|
||||
it('should forbidden if no password provide', async () => {
|
||||
spyOn(axiosAdaptor, 'get').mockRejectedValue({
|
||||
response: {
|
||||
data: {
|
||||
message: 'password required',
|
||||
},
|
||||
status: 403,
|
||||
},
|
||||
})
|
||||
|
||||
await expect(client.note.getNoteById('1')).rejects.toThrowError(
|
||||
RequestError,
|
||||
)
|
||||
})
|
||||
|
||||
test('GET /notes/topics/:id', async () => {
|
||||
mockResponse('/notes/topics/11111111', { data: [], pagination: {} })
|
||||
|
||||
const data = await client.note.getNoteByTopicId('11111111')
|
||||
|
||||
expect(data).toEqual({ data: [], pagination: {} })
|
||||
})
|
||||
})
|
||||
49
packages/api-client/__tests__/contronllers/page.test.ts
Normal file
49
packages/api-client/__tests__/contronllers/page.test.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { mockRequestInstance } from '~/__tests__/helpers/instance'
|
||||
import { mockResponse } from '~/__tests__/helpers/response'
|
||||
import { PageController } from '~/controllers/page'
|
||||
|
||||
describe('test page client', () => {
|
||||
const client = mockRequestInstance(PageController)
|
||||
|
||||
it('should get page list', async () => {
|
||||
mockResponse('/pages?size=10&page=1', {
|
||||
data: [],
|
||||
pagination: {},
|
||||
})
|
||||
const data = await client.page.getList()
|
||||
expect(data).toEqual({ data: [], pagination: {} })
|
||||
})
|
||||
|
||||
it('should get post list filter filed', async () => {
|
||||
const mocked = mockResponse('/pages?page=1&size=1&select=created+title', {
|
||||
data: [{}],
|
||||
})
|
||||
|
||||
const data = await client.page.getList(1, 1, {
|
||||
select: ['created', 'title'],
|
||||
})
|
||||
expect(data).toEqual(mocked)
|
||||
})
|
||||
|
||||
it('should get single page', async () => {
|
||||
mockResponse('/pages/1', {
|
||||
title: '1',
|
||||
})
|
||||
|
||||
const data = await client.page.getById('1')
|
||||
|
||||
expect(data).toStrictEqual({ title: '1' })
|
||||
expect(data.$raw).toBeDefined()
|
||||
})
|
||||
|
||||
it('should get by slug', async () => {
|
||||
mockResponse('/pages/slug/about', {
|
||||
title: 'about',
|
||||
text: 'about!',
|
||||
})
|
||||
|
||||
const data = await client.page.getBySlug('about')
|
||||
expect(data.title).toBe('about')
|
||||
expect(data.text).toBe('about!')
|
||||
})
|
||||
})
|
||||
77
packages/api-client/__tests__/contronllers/post.test.ts
Normal file
77
packages/api-client/__tests__/contronllers/post.test.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { mockRequestInstance } from '~/__tests__/helpers/instance'
|
||||
import { mockResponse } from '~/__tests__/helpers/response'
|
||||
import { PostController } from '~/controllers'
|
||||
|
||||
describe('test post client', () => {
|
||||
const client = mockRequestInstance(PostController)
|
||||
|
||||
it('should get post list', async () => {
|
||||
mockResponse('/posts', { data: [] })
|
||||
|
||||
const data = await client.post.getList()
|
||||
expect(data).toEqual({ data: [] })
|
||||
})
|
||||
|
||||
it('should get post list filter filed', async () => {
|
||||
const mocked = mockResponse('/posts?page=1&size=1&select=created+title', {
|
||||
data: [
|
||||
{
|
||||
id: '61586f7e769f07b6852f3da0',
|
||||
title: '终于可以使用 Docker 托管整个 Mix Space 了',
|
||||
created: '2021-10-02T14:41:02.742Z',
|
||||
category: null,
|
||||
},
|
||||
{
|
||||
id: '614c539cfdf566c5d93a383f',
|
||||
title: '再遇 Docker,容器化 Node 应用',
|
||||
created: '2021-09-23T10:14:52.491Z',
|
||||
category: null,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const data = await client.post.getList(1, 1, {
|
||||
select: ['created', 'title'],
|
||||
})
|
||||
expect(data).toEqual(mocked)
|
||||
})
|
||||
|
||||
it('should get latest post', async () => {
|
||||
mockResponse('/posts/latest', { title: '1' })
|
||||
const data = await client.post.getLatest()
|
||||
expect(data.title).toBe('1')
|
||||
})
|
||||
|
||||
it('should get single post by id', async () => {
|
||||
mockResponse('/posts/613c91d0326cfffc61923ea2', {
|
||||
title: '1',
|
||||
})
|
||||
|
||||
const data = await client.post.getPost('613c91d0326cfffc61923ea2')
|
||||
|
||||
expect(data).toStrictEqual({ title: '1' })
|
||||
expect(data.$raw).toBeDefined()
|
||||
})
|
||||
|
||||
it('should get single post by slug and category', async () => {
|
||||
mockResponse('/posts/website/host-an-entire-Mix-Space-using-Docker', {
|
||||
title: '1',
|
||||
})
|
||||
|
||||
const data = await client.post.getPost(
|
||||
'website',
|
||||
'host-an-entire-Mix-Space-using-Docker',
|
||||
)
|
||||
|
||||
expect(data).toStrictEqual({ title: '1' })
|
||||
expect(data.$raw).toBeDefined()
|
||||
})
|
||||
|
||||
it('should thumbs-up post', async () => {
|
||||
mockResponse('/posts/_thumbs-up?id=1', null)
|
||||
|
||||
const data = await client.post.thumbsUp('1')
|
||||
|
||||
expect(data).toBeNull()
|
||||
})
|
||||
})
|
||||
49
packages/api-client/__tests__/contronllers/recently.test.ts
Normal file
49
packages/api-client/__tests__/contronllers/recently.test.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { mockRequestInstance } from '~/__tests__/helpers/instance'
|
||||
import { mockResponse } from '~/__tests__/helpers/response'
|
||||
import { RecentlyController } from '~/controllers'
|
||||
|
||||
describe('test recently client, /recently', () => {
|
||||
const client = mockRequestInstance(RecentlyController)
|
||||
|
||||
test('GET /', async () => {
|
||||
const mocked = mockResponse('/recently?before=616182162657089e483aac5c', {
|
||||
data: [
|
||||
{
|
||||
id: '615c58cbf41656a119b1a4a9',
|
||||
content: 'x',
|
||||
created: '2021-10-05T13:53:15.891Z',
|
||||
},
|
||||
],
|
||||
})
|
||||
const data = await client.recently.getList('616182162657089e483aac5c')
|
||||
expect(data).toEqual(mocked)
|
||||
})
|
||||
|
||||
test('GET /latest', async () => {
|
||||
const mocked = mockResponse('/recently/latest', {
|
||||
id: '615c58cbf41656a119b1a4a9',
|
||||
content: 'xxx',
|
||||
created: '2021-10-05T13:53:15.891Z',
|
||||
})
|
||||
const data = await client.recently.getLatestOne()
|
||||
expect(data).toEqual(mocked)
|
||||
})
|
||||
|
||||
test('GET /all', async () => {
|
||||
const mocked = mockResponse('/recently/all', {
|
||||
data: [
|
||||
{
|
||||
id: '615c58cbf41656a119b1a4a9',
|
||||
content: 'x',
|
||||
created: '2021-10-05T13:53:15.891Z',
|
||||
},
|
||||
],
|
||||
})
|
||||
const data = await client.recently.getAll()
|
||||
expect(data).toEqual(mocked)
|
||||
})
|
||||
|
||||
it('should `recently` == `shorthand`', () => {
|
||||
expect(client.recently).toEqual(client.shorthand)
|
||||
})
|
||||
})
|
||||
106
packages/api-client/__tests__/contronllers/say.test.ts
Normal file
106
packages/api-client/__tests__/contronllers/say.test.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import camelcaseKeys from 'camelcase-keys'
|
||||
|
||||
import { mockRequestInstance } from '~/__tests__/helpers/instance'
|
||||
import { mockResponse } from '~/__tests__/helpers/response'
|
||||
import { SayController } from '~/controllers/say'
|
||||
|
||||
describe('test say client', () => {
|
||||
const client = mockRequestInstance(SayController)
|
||||
|
||||
test('GET /says/all', async () => {
|
||||
const mocked = mockResponse('/says/all', {
|
||||
data: [
|
||||
{
|
||||
id: '5eb52a73505ad56acfd25c94',
|
||||
source: '网络',
|
||||
text: '找不到路,就自己走一条出来。',
|
||||
author: '魅影陌客',
|
||||
created: '2020-05-08T09:46:27.694Z',
|
||||
},
|
||||
{
|
||||
id: '5eb52a94505ad56acfd25c95',
|
||||
source: '网络',
|
||||
text: '生活中若没有朋友,就像生活中没有阳光一样。',
|
||||
author: '能美',
|
||||
created: '2020-05-08T09:47:00.436Z',
|
||||
},
|
||||
],
|
||||
})
|
||||
const data = await client.say.getAll()
|
||||
expect(data.$raw.data).toEqual(mocked)
|
||||
expect(data.data).toEqual(mocked.data)
|
||||
expect(data.data[0].text).toEqual('找不到路,就自己走一条出来。')
|
||||
})
|
||||
|
||||
describe('GET /says', () => {
|
||||
it('should get without page and size', async () => {
|
||||
const mocked = mockResponse('/says', {
|
||||
data: [
|
||||
{
|
||||
id: '61397d9892992823d7329bc9',
|
||||
text: '每位师傅各有所长,我都会一点点。',
|
||||
author: '陆沉',
|
||||
source: '',
|
||||
created: '2021-09-09T03:20:56.769Z',
|
||||
},
|
||||
{
|
||||
id: '60853492fbfab397775cc12d',
|
||||
text: '我不是一个优秀的人,只是我们观测的角度不同。',
|
||||
created: '2021-04-25T09:21:22.115Z',
|
||||
},
|
||||
],
|
||||
pagination: {
|
||||
total: 26,
|
||||
current_page: 1,
|
||||
total_page: 3,
|
||||
size: 10,
|
||||
has_next_page: true,
|
||||
has_prev_page: false,
|
||||
},
|
||||
})
|
||||
|
||||
const data = await client.say.getAllPaginated()
|
||||
expect(data.$raw.data).toEqual(mocked)
|
||||
expect(data.data).toEqual(camelcaseKeys(mocked.data, { deep: true }))
|
||||
expect(data.data[0].text).toEqual('每位师傅各有所长,我都会一点点。')
|
||||
})
|
||||
|
||||
it('should with page and size', async () => {
|
||||
const mocked = await mockResponse('/says?size=1&page=1', {
|
||||
data: [
|
||||
{
|
||||
id: '61397d9892992823d7329bc9',
|
||||
text: '每位师傅各有所长,我都会一点点。',
|
||||
author: '陆沉',
|
||||
source: '',
|
||||
created: '2021-09-09T03:20:56.769Z',
|
||||
},
|
||||
],
|
||||
pagination: {
|
||||
total: 26,
|
||||
current_page: 1,
|
||||
total_page: 26,
|
||||
size: 1,
|
||||
has_next_page: true,
|
||||
has_prev_page: false,
|
||||
},
|
||||
})
|
||||
|
||||
const data = await client.say.getAllPaginated(1, 1)
|
||||
expect(data.$raw.data).toEqual(mocked)
|
||||
})
|
||||
})
|
||||
|
||||
test('GET /says/:id', async () => {
|
||||
const mocked = mockResponse('/says/61397d9892992823d7329bc9', {
|
||||
id: '61397d9892992823d7329bc9',
|
||||
text: '每位师傅各有所长,我都会一点点。',
|
||||
author: '陆沉',
|
||||
source: '',
|
||||
created: '2021-09-09T03:20:56.769Z',
|
||||
})
|
||||
const data = await client.say.getById('61397d9892992823d7329bc9')
|
||||
expect(data.$raw.data).toEqual(mocked)
|
||||
expect(data.id).toEqual('61397d9892992823d7329bc9')
|
||||
})
|
||||
})
|
||||
81
packages/api-client/__tests__/contronllers/search.test.ts
Normal file
81
packages/api-client/__tests__/contronllers/search.test.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import camelcaseKeys from 'camelcase-keys'
|
||||
|
||||
import { mockRequestInstance } from '~/__tests__/helpers/instance'
|
||||
import { mockResponse } from '~/__tests__/helpers/response'
|
||||
import { SearchController } from '~/controllers'
|
||||
|
||||
import mockData from '../mock/algolia.json'
|
||||
|
||||
describe('test search client, /search', () => {
|
||||
const client = mockRequestInstance(SearchController)
|
||||
|
||||
test('GET /search/post', async () => {
|
||||
const mocked = mockResponse('/search/post?keyword=1', {
|
||||
data: [
|
||||
{
|
||||
modified: '2020-11-14T16:15:36.162Z',
|
||||
id: '5eb2c62a613a5ab0642f1f80',
|
||||
title: '打印沙漏(C#实现)',
|
||||
slug: 'acm-test',
|
||||
created: '2019-01-31T13:02:00.000Z',
|
||||
category: {
|
||||
type: 0,
|
||||
id: '5eb2c62a613a5ab0642f1f7a',
|
||||
count: 56,
|
||||
name: '编程',
|
||||
slug: 'programming',
|
||||
created: '2020-05-06T14:14:02.339Z',
|
||||
},
|
||||
},
|
||||
],
|
||||
pagination: {
|
||||
total: 86,
|
||||
current_page: 1,
|
||||
total_page: 9,
|
||||
size: 10,
|
||||
has_next_page: true,
|
||||
has_prev_page: false,
|
||||
},
|
||||
})
|
||||
|
||||
const data = await client.search.search('post', '1')
|
||||
expect(data).toEqual(camelcaseKeys(mocked, { deep: true }))
|
||||
expect(data.data[0].id).toEqual('5eb2c62a613a5ab0642f1f80')
|
||||
})
|
||||
|
||||
test('GET /search/note', async () => {
|
||||
const mocked = mockResponse('/search/note?keyword=1', {
|
||||
data: [
|
||||
{
|
||||
modified: '2020-11-15T09:43:33.199Z',
|
||||
id: '5eb35d6f5ae43bbd0c90b8c0',
|
||||
title: '回顾快要逝去的寒假',
|
||||
created: '2019-02-19T11:59:00.000Z',
|
||||
nid: 11,
|
||||
},
|
||||
],
|
||||
pagination: {
|
||||
total: 86,
|
||||
current_page: 1,
|
||||
total_page: 9,
|
||||
size: 10,
|
||||
has_next_page: true,
|
||||
has_prev_page: false,
|
||||
},
|
||||
})
|
||||
|
||||
const data = await client.search.search('note', '1')
|
||||
expect(data).toEqual(camelcaseKeys(mocked, { deep: true }))
|
||||
expect(data.data[0].id).toEqual('5eb35d6f5ae43bbd0c90b8c0')
|
||||
})
|
||||
|
||||
test('GET /search/algolia', async () => {
|
||||
mockResponse('/search/algolia', mockData)
|
||||
const data = await client.search.searchByAlgolia('algolia')
|
||||
|
||||
expect(data.data[0].id).toEqual('5fe97d1d5b11408f99ada0fd')
|
||||
expect(data.raw).toBeDefined()
|
||||
|
||||
expect(data.$raw.data).toEqual(mockData)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,19 @@
|
||||
import { mockRequestInstance } from '~/__tests__/helpers/instance'
|
||||
import { mockResponse } from '~/__tests__/helpers/response'
|
||||
import { ServerlessController } from '~/controllers'
|
||||
|
||||
describe('test Snippet client', () => {
|
||||
const client = mockRequestInstance(ServerlessController)
|
||||
|
||||
test('GET /:reference/:name', async () => {
|
||||
const mocked = mockResponse('/serverless/api/ping', { message: 'pong' })
|
||||
|
||||
const data = await client.serverless.getByReferenceAndName<{}>(
|
||||
'api',
|
||||
'ping',
|
||||
)
|
||||
|
||||
expect(data).toEqual(mocked)
|
||||
expect(data.$raw.data).toEqual(mocked)
|
||||
})
|
||||
})
|
||||
38
packages/api-client/__tests__/contronllers/snippet.test.ts
Normal file
38
packages/api-client/__tests__/contronllers/snippet.test.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { mockRequestInstance } from '~/__tests__/helpers/instance'
|
||||
import { mockResponse } from '~/__tests__/helpers/response'
|
||||
import { SnippetController } from '~/controllers'
|
||||
|
||||
describe('test Snippet client', () => {
|
||||
const client = mockRequestInstance(SnippetController)
|
||||
|
||||
// test('GET /:id', async () => {
|
||||
// const mocked = mockResponse('/snippets/61a0cac4b4aefa000fcc1822', {
|
||||
// id: '61a0cac4b4aefa000fcc1822',
|
||||
// type: 'json',
|
||||
// private: true,
|
||||
// reference: 'theme',
|
||||
// raw: '{}',
|
||||
// name: 'config',
|
||||
// created: '2021-11-26T11:53:40.863Z',
|
||||
// updated: '2021-11-26T11:53:40.863Z',
|
||||
// })
|
||||
|
||||
// const data = await client.snippet.getById('61a0cac4b4aefa000fcc1822')
|
||||
|
||||
// expect(data).toEqual(mocked)
|
||||
// expect(data.$raw.data).toEqual(mocked)
|
||||
// expect(data.raw).toEqual('{}')
|
||||
// })
|
||||
|
||||
test('GET /:reference/:name', async () => {
|
||||
const mocked = mockResponse('/snippets/theme/config', {})
|
||||
|
||||
const data = await client.snippet.getByReferenceAndName<{}>(
|
||||
'theme',
|
||||
'config',
|
||||
)
|
||||
|
||||
expect(data).toEqual(mocked)
|
||||
expect(data.$raw.data).toEqual(mocked)
|
||||
})
|
||||
})
|
||||
17
packages/api-client/__tests__/contronllers/topic.test.ts
Normal file
17
packages/api-client/__tests__/contronllers/topic.test.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import camelcaseKeys from 'camelcase-keys'
|
||||
|
||||
import { mockRequestInstance } from '~/__tests__/helpers/instance'
|
||||
import { mockResponse } from '~/__tests__/helpers/response'
|
||||
import { TopicController } from '~/controllers/topic'
|
||||
|
||||
describe('test topic client', () => {
|
||||
const client = mockRequestInstance(TopicController)
|
||||
|
||||
test('GET /topics/slug/:slug', async () => {
|
||||
const mocked = mockResponse('/topics/slug/111', {
|
||||
name: 'name-topic',
|
||||
})
|
||||
const data = await client.topic.getTopicBySlug('111')
|
||||
expect(data).toEqual(camelcaseKeys(mocked))
|
||||
})
|
||||
})
|
||||
59
packages/api-client/__tests__/contronllers/user.test.ts
Normal file
59
packages/api-client/__tests__/contronllers/user.test.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import camelcaseKeys from 'camelcase-keys'
|
||||
|
||||
import { mockRequestInstance } from '~/__tests__/helpers/instance'
|
||||
import { mockResponse } from '~/__tests__/helpers/response'
|
||||
import { UserController } from '~/controllers'
|
||||
|
||||
describe('test user client', () => {
|
||||
const client = mockRequestInstance(UserController)
|
||||
|
||||
test('GET /master', async () => {
|
||||
const mocked = mockResponse('/master', {
|
||||
id: '5ea4fe632507ba128f4c938c',
|
||||
introduce: '这是我的小世界呀',
|
||||
mail: 'i@innei.ren',
|
||||
url: 'https://innei.ren',
|
||||
name: 'Innei',
|
||||
social_ids: {
|
||||
bili_id: 26578164,
|
||||
netease_id: 84302804,
|
||||
github: 'Innei',
|
||||
},
|
||||
username: 'Innei',
|
||||
created: '2020-04-26T03:22:11.784Z',
|
||||
modified: '2020-11-13T09:38:49.014Z',
|
||||
last_login_time: '2021-11-17T13:42:48.209Z',
|
||||
avatar: 'https://cdn.innei.ren/avatar.png',
|
||||
})
|
||||
const data = await client.user.getMasterInfo()
|
||||
expect(data.id).toEqual(mocked.id)
|
||||
expect(data).toEqual(camelcaseKeys(mocked, { deep: true }))
|
||||
expect(data.$raw.data).toEqual(mocked)
|
||||
})
|
||||
|
||||
test('POST /login', async () => {
|
||||
const mocked = mockResponse(
|
||||
'/master/login',
|
||||
{
|
||||
id: '5ea4fe632507ba128f4c938c',
|
||||
},
|
||||
'post',
|
||||
)
|
||||
const data = await client.user.login('test', 'test')
|
||||
expect(data.id).toEqual(mocked.id)
|
||||
expect(data.$raw.data).toEqual(mocked)
|
||||
})
|
||||
|
||||
test('GET /check_logged', async () => {
|
||||
const mocked = mockResponse('/check_logged?token=bearer token', {
|
||||
isGuest: true,
|
||||
})
|
||||
const data = await client.user.checkTokenValid('token')
|
||||
expect(data).toEqual(mocked)
|
||||
})
|
||||
|
||||
it('should call `master.xx` work', () => {
|
||||
expect(client.master.getMasterInfo).toBeInstanceOf(Function)
|
||||
expect(client.master).toEqual(client.user)
|
||||
})
|
||||
})
|
||||
293
packages/api-client/__tests__/core/client.test.ts
Normal file
293
packages/api-client/__tests__/core/client.test.ts
Normal file
@@ -0,0 +1,293 @@
|
||||
import { AxiosError, AxiosResponse } from 'axios'
|
||||
import { vi } from 'vitest'
|
||||
|
||||
import { axiosAdaptor } from '~/adaptors/axios'
|
||||
import { umiAdaptor } from '~/adaptors/umi-request'
|
||||
import {
|
||||
NoteController,
|
||||
PostController,
|
||||
allContollerNames,
|
||||
allControllers,
|
||||
} from '~/controllers'
|
||||
import { RequestError, createClient } from '~/core'
|
||||
import { IRequestAdapter } from '~/interfaces/adapter'
|
||||
import { ClientOptions } from '~/interfaces/client'
|
||||
|
||||
const { spyOn } = vi
|
||||
|
||||
// axios wrapper test
|
||||
const generateClient = <
|
||||
Response = AxiosResponse<unknown>,
|
||||
AdaptorType extends IRequestAdapter = typeof axiosAdaptor,
|
||||
>(
|
||||
adapter?: AdaptorType,
|
||||
options?: ClientOptions,
|
||||
) =>
|
||||
createClient(adapter ?? axiosAdaptor)<Response>(
|
||||
'http://127.0.0.1:2323',
|
||||
options,
|
||||
)
|
||||
describe('test client', () => {
|
||||
it('should create new client with axios', () => {
|
||||
const client = generateClient()
|
||||
expect(client).toBeDefined()
|
||||
})
|
||||
|
||||
describe('client `get` method', () => {
|
||||
test('case 1', async () => {
|
||||
spyOn(axiosAdaptor, 'get').mockImplementation((url) => {
|
||||
if (url === 'http://127.0.0.1:2323/a/a?foo=bar') {
|
||||
return Promise.resolve({ data: { ok: 1 } })
|
||||
}
|
||||
|
||||
return Promise.resolve({ data: null })
|
||||
})
|
||||
|
||||
const client = generateClient()
|
||||
const data = await client.proxy.a.a.get({ params: { foo: 'bar' } })
|
||||
|
||||
expect(data).toStrictEqual({ ok: 1 })
|
||||
})
|
||||
|
||||
test('case 2', async () => {
|
||||
spyOn(axiosAdaptor, 'get').mockImplementation((url) => {
|
||||
if (url === 'http://127.0.0.1:2323/a/a') {
|
||||
return Promise.resolve({ data: { ok: 1 } })
|
||||
}
|
||||
|
||||
return Promise.resolve({ data: null })
|
||||
})
|
||||
|
||||
const client = generateClient()
|
||||
const data = await client.proxy.a.a.get()
|
||||
|
||||
expect(data).toStrictEqual({ ok: 1 })
|
||||
|
||||
{
|
||||
spyOn(axiosAdaptor, 'get').mockImplementation((url) => {
|
||||
if (url === 'http://127.0.0.1:2323/a/b') {
|
||||
return Promise.resolve({ data: { ok: 1 } })
|
||||
}
|
||||
|
||||
return Promise.resolve({ data: null })
|
||||
})
|
||||
|
||||
const client = generateClient()
|
||||
const data = await client.proxy.a.b.get()
|
||||
|
||||
expect(data).toStrictEqual({ ok: 1 })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('should throw error if not inject other client', () => {
|
||||
const client = generateClient()
|
||||
allContollerNames.forEach((name) => {
|
||||
expect(() => (client as any)[name].name).toThrow(
|
||||
`${
|
||||
name.charAt(0).toUpperCase() + name.slice(1)
|
||||
} Client not inject yet, please inject with client.injectClients(...)`,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('should work if inject client', () => {
|
||||
const client = generateClient()
|
||||
|
||||
client.injectControllers(allControllers)
|
||||
allContollerNames.forEach((name) => {
|
||||
expect(() => (client as any)[name].name).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
it('should inject single client worked', () => {
|
||||
const client = generateClient()
|
||||
|
||||
client.injectControllers(PostController)
|
||||
expect(client.post.name).toBeDefined()
|
||||
})
|
||||
|
||||
it('should inject multi client worked', () => {
|
||||
const client = generateClient()
|
||||
|
||||
client.injectControllers(PostController, NoteController)
|
||||
expect(client.post.name).toBeDefined()
|
||||
expect(client.note.name).toBeDefined()
|
||||
})
|
||||
|
||||
it('should inject controller when init', () => {
|
||||
const client = createClient(axiosAdaptor)('http://127.0.0.1:2323', {
|
||||
controllers: [PostController, NoteController],
|
||||
})
|
||||
expect(client.post.name).toBeDefined()
|
||||
expect(client.note.name).toBeDefined()
|
||||
})
|
||||
|
||||
it('should infer response wrapper type', async () => {
|
||||
const client = generateClient<AxiosResponse>(axiosAdaptor)
|
||||
client.injectControllers(PostController)
|
||||
spyOn(axiosAdaptor, 'get').mockImplementation((url) => {
|
||||
if (url === 'http://127.0.0.1:2323/posts/latest') {
|
||||
return Promise.resolve({ data: { ok: 1 }, status: 200 })
|
||||
}
|
||||
|
||||
return Promise.resolve({ data: null })
|
||||
})
|
||||
|
||||
const data = await client.post.getLatest()
|
||||
|
||||
expect(data.$raw.status).toBe(200)
|
||||
})
|
||||
|
||||
it('should infer axios instance type', async () => {
|
||||
const client = generateClient<AxiosResponse>(axiosAdaptor)
|
||||
spyOn(axiosAdaptor, 'get').mockImplementation((url) => {
|
||||
if (url === 'http://127.0.0.1:2323/a') {
|
||||
return Promise.resolve({ data: { ok: 1 }, status: 200 })
|
||||
}
|
||||
|
||||
return Promise.resolve({ data: null })
|
||||
})
|
||||
expect(client.instance.default).toBe(axiosAdaptor.default)
|
||||
const res = await client.proxy.a.get()
|
||||
expect(res.$raw.status).toBe(200)
|
||||
|
||||
{
|
||||
spyOn(umiAdaptor, 'get').mockImplementation((url) => {
|
||||
if (url === 'http://127.0.0.1:2323/a') {
|
||||
return Promise.resolve({
|
||||
data: { ok: 1 },
|
||||
response: { status: 200, body: {} },
|
||||
})
|
||||
}
|
||||
|
||||
return Promise.resolve({ data: null })
|
||||
})
|
||||
const client2 = createClient(umiAdaptor)('http://127.0.0.1:2323')
|
||||
expect(client2.instance.default).toBe(umiAdaptor.default)
|
||||
const res = await client2.proxy.a.get()
|
||||
expect(res.$raw.response.status).toBe(200)
|
||||
expect(res.$raw.response.body).toStrictEqual({})
|
||||
}
|
||||
})
|
||||
|
||||
it('should resolve joint path call toString()', () => {
|
||||
const client = generateClient()
|
||||
{
|
||||
const path = client.proxy.foo.bar.toString()
|
||||
expect(path).toBe('/foo/bar')
|
||||
}
|
||||
|
||||
{
|
||||
const path = client.proxy.foo.bar.toString(true)
|
||||
expect(path).toBe('http://127.0.0.1:2323/foo/bar')
|
||||
}
|
||||
})
|
||||
|
||||
it('should do not json convert case if payload is string or other primitive type', async () => {
|
||||
const client = generateClient<AxiosResponse>(axiosAdaptor)
|
||||
spyOn(axiosAdaptor, 'get').mockImplementation((url) => {
|
||||
if (url === 'http://127.0.0.1:2323/a') {
|
||||
return Promise.resolve({ data: 'foo', status: 200 })
|
||||
}
|
||||
|
||||
return Promise.resolve({ data: null })
|
||||
})
|
||||
|
||||
const data = await client.proxy.a.get()
|
||||
expect(data).toBe('foo')
|
||||
})
|
||||
|
||||
it('should throw exception with custom message and code', async () => {
|
||||
const client = generateClient<AxiosResponse>(axiosAdaptor, {
|
||||
// @ts-ignore
|
||||
getCodeMessageFromException: (e: AxiosError) => {
|
||||
return {
|
||||
code: e.response?.status,
|
||||
message: e.message,
|
||||
}
|
||||
},
|
||||
})
|
||||
spyOn(axiosAdaptor, 'get').mockImplementation(() => {
|
||||
return Promise.reject(
|
||||
new AxiosError(
|
||||
'not found',
|
||||
'NOT_FOUND',
|
||||
{},
|
||||
{},
|
||||
{
|
||||
status: 404,
|
||||
config: {},
|
||||
data: {},
|
||||
headers: {},
|
||||
statusText: 'not found',
|
||||
},
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
try {
|
||||
await client.proxy.a.get()
|
||||
} catch (er: any) {
|
||||
expect(er).toBeInstanceOf(RequestError)
|
||||
expect(er.status).toBe(404)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw custom exception', async () => {
|
||||
class MyRequestError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public status: number,
|
||||
public path: string,
|
||||
public raw: any,
|
||||
) {
|
||||
super(message)
|
||||
}
|
||||
|
||||
toResponse() {
|
||||
return {
|
||||
status: this.status,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const client = generateClient<AxiosResponse>(axiosAdaptor, {
|
||||
// @ts-ignore
|
||||
customThrowResponseError(err) {
|
||||
return new MyRequestError(
|
||||
err.message,
|
||||
err.response?.status,
|
||||
err.path,
|
||||
err.raw,
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
spyOn(axiosAdaptor, 'get').mockImplementation(() => {
|
||||
return Promise.reject(
|
||||
new AxiosError(
|
||||
'not found',
|
||||
'NOT_FOUND',
|
||||
{},
|
||||
{},
|
||||
{
|
||||
status: 404,
|
||||
config: {},
|
||||
data: {},
|
||||
headers: {},
|
||||
statusText: 'not found',
|
||||
},
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
try {
|
||||
await client.proxy.a.get()
|
||||
} catch (er: any) {
|
||||
expect(er).toBeInstanceOf(MyRequestError)
|
||||
expect(er.toResponse).toBeDefined()
|
||||
expect(er.status).toBe(404)
|
||||
}
|
||||
})
|
||||
})
|
||||
108
packages/api-client/__tests__/helpers/adaptor-test.ts
Normal file
108
packages/api-client/__tests__/helpers/adaptor-test.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { allControllers } from '~/controllers'
|
||||
import { HTTPClient, RequestError, createClient } from '~/core'
|
||||
import { IRequestAdapter } from '~/interfaces/adapter'
|
||||
|
||||
import { createMockServer } from './e2e-mock-server'
|
||||
|
||||
export const testAdaptor = (adaptor: IRequestAdapter) => {
|
||||
let client: HTTPClient
|
||||
const { app, close, port } = createMockServer()
|
||||
|
||||
afterAll(() => {
|
||||
close()
|
||||
})
|
||||
|
||||
beforeAll(() => {
|
||||
client = createClient(adaptor)(`http://localhost:${port}`)
|
||||
client.injectControllers(allControllers)
|
||||
})
|
||||
test('get', async () => {
|
||||
app.get('/posts/1', (req, res) => {
|
||||
res.send({
|
||||
id: '1',
|
||||
category_id: '11',
|
||||
})
|
||||
})
|
||||
const res = await client.post.getPost('1')
|
||||
|
||||
expect(res).toStrictEqual({
|
||||
id: '1',
|
||||
categoryId: '11',
|
||||
})
|
||||
|
||||
expect(res.$raw.data.category_id).toEqual('11')
|
||||
})
|
||||
|
||||
test('post', async () => {
|
||||
app.post('/comments/1', (req, res) => {
|
||||
const { body } = req
|
||||
|
||||
res.send({
|
||||
...body,
|
||||
})
|
||||
})
|
||||
const dto = {
|
||||
text: 'hello',
|
||||
author: 'test',
|
||||
mail: '1@ee.com',
|
||||
}
|
||||
const res = await client.comment.comment('1', dto)
|
||||
|
||||
expect(res).toStrictEqual(dto)
|
||||
})
|
||||
|
||||
test('get with search query', async () => {
|
||||
app.get('/search/post', (req, res) => {
|
||||
if (req.query.keyword) {
|
||||
return res.send({ result: 1 })
|
||||
}
|
||||
res.send(null)
|
||||
})
|
||||
|
||||
const res = await client.search.search('post', 'keyword')
|
||||
expect(res).toStrictEqual({ result: 1 })
|
||||
})
|
||||
|
||||
test('rawResponse rawRequest should defined', async () => {
|
||||
app.get('/search/post', (req, res) => {
|
||||
if (req.query.keyword) {
|
||||
return res.send({ result: 1 })
|
||||
}
|
||||
res.send(null)
|
||||
})
|
||||
|
||||
const res = await client.search.search('post', 'keyword')
|
||||
expect(res.$raw).toBeDefined()
|
||||
expect(res.$raw.data).toBeDefined()
|
||||
})
|
||||
|
||||
it('should error catch', async () => {
|
||||
app.get('/error', (req, res) => {
|
||||
res.status(500).send({
|
||||
message: 'error message',
|
||||
})
|
||||
})
|
||||
await expect(client.proxy.error.get()).rejects.toThrowError(RequestError)
|
||||
})
|
||||
|
||||
it('should use number as path', async () => {
|
||||
app.get('/1/1', (req, res) => {
|
||||
res.send({ data: 1, foo_bar: 'foo' })
|
||||
})
|
||||
|
||||
const res = await client.proxy(1)(1).get<{ data: number; fooBar: string }>()
|
||||
|
||||
expect(res).toStrictEqual({ data: 1, fooBar: 'foo' })
|
||||
expect(res.$raw.data).toStrictEqual({ data: 1, foo_bar: 'foo' })
|
||||
expect(res.$request).toBeDefined()
|
||||
})
|
||||
|
||||
it('should get string payload', async () => {
|
||||
app.get('/string', (req, res) => {
|
||||
res.send('x')
|
||||
})
|
||||
|
||||
const res = await client.proxy('string').get<string>()
|
||||
expect(res).toStrictEqual('x')
|
||||
})
|
||||
}
|
||||
22
packages/api-client/__tests__/helpers/e2e-mock-server.ts
Normal file
22
packages/api-client/__tests__/helpers/e2e-mock-server.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import cors from 'cors'
|
||||
import express from 'express'
|
||||
import { AddressInfo } from 'net'
|
||||
|
||||
type Express = ReturnType<typeof express>
|
||||
export const createMockServer = (options: { port?: number } = {}) => {
|
||||
const { port = 0 } = options
|
||||
|
||||
const app: Express = express()
|
||||
app.use(express.json())
|
||||
app.use(cors())
|
||||
const server = app.listen(port)
|
||||
|
||||
return {
|
||||
app,
|
||||
port: (server.address() as AddressInfo).port,
|
||||
server,
|
||||
close() {
|
||||
server.close()
|
||||
},
|
||||
}
|
||||
}
|
||||
39
packages/api-client/__tests__/helpers/global-fetch.ts
Normal file
39
packages/api-client/__tests__/helpers/global-fetch.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
// @ts-nocheck
|
||||
|
||||
/* eslint-disable */
|
||||
import AbortController from 'abort-controller'
|
||||
import fetch, { Headers, Request, Response } from 'node-fetch'
|
||||
|
||||
const TEN_MEGABYTES = 1000 * 1000 * 10
|
||||
|
||||
if (!globalThis.fetch) {
|
||||
globalThis.fetch = (url, options) =>
|
||||
fetch(url, { highWaterMark: TEN_MEGABYTES, ...options })
|
||||
}
|
||||
|
||||
if (!globalThis.Headers) {
|
||||
globalThis.Headers = Headers
|
||||
}
|
||||
|
||||
if (!globalThis.Request) {
|
||||
globalThis.Request = Request
|
||||
}
|
||||
|
||||
if (!globalThis.Response) {
|
||||
globalThis.Response = Response
|
||||
}
|
||||
|
||||
if (!globalThis.AbortController) {
|
||||
globalThis.AbortController = AbortController
|
||||
}
|
||||
|
||||
if (!globalThis.ReadableStream) {
|
||||
try {
|
||||
// eslint-disable-next-line node/file-extension-in-import, node/no-unsupported-features/es-syntax
|
||||
globalThis.ReadableStream = await import(
|
||||
'web-streams-polyfill/ponyfill/es2018'
|
||||
)
|
||||
} catch {}
|
||||
}
|
||||
|
||||
export {}
|
||||
11
packages/api-client/__tests__/helpers/instance.ts
Normal file
11
packages/api-client/__tests__/helpers/instance.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { axiosAdaptor } from '~/adaptors/axios'
|
||||
import { HTTPClient, createClient } from '~/core'
|
||||
import { IController } from '~/interfaces/controller'
|
||||
|
||||
export const mockRequestInstance = (
|
||||
injectController: new (client: HTTPClient) => IController,
|
||||
) => {
|
||||
const client = createClient(axiosAdaptor)('https://api.innei.ren/v2')
|
||||
client.injectControllers(injectController)
|
||||
return client
|
||||
}
|
||||
76
packages/api-client/__tests__/helpers/response.ts
Normal file
76
packages/api-client/__tests__/helpers/response.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { isEqual } from 'lodash'
|
||||
import { URLSearchParams } from 'url'
|
||||
import { vi } from 'vitest'
|
||||
|
||||
import { axiosAdaptor } from '~/adaptors/axios'
|
||||
|
||||
const { spyOn } = vi
|
||||
|
||||
export const buildResponseDataWrapper = (data: any) => ({ data })
|
||||
|
||||
export const mockResponse = <T>(
|
||||
path: string,
|
||||
data: T,
|
||||
method = 'get',
|
||||
requestBody?: any,
|
||||
) => {
|
||||
const exceptUrlObject = new URL(
|
||||
path.startsWith('http')
|
||||
? path
|
||||
: `https://example.com/${path.replace(/^\//, '')}`,
|
||||
)
|
||||
// @ts-ignore
|
||||
spyOn(axiosAdaptor, method).mockImplementation(
|
||||
// @ts-ignore
|
||||
async (requestUrl: string, options: any) => {
|
||||
const requestUrlObject = new URL(requestUrl)
|
||||
|
||||
if (requestBody) {
|
||||
const { data } = options || {}
|
||||
if (!isEqual(requestBody, data)) {
|
||||
throw new Error(
|
||||
`body not equal, got: ${JSON.stringify(
|
||||
data,
|
||||
)} except: ${JSON.stringify(requestBody)}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
requestUrlObject.pathname.endsWith(exceptUrlObject.pathname) &&
|
||||
(exceptUrlObject.search
|
||||
? isSearchEqual(
|
||||
exceptUrlObject.searchParams,
|
||||
requestUrlObject.searchParams,
|
||||
)
|
||||
: true)
|
||||
) {
|
||||
return buildResponseDataWrapper(data)
|
||||
} else {
|
||||
return buildResponseDataWrapper({
|
||||
error: 1,
|
||||
requestPath: requestUrlObject.pathname + requestUrlObject.search,
|
||||
expectPath: path,
|
||||
})
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
const isSearchEqual = (a: URLSearchParams, b: URLSearchParams) => {
|
||||
const keys = Array.from(a.keys()).sort()
|
||||
if (keys.toString() !== Array.from(b.keys()).sort().toString()) {
|
||||
return false
|
||||
}
|
||||
return keys.every((key) => {
|
||||
const res = a.get(key) === b.get(key)
|
||||
if (!res) {
|
||||
console.log(
|
||||
`key ${key} not equal, receive ${a.get(key)} want ${b.get(key)}`,
|
||||
)
|
||||
}
|
||||
return res
|
||||
})
|
||||
}
|
||||
446
packages/api-client/__tests__/mock/algolia.json
Normal file
446
packages/api-client/__tests__/mock/algolia.json
Normal file
File diff suppressed because one or more lines are too long
27
packages/api-client/__tests__/utils/auto-bind.spec.ts
Normal file
27
packages/api-client/__tests__/utils/auto-bind.spec.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { autoBind } from '~/utils/auto-bind'
|
||||
|
||||
describe('test auto bind', () => {
|
||||
it('should bind in class', () => {
|
||||
class A {
|
||||
constructor() {
|
||||
autoBind(this)
|
||||
}
|
||||
name = 'A'
|
||||
foo() {
|
||||
return this?.name
|
||||
}
|
||||
}
|
||||
|
||||
expect(new A().foo()).toBe('A')
|
||||
|
||||
function tester(caller: any) {
|
||||
return caller()
|
||||
}
|
||||
expect(tester(new A().foo)).toBe('A')
|
||||
|
||||
function tester2<T extends (...args: any) => any>(caller: T) {
|
||||
return caller.call({})
|
||||
}
|
||||
expect(tester2(new A().foo)).toBe('A')
|
||||
})
|
||||
})
|
||||
86
packages/api-client/__tests__/utils/camelcase-keys.spec.ts
Normal file
86
packages/api-client/__tests__/utils/camelcase-keys.spec.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import camelcaseKeysLib from 'camelcase-keys'
|
||||
|
||||
import { camelcaseKeys } from '~/utils/camelcase-keys'
|
||||
|
||||
describe('test camelcase keys', () => {
|
||||
it('case 1 normal', () => {
|
||||
const obj = {
|
||||
tool: 'too',
|
||||
tool_name: 'too_name',
|
||||
|
||||
a_b: 1,
|
||||
a: 1,
|
||||
b: {
|
||||
c_d: 1,
|
||||
},
|
||||
}
|
||||
|
||||
expect(camelcaseKeys(obj)).toStrictEqual(
|
||||
camelcaseKeysLib(obj, {
|
||||
deep: true,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('case 2: key has number', () => {
|
||||
const obj = {
|
||||
b147da0eaecbea00aeb62055: {
|
||||
data: {},
|
||||
},
|
||||
a_c11ab_Ac: [
|
||||
{
|
||||
a_b: 1,
|
||||
},
|
||||
1,
|
||||
],
|
||||
}
|
||||
|
||||
expect(camelcaseKeys(obj)).toStrictEqual({
|
||||
b147da0eaecbea00aeb62055: {
|
||||
data: {},
|
||||
},
|
||||
aC11abAc: [
|
||||
{
|
||||
aB: 1,
|
||||
},
|
||||
1,
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('case 3: not a object', () => {
|
||||
const value = 1
|
||||
expect(camelcaseKeys(value)).toBe(value)
|
||||
})
|
||||
|
||||
it('case 4: nullable value', () => {
|
||||
let value = null
|
||||
expect(camelcaseKeys(value)).toBe(value)
|
||||
|
||||
value = undefined
|
||||
expect(camelcaseKeys(value)).toBe(value)
|
||||
|
||||
value = NaN
|
||||
expect(camelcaseKeys(value)).toBe(value)
|
||||
})
|
||||
|
||||
it('case 5: array', () => {
|
||||
const arr = [
|
||||
{
|
||||
a_b: 1,
|
||||
},
|
||||
null,
|
||||
undefined,
|
||||
+0,
|
||||
-0,
|
||||
Infinity,
|
||||
{
|
||||
a_b: 1,
|
||||
},
|
||||
]
|
||||
|
||||
expect(camelcaseKeys(arr)).toStrictEqual(
|
||||
camelcaseKeysLib(arr, { deep: true }),
|
||||
)
|
||||
})
|
||||
})
|
||||
33
packages/api-client/__tests__/utils/index.test.ts
Normal file
33
packages/api-client/__tests__/utils/index.test.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { destructureData } from '~/utils'
|
||||
|
||||
describe('test utils', () => {
|
||||
test('destructureData', () => {
|
||||
const d = destructureData({ data: { a: 1, b: 2 } })
|
||||
expect(d).toEqual({ a: 1, b: 2 })
|
||||
|
||||
const d2 = destructureData({ data: { a: 1, b: 2 }, c: 3 })
|
||||
expect(d2).toEqual({ data: { a: 1, b: 2 }, c: 3 })
|
||||
|
||||
const d3 = destructureData({ data: [{ a: 1 }] })
|
||||
expect(d3).toEqual({ data: [{ a: 1 }] })
|
||||
|
||||
const d4 = destructureData({ a: 1 })
|
||||
expect(d4).toEqual({ a: 1 })
|
||||
|
||||
const d5 = destructureData([])
|
||||
expect(d5).toEqual([])
|
||||
|
||||
const d6 = destructureData({ data: [] })
|
||||
expect(d6).toEqual({ data: [] })
|
||||
|
||||
const d7 = destructureData(
|
||||
(() => {
|
||||
const d = { data: { a: 1 } }
|
||||
Object.defineProperty(d, '$raw', { value: { a: 1 }, enumerable: false })
|
||||
return d
|
||||
})(),
|
||||
)
|
||||
expect(d7).toEqual({ a: 1 })
|
||||
expect(d7.$raw).toBeTruthy()
|
||||
})
|
||||
})
|
||||
8
packages/api-client/__tests__/utils/path.spec.ts
Normal file
8
packages/api-client/__tests__/utils/path.spec.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { resolveFullPath } from '~/utils/path'
|
||||
|
||||
describe('TEST path.utils', () => {
|
||||
it('should resolve full path', () => {
|
||||
const path = resolveFullPath('http://localhost:3000', '/api/v1/users')
|
||||
expect(path).toBe('http://localhost:3000/api/v1/users')
|
||||
})
|
||||
})
|
||||
44
packages/api-client/adaptors/axios.ts
Normal file
44
packages/api-client/adaptors/axios.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import axios, { AxiosInstance, AxiosResponse } from 'axios'
|
||||
|
||||
import { IRequestAdapter } from '~/interfaces/adapter'
|
||||
|
||||
// eslint-disable-next-line spaced-comment
|
||||
const $http = /*#__PURE__*/ axios.create({})
|
||||
|
||||
// ignore axios `method` declare not assignable to `Method`
|
||||
export const axiosAdaptor: IRequestAdapter<
|
||||
AxiosInstance,
|
||||
AxiosResponse<unknown>
|
||||
> = Object.preventExtensions({
|
||||
get default() {
|
||||
return $http
|
||||
},
|
||||
responseWrapper: {} as any as AxiosResponse<unknown>,
|
||||
get(url, options) {
|
||||
// @ts-ignore
|
||||
return $http.get(url, options)
|
||||
},
|
||||
post(url, options) {
|
||||
const { data, ...config } = options || {}
|
||||
// @ts-ignore
|
||||
return $http.post(url, data, config)
|
||||
},
|
||||
put(url, options) {
|
||||
const { data, ...config } = options || {}
|
||||
// @ts-ignore
|
||||
return $http.put(url, data, config)
|
||||
},
|
||||
delete(url, options) {
|
||||
const { ...config } = options || {}
|
||||
// @ts-ignore
|
||||
return $http.delete(url, config)
|
||||
},
|
||||
patch(url, options) {
|
||||
const { data, ...config } = options || {}
|
||||
// @ts-ignore
|
||||
return $http.patch(url, data, config)
|
||||
},
|
||||
})
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default axiosAdaptor
|
||||
76
packages/api-client/adaptors/ky.ts
Normal file
76
packages/api-client/adaptors/ky.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import ky, { Options, ResponsePromise } from 'ky'
|
||||
import { KyInstance } from 'ky/distribution/types/ky'
|
||||
|
||||
import { IRequestAdapter } from '~/interfaces/adapter'
|
||||
|
||||
// eslint-disable-next-line spaced-comment
|
||||
const $http: KyInstance = /*#__PURE__*/ ky.create({})
|
||||
// TODO post data not only json,
|
||||
const getDataFromKyResponse = async (response: ResponsePromise) => {
|
||||
const res = await response
|
||||
|
||||
const isJsonType = res.headers
|
||||
.get('content-type')
|
||||
?.includes('application/json')
|
||||
const json = isJsonType ? await res.clone().json() : null
|
||||
|
||||
const result: Awaited<ResponsePromise> & {
|
||||
data: any
|
||||
} = {
|
||||
...res,
|
||||
data: json ?? (await res.clone().text()),
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export const createKyAdaptor = (ky: KyInstance) => {
|
||||
const adaptor: IRequestAdapter<KyInstance, ResponsePromise> =
|
||||
Object.preventExtensions({
|
||||
get default() {
|
||||
return ky
|
||||
},
|
||||
|
||||
responseWrapper: {} as any as ResponsePromise,
|
||||
get(url, options) {
|
||||
return getDataFromKyResponse(ky.get(url, options))
|
||||
},
|
||||
post(url, options) {
|
||||
const data = options.data
|
||||
delete options.data
|
||||
const kyOptions: Options = {
|
||||
...options,
|
||||
json: data,
|
||||
}
|
||||
|
||||
return getDataFromKyResponse(ky.post(url, kyOptions))
|
||||
},
|
||||
put(url, options) {
|
||||
const data = options.data
|
||||
delete options.data
|
||||
const kyOptions: Options = {
|
||||
...options,
|
||||
json: data,
|
||||
}
|
||||
return getDataFromKyResponse(ky.put(url, kyOptions))
|
||||
},
|
||||
|
||||
patch(url, options) {
|
||||
const data = options.data
|
||||
delete options.data
|
||||
const kyOptions: Options = {
|
||||
...options,
|
||||
json: data,
|
||||
}
|
||||
return getDataFromKyResponse(ky.patch(url, kyOptions))
|
||||
},
|
||||
delete(url, options) {
|
||||
return getDataFromKyResponse(ky.delete(url, options))
|
||||
},
|
||||
})
|
||||
return adaptor
|
||||
}
|
||||
|
||||
export const defaultKyAdaptor = createKyAdaptor($http)
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default defaultKyAdaptor
|
||||
38
packages/api-client/adaptors/umi-request.ts
Normal file
38
packages/api-client/adaptors/umi-request.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { RequestMethod, RequestResponse, extend } from 'umi-request'
|
||||
|
||||
import { IRequestAdapter } from '~/interfaces/adapter'
|
||||
|
||||
// eslint-disable-next-line spaced-comment
|
||||
const $http = /*#__PURE__*/ extend({
|
||||
getResponse: true,
|
||||
requestType: 'json',
|
||||
responseType: 'json',
|
||||
})
|
||||
|
||||
export const umiAdaptor: IRequestAdapter<
|
||||
RequestMethod<true>,
|
||||
RequestResponse
|
||||
> = Object.preventExtensions({
|
||||
get default() {
|
||||
return $http
|
||||
},
|
||||
responseWrapper: {} as any as RequestResponse,
|
||||
get(url, options) {
|
||||
return $http.get(url, options)
|
||||
},
|
||||
post(url, options) {
|
||||
return $http.post(url, options)
|
||||
},
|
||||
put(url, options) {
|
||||
return $http.put(url, options)
|
||||
},
|
||||
delete(url, options) {
|
||||
return $http.delete(url, options)
|
||||
},
|
||||
patch(url, options) {
|
||||
return $http.patch(url, options)
|
||||
},
|
||||
})
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default umiAdaptor
|
||||
70
packages/api-client/controllers/aggregate.ts
Normal file
70
packages/api-client/controllers/aggregate.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { IRequestAdapter } from '~/interfaces/adapter'
|
||||
import { IController } from '~/interfaces/controller'
|
||||
import { SortOrder } from '~/interfaces/options'
|
||||
import { IRequestHandler, RequestProxyResult } from '~/interfaces/request'
|
||||
import {
|
||||
AggregateRoot,
|
||||
AggregateStat,
|
||||
AggregateTop,
|
||||
TimelineData,
|
||||
TimelineType,
|
||||
} from '~/models/aggregate'
|
||||
import { sortOrderToNumber } from '~/utils'
|
||||
import { autoBind } from '~/utils/auto-bind'
|
||||
|
||||
import { HTTPClient } from '../core'
|
||||
|
||||
declare module '../core/client' {
|
||||
interface HTTPClient<
|
||||
T extends IRequestAdapter = IRequestAdapter,
|
||||
ResponseWrapper = unknown,
|
||||
> {
|
||||
aggregate: AggregateController<ResponseWrapper>
|
||||
}
|
||||
}
|
||||
|
||||
export class AggregateController<ResponseWrapper> implements IController {
|
||||
base = 'aggregate'
|
||||
name = 'aggregate'
|
||||
constructor(private client: HTTPClient) {
|
||||
autoBind(this)
|
||||
}
|
||||
public get proxy(): IRequestHandler<ResponseWrapper> {
|
||||
return this.client.proxy(this.base)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取聚合数据
|
||||
*/
|
||||
getAggregateData(): RequestProxyResult<AggregateRoot, ResponseWrapper> {
|
||||
return this.proxy.get<AggregateRoot>()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最新发布的内容
|
||||
*/
|
||||
getTop(size = 5) {
|
||||
return this.proxy.top.get<AggregateTop>({ params: { size } })
|
||||
}
|
||||
|
||||
getTimeline(options?: {
|
||||
sort?: SortOrder
|
||||
type?: TimelineType
|
||||
year?: number
|
||||
}) {
|
||||
const { sort, type, year } = options || {}
|
||||
return this.proxy.timeline.get<{ data: TimelineData }>({
|
||||
params: {
|
||||
sort: sort && sortOrderToNumber(sort),
|
||||
type,
|
||||
year,
|
||||
},
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 获取聚合数据统计
|
||||
*/
|
||||
getStat() {
|
||||
return this.proxy.stat.get<AggregateStat>()
|
||||
}
|
||||
}
|
||||
37
packages/api-client/controllers/base.ts
Normal file
37
packages/api-client/controllers/base.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { IRequestHandler, RequestProxyResult } from '~/interfaces/request'
|
||||
import { PaginateResult } from '~/models/base'
|
||||
import { autoBind } from '~/utils/auto-bind'
|
||||
|
||||
import { HTTPClient } from '../core'
|
||||
|
||||
export type SortOptions = {
|
||||
sortBy?: string
|
||||
sortOrder?: 1 | -1 | 'asc' | 'desc'
|
||||
}
|
||||
export abstract class BaseCrudController<T, ResponseWrapper> {
|
||||
base!: string
|
||||
constructor(protected client: HTTPClient) {
|
||||
autoBind(this)
|
||||
}
|
||||
public get proxy(): IRequestHandler<ResponseWrapper> {
|
||||
return this.client.proxy(this.base)
|
||||
}
|
||||
|
||||
getById(id: string): RequestProxyResult<T, ResponseWrapper> {
|
||||
return this.proxy(id).get<T>()
|
||||
}
|
||||
|
||||
getAll() {
|
||||
return this.proxy.all.get<{ data: T[] }>()
|
||||
}
|
||||
/**
|
||||
* 带分页的查询
|
||||
* @param page
|
||||
* @param perPage
|
||||
*/
|
||||
getAllPaginated(page?: number, perPage?: number, sortOption?: SortOptions) {
|
||||
return this.proxy.get<PaginateResult<T>>({
|
||||
params: { page, size: perPage, ...sortOption },
|
||||
})
|
||||
}
|
||||
}
|
||||
119
packages/api-client/controllers/category.ts
Normal file
119
packages/api-client/controllers/category.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { IRequestAdapter } from '~/interfaces/adapter'
|
||||
import { IController } from '~/interfaces/controller'
|
||||
import {
|
||||
IRequestHandler,
|
||||
RequestProxyResult,
|
||||
ResponseProxyExtraRaw,
|
||||
} from '~/interfaces/request'
|
||||
import { attachRawFromOneToAnthor, destructureData } from '~/utils'
|
||||
import { autoBind } from '~/utils/auto-bind'
|
||||
|
||||
import { HTTPClient } from '../core/client'
|
||||
import { RequestError } from '../core/error'
|
||||
import {
|
||||
CategoryEntries,
|
||||
CategoryModel,
|
||||
CategoryType,
|
||||
CategoryWithChildrenModel,
|
||||
TagModel,
|
||||
} from '../models/category'
|
||||
import { PostModel } from '../models/post'
|
||||
|
||||
declare module '../core/client' {
|
||||
interface HTTPClient<
|
||||
T extends IRequestAdapter = IRequestAdapter,
|
||||
ResponseWrapper = unknown,
|
||||
> {
|
||||
category: CategoryController<ResponseWrapper>
|
||||
}
|
||||
}
|
||||
|
||||
export class CategoryController<ResponseWrapper> implements IController {
|
||||
name = 'category'
|
||||
base = 'categories'
|
||||
constructor(private client: HTTPClient) {
|
||||
autoBind(this)
|
||||
}
|
||||
|
||||
public get proxy(): IRequestHandler<ResponseWrapper> {
|
||||
return this.client.proxy(this.base)
|
||||
}
|
||||
|
||||
getAllCategories(): RequestProxyResult<
|
||||
{ data: CategoryModel[] },
|
||||
ResponseWrapper
|
||||
> {
|
||||
return this.proxy.get({
|
||||
params: {
|
||||
type: CategoryType.Category,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
getAllTags(): RequestProxyResult<{ data: TagModel[] }, ResponseWrapper> {
|
||||
return this.proxy.get<{ data: TagModel[] }>({
|
||||
params: {
|
||||
type: CategoryType.Tag,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async getCategoryDetail(
|
||||
id: string,
|
||||
): Promise<ResponseProxyExtraRaw<CategoryWithChildrenModel>>
|
||||
async getCategoryDetail(
|
||||
ids: string[],
|
||||
): Promise<ResponseProxyExtraRaw<Map<string, CategoryWithChildrenModel>>>
|
||||
async getCategoryDetail(ids: string | string[]): Promise<any> {
|
||||
if (typeof ids === 'string') {
|
||||
const data = await this.proxy.get<CategoryEntries>({
|
||||
params: {
|
||||
ids,
|
||||
},
|
||||
})
|
||||
const result = Object.values(data.entries)[0]
|
||||
attachRawFromOneToAnthor(data, result)
|
||||
return result
|
||||
} else if (Array.isArray(ids)) {
|
||||
const data = await this.proxy.get<CategoryEntries>({
|
||||
params: {
|
||||
ids: ids.join(','),
|
||||
},
|
||||
})
|
||||
const entries = data?.entries
|
||||
if (!entries) {
|
||||
throw new RequestError(
|
||||
'data structure error',
|
||||
500,
|
||||
data.$request.path,
|
||||
data,
|
||||
)
|
||||
}
|
||||
|
||||
const map = new Map<string, CategoryWithChildrenModel>(
|
||||
Object.entries(entries).map(([id, value]) => [id.toLowerCase(), value]),
|
||||
)
|
||||
|
||||
attachRawFromOneToAnthor(data, map)
|
||||
return map
|
||||
}
|
||||
}
|
||||
|
||||
async getCategoryByIdOrSlug(idOrSlug: string) {
|
||||
const res = await this.proxy(idOrSlug).get<CategoryWithChildrenModel>()
|
||||
return destructureData(res) as typeof res
|
||||
}
|
||||
|
||||
async getTagByName(name: string) {
|
||||
const res = await this.proxy(name).get<{
|
||||
tag: string
|
||||
data: Pick<PostModel, 'id' | 'title' | 'slug' | 'category' | 'created'>[]
|
||||
}>({
|
||||
params: {
|
||||
tag: 1,
|
||||
},
|
||||
})
|
||||
|
||||
return res
|
||||
}
|
||||
}
|
||||
69
packages/api-client/controllers/comment.ts
Normal file
69
packages/api-client/controllers/comment.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { IRequestAdapter } from '~/interfaces/adapter'
|
||||
import { IController } from '~/interfaces/controller'
|
||||
import { PaginationParams } from '~/interfaces/params'
|
||||
import { IRequestHandler } from '~/interfaces/request'
|
||||
import { PaginateResult } from '~/models/base'
|
||||
import { CommentModel } from '~/models/comment'
|
||||
import { autoBind } from '~/utils/auto-bind'
|
||||
|
||||
import { HTTPClient } from '../core'
|
||||
import { CommentDto } from '../dtos/comment'
|
||||
|
||||
declare module '../core/client' {
|
||||
interface HTTPClient<
|
||||
T extends IRequestAdapter = IRequestAdapter,
|
||||
ResponseWrapper = unknown,
|
||||
> {
|
||||
comment: CommentController<ResponseWrapper>
|
||||
}
|
||||
}
|
||||
|
||||
export class CommentController<ResponseWrapper> implements IController {
|
||||
base = 'comments'
|
||||
name = 'comment'
|
||||
|
||||
constructor(private readonly client: HTTPClient) {
|
||||
autoBind(this)
|
||||
}
|
||||
|
||||
get proxy(): IRequestHandler<ResponseWrapper> {
|
||||
return this.client.proxy(this.base)
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 comment id 获取评论, 包括子评论
|
||||
*/
|
||||
getById(id: string) {
|
||||
return this.proxy(id).get<CommentModel & { ref: string }>()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文章的评论列表
|
||||
* @param refId 文章 Id
|
||||
*/
|
||||
getByRefId(refId: string, pagination: PaginationParams = {}) {
|
||||
const { page, size } = pagination
|
||||
return this.proxy
|
||||
.ref(refId)
|
||||
.get<PaginateResult<CommentModel & { ref: string }>>({
|
||||
params: { page: page || 1, size: size || 10 },
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 评论
|
||||
*/
|
||||
comment(refId: string, data: CommentDto) {
|
||||
return this.proxy(refId).post<CommentModel>({
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 回复评论
|
||||
*/
|
||||
reply(commentId: string, data: CommentDto) {
|
||||
return this.proxy.reply(commentId).post<CommentModel>({
|
||||
data,
|
||||
})
|
||||
}
|
||||
}
|
||||
74
packages/api-client/controllers/index.ts
Normal file
74
packages/api-client/controllers/index.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { AggregateController } from './aggregate'
|
||||
import { CategoryController } from './category'
|
||||
import { CommentController } from './comment'
|
||||
import { LinkController } from './link'
|
||||
import { NoteController } from './note'
|
||||
import { PageController } from './page'
|
||||
import { PostController } from './post'
|
||||
import { ProjectController } from './project'
|
||||
import { RecentlyController } from './recently'
|
||||
import { SayController } from './say'
|
||||
import { SearchController } from './search'
|
||||
import { ServerlessController } from './severless'
|
||||
import { SnippetController } from './snippet'
|
||||
import { TopicController } from './topic'
|
||||
import { UserController } from './user'
|
||||
|
||||
export const allControllers = [
|
||||
AggregateController,
|
||||
CategoryController,
|
||||
CommentController,
|
||||
LinkController,
|
||||
NoteController,
|
||||
PageController,
|
||||
PostController,
|
||||
ProjectController,
|
||||
RecentlyController,
|
||||
TopicController,
|
||||
SayController,
|
||||
SearchController,
|
||||
SnippetController,
|
||||
ServerlessController,
|
||||
UserController,
|
||||
]
|
||||
|
||||
export const allContollerNames = [
|
||||
'aggregate',
|
||||
'category',
|
||||
'comment',
|
||||
'link',
|
||||
'note',
|
||||
'page',
|
||||
'post',
|
||||
'project',
|
||||
'topic',
|
||||
'recently',
|
||||
'say',
|
||||
'search',
|
||||
'snippet',
|
||||
'serverless',
|
||||
'user',
|
||||
|
||||
// alias,
|
||||
'friend',
|
||||
'master',
|
||||
'shorthand',
|
||||
] as const
|
||||
|
||||
export {
|
||||
AggregateController,
|
||||
CategoryController,
|
||||
CommentController,
|
||||
LinkController,
|
||||
NoteController,
|
||||
PageController,
|
||||
PostController,
|
||||
ProjectController,
|
||||
RecentlyController,
|
||||
SayController,
|
||||
SearchController,
|
||||
SnippetController,
|
||||
ServerlessController,
|
||||
UserController,
|
||||
TopicController,
|
||||
}
|
||||
47
packages/api-client/controllers/link.ts
Normal file
47
packages/api-client/controllers/link.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { IRequestAdapter } from '~/interfaces/adapter'
|
||||
import { LinkModel } from '~/models/link'
|
||||
import { autoBind } from '~/utils/auto-bind'
|
||||
|
||||
import { HTTPClient } from '../core'
|
||||
import { BaseCrudController } from './base'
|
||||
|
||||
declare module '../core/client' {
|
||||
interface HTTPClient<
|
||||
T extends IRequestAdapter = IRequestAdapter,
|
||||
ResponseWrapper = unknown,
|
||||
> {
|
||||
link: LinkController<ResponseWrapper>
|
||||
friend: LinkController<ResponseWrapper>
|
||||
}
|
||||
}
|
||||
|
||||
export class LinkController<ResponseWrapper> extends BaseCrudController<
|
||||
LinkModel,
|
||||
ResponseWrapper
|
||||
> {
|
||||
constructor(protected readonly client: HTTPClient) {
|
||||
super(client)
|
||||
autoBind(this)
|
||||
}
|
||||
|
||||
// 是否可以申请友链
|
||||
async canApplyLink() {
|
||||
const { can } = await this.proxy.audit.get<{ can: boolean }>()
|
||||
return can
|
||||
}
|
||||
|
||||
// 申请友链
|
||||
async applyLink(
|
||||
data: Pick<
|
||||
LinkModel,
|
||||
'avatar' | 'name' | 'description' | 'url' | 'email'
|
||||
> & {
|
||||
author: string
|
||||
},
|
||||
) {
|
||||
return await this.proxy.audit.post<never>({ data })
|
||||
}
|
||||
|
||||
name = ['link', 'friend']
|
||||
base = 'links'
|
||||
}
|
||||
129
packages/api-client/controllers/note.ts
Normal file
129
packages/api-client/controllers/note.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { IRequestAdapter } from '~/interfaces/adapter'
|
||||
import { IController } from '~/interfaces/controller'
|
||||
import { IRequestHandler, RequestProxyResult } from '~/interfaces/request'
|
||||
import { SelectFields } from '~/interfaces/types'
|
||||
import { PaginateResult } from '~/models/base'
|
||||
import { NoteModel, NoteWrappedPayload } from '~/models/note'
|
||||
import { autoBind } from '~/utils/auto-bind'
|
||||
|
||||
import { HTTPClient } from '../core/client'
|
||||
import { SortOptions } from './base'
|
||||
|
||||
declare module '../core/client' {
|
||||
interface HTTPClient<
|
||||
T extends IRequestAdapter = IRequestAdapter,
|
||||
ResponseWrapper = unknown,
|
||||
> {
|
||||
note: NoteController<ResponseWrapper>
|
||||
}
|
||||
}
|
||||
|
||||
export type NoteListOptions = {
|
||||
select?: SelectFields<keyof NoteModel>
|
||||
year?: number
|
||||
sortBy?: 'weather' | 'mood' | 'title' | 'created' | 'modified'
|
||||
sortOrder?: 1 | -1
|
||||
}
|
||||
|
||||
export class NoteController<ResponseWrapper> implements IController {
|
||||
base = 'notes'
|
||||
name = 'note'
|
||||
|
||||
constructor(private client: HTTPClient) {
|
||||
autoBind(this)
|
||||
}
|
||||
get proxy(): IRequestHandler<ResponseWrapper> {
|
||||
return this.client.proxy(this.base)
|
||||
}
|
||||
|
||||
/**
|
||||
* 最新日记
|
||||
*/
|
||||
getLatest() {
|
||||
return this.proxy.latest.get<NoteWrappedPayload>()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取一篇日记, 根据 Id 查询需要鉴权
|
||||
* @param id id | nid
|
||||
* @param password 访问密码
|
||||
*/
|
||||
|
||||
getNoteById(
|
||||
id: string,
|
||||
): Promise<RequestProxyResult<NoteModel, ResponseWrapper>>
|
||||
getNoteById(id: number): Promise<NoteWrappedPayload>
|
||||
getNoteById(id: number, password: string): Promise<NoteWrappedPayload>
|
||||
getNoteById(
|
||||
id: number,
|
||||
password: undefined,
|
||||
singleResult: true,
|
||||
): Promise<RequestProxyResult<NoteModel, ResponseWrapper>>
|
||||
getNoteById(
|
||||
id: number,
|
||||
password: string,
|
||||
singleResult: true,
|
||||
): Promise<RequestProxyResult<NoteModel, ResponseWrapper>>
|
||||
getNoteById(...rest: any[]): any {
|
||||
const [id, password = undefined, singleResult = false] = rest
|
||||
|
||||
if (typeof id === 'number') {
|
||||
return this.proxy.nid(id.toString()).get<NoteWrappedPayload>({
|
||||
params: { password, single: singleResult ? '1' : undefined },
|
||||
})
|
||||
} else {
|
||||
return this.proxy(id).get<NoteModel>()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 日记列表分页
|
||||
*/
|
||||
|
||||
getList(page = 1, perPage = 10, options: NoteListOptions = {}) {
|
||||
const { select, sortBy, sortOrder, year } = options
|
||||
return this.proxy.get<PaginateResult<NoteModel>>({
|
||||
params: {
|
||||
page,
|
||||
size: perPage,
|
||||
select: select?.join(' '),
|
||||
sortBy,
|
||||
sortOrder,
|
||||
year,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前日记的上下各 n / 2 篇日记
|
||||
*/
|
||||
getMiddleList(id: string, size = 5) {
|
||||
return this.proxy.list(id).get<{
|
||||
data: Pick<NoteModel, 'id' | 'title' | 'nid' | 'created'>[]
|
||||
size: number
|
||||
}>({
|
||||
params: { size },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 喜欢这篇日记
|
||||
*/
|
||||
likeIt(id: string | number) {
|
||||
return this.proxy.like(id).get<never>()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取专栏内的所有日记
|
||||
*/
|
||||
getNoteByTopicId(
|
||||
topicId: string,
|
||||
page = 1,
|
||||
size = 10,
|
||||
sortOptions: SortOptions = {},
|
||||
) {
|
||||
return this.proxy.topics(topicId).get<PaginateResult<NoteModel>>({
|
||||
params: { page, size, ...sortOptions },
|
||||
})
|
||||
}
|
||||
}
|
||||
65
packages/api-client/controllers/page.ts
Normal file
65
packages/api-client/controllers/page.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { IRequestAdapter } from '~/interfaces/adapter'
|
||||
import { IController } from '~/interfaces/controller'
|
||||
import { IRequestHandler } from '~/interfaces/request'
|
||||
import { SelectFields } from '~/interfaces/types'
|
||||
import { PaginateResult } from '~/models/base'
|
||||
import { PageModel } from '~/models/page'
|
||||
import { autoBind } from '~/utils/auto-bind'
|
||||
|
||||
import { HTTPClient } from '../core'
|
||||
|
||||
declare module '../core/client' {
|
||||
interface HTTPClient<
|
||||
T extends IRequestAdapter = IRequestAdapter,
|
||||
ResponseWrapper = unknown,
|
||||
> {
|
||||
page: PageController<ResponseWrapper>
|
||||
}
|
||||
}
|
||||
|
||||
export type PageListOptions = {
|
||||
select?: SelectFields<keyof PageModel>
|
||||
sortBy?: 'order' | 'subtitle' | 'title' | 'created' | 'modified'
|
||||
sortOrder?: 1 | -1
|
||||
}
|
||||
|
||||
export class PageController<ResponseWrapper> implements IController {
|
||||
constructor(private readonly client: HTTPClient) {
|
||||
autoBind(this)
|
||||
}
|
||||
base = 'pages'
|
||||
name = 'page'
|
||||
get proxy(): IRequestHandler<ResponseWrapper> {
|
||||
return this.client.proxy(this.base)
|
||||
}
|
||||
/**
|
||||
* 页面列表
|
||||
*/
|
||||
getList(page = 1, perPage = 10, options: PageListOptions = {}) {
|
||||
const { select, sortBy, sortOrder } = options
|
||||
return this.proxy.get<PaginateResult<PageModel>>({
|
||||
params: {
|
||||
page,
|
||||
size: perPage,
|
||||
select: select?.join(' '),
|
||||
sortBy,
|
||||
sortOrder,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面详情
|
||||
*/
|
||||
getById(id: string) {
|
||||
return this.proxy(id).get<PageModel>()
|
||||
}
|
||||
/**
|
||||
* 根据路径获取页面
|
||||
* @param slug 路径
|
||||
* @returns
|
||||
*/
|
||||
getBySlug(slug: string) {
|
||||
return this.proxy.slug(slug).get<PageModel>({})
|
||||
}
|
||||
}
|
||||
95
packages/api-client/controllers/post.ts
Normal file
95
packages/api-client/controllers/post.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { IRequestAdapter } from '~/interfaces/adapter'
|
||||
import { IController } from '~/interfaces/controller'
|
||||
import { IRequestHandler, RequestProxyResult } from '~/interfaces/request'
|
||||
import { SelectFields } from '~/interfaces/types'
|
||||
import { PaginateResult } from '~/models/base'
|
||||
import { PostModel } from '~/models/post'
|
||||
import { autoBind } from '~/utils/auto-bind'
|
||||
|
||||
import { HTTPClient } from '../core/client'
|
||||
|
||||
declare module '../core/client' {
|
||||
interface HTTPClient<
|
||||
T extends IRequestAdapter = IRequestAdapter,
|
||||
ResponseWrapper = unknown,
|
||||
> {
|
||||
post: PostController<ResponseWrapper>
|
||||
}
|
||||
}
|
||||
|
||||
export type PostListOptions = {
|
||||
select?: SelectFields<keyof PostModel>
|
||||
year?: number
|
||||
sortBy?: 'categoryId' | 'title' | 'created' | 'modified'
|
||||
sortOrder?: 1 | -1
|
||||
}
|
||||
|
||||
export class PostController<ResponseWrapper> implements IController {
|
||||
constructor(private client: HTTPClient) {
|
||||
autoBind(this)
|
||||
}
|
||||
|
||||
base = 'posts'
|
||||
|
||||
name = 'post'
|
||||
|
||||
public get proxy(): IRequestHandler<ResponseWrapper> {
|
||||
return this.client.proxy(this.base)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文章列表分页
|
||||
* @param page
|
||||
* @param perPage
|
||||
* @returns
|
||||
*/
|
||||
getList(page = 1, perPage = 10, options: PostListOptions = {}) {
|
||||
const { select, sortBy, sortOrder, year } = options
|
||||
return this.proxy.get<PaginateResult<PostModel>>({
|
||||
params: {
|
||||
page,
|
||||
size: perPage,
|
||||
select: select?.join(' '),
|
||||
sortBy,
|
||||
sortOrder,
|
||||
year,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据分类和路径查找文章
|
||||
* @param categoryName
|
||||
* @param slug
|
||||
*/
|
||||
getPost(
|
||||
categoryName: string,
|
||||
slug: string,
|
||||
): RequestProxyResult<PostModel, ResponseWrapper>
|
||||
/**
|
||||
* 根据 ID 查找文章
|
||||
* @param id
|
||||
*/
|
||||
getPost(id: string): RequestProxyResult<PostModel, ResponseWrapper>
|
||||
getPost(idOrCategoryName: string, slug?: string): any {
|
||||
if (arguments.length == 1) {
|
||||
return this.proxy(idOrCategoryName).get<PostModel>()
|
||||
} else {
|
||||
return this.proxy(idOrCategoryName)(slug).get<PostModel>()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最新的文章
|
||||
*/
|
||||
getLatest() {
|
||||
return this.proxy.latest.get<PostModel>()
|
||||
}
|
||||
|
||||
/**
|
||||
* 点赞
|
||||
*/
|
||||
thumbsUp(id: string) {
|
||||
return this.proxy('_thumbs-up').get<void>({ params: { id } })
|
||||
}
|
||||
}
|
||||
28
packages/api-client/controllers/project.ts
Normal file
28
packages/api-client/controllers/project.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { IRequestAdapter } from '~/interfaces/adapter'
|
||||
import { ProjectModel } from '~/models/project'
|
||||
import { autoBind } from '~/utils/auto-bind'
|
||||
|
||||
import { HTTPClient } from '../core'
|
||||
import { BaseCrudController } from './base'
|
||||
|
||||
declare module '../core/client' {
|
||||
interface HTTPClient<
|
||||
T extends IRequestAdapter = IRequestAdapter,
|
||||
ResponseWrapper = unknown,
|
||||
> {
|
||||
project: ProjectController<ResponseWrapper>
|
||||
}
|
||||
}
|
||||
|
||||
export class ProjectController<ResponseWrapper> extends BaseCrudController<
|
||||
ProjectModel,
|
||||
ResponseWrapper
|
||||
> {
|
||||
constructor(protected readonly client: HTTPClient) {
|
||||
super(client)
|
||||
autoBind(this)
|
||||
}
|
||||
|
||||
base = 'projects'
|
||||
name = 'project'
|
||||
}
|
||||
54
packages/api-client/controllers/recently.ts
Normal file
54
packages/api-client/controllers/recently.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { IRequestAdapter } from '~/interfaces/adapter'
|
||||
import { IController } from '~/interfaces/controller'
|
||||
import { IRequestHandler } from '~/interfaces/request'
|
||||
import { RecentlyModel } from '~/models/recently'
|
||||
import { autoBind } from '~/utils/auto-bind'
|
||||
|
||||
import { HTTPClient } from '../core'
|
||||
|
||||
declare module '../core/client' {
|
||||
interface HTTPClient<
|
||||
T extends IRequestAdapter = IRequestAdapter,
|
||||
ResponseWrapper = unknown,
|
||||
> {
|
||||
recently: RecentlyController<ResponseWrapper>
|
||||
shorthand: RecentlyController<ResponseWrapper>
|
||||
}
|
||||
}
|
||||
|
||||
export class RecentlyController<ResponseWrapper> implements IController {
|
||||
base = 'recently'
|
||||
name = ['recently', 'shorthand']
|
||||
|
||||
constructor(private readonly client: HTTPClient) {
|
||||
autoBind(this)
|
||||
}
|
||||
|
||||
get proxy(): IRequestHandler<ResponseWrapper> {
|
||||
return this.client.proxy(this.base)
|
||||
}
|
||||
/**
|
||||
* 获取最新一条
|
||||
*/
|
||||
getLatestOne() {
|
||||
return this.proxy.latest.get<RecentlyModel>()
|
||||
}
|
||||
|
||||
getAll() {
|
||||
return this.proxy.all.get<{ data: RecentlyModel[] }>()
|
||||
}
|
||||
|
||||
getList(
|
||||
before?: string | undefined,
|
||||
after?: string | undefined,
|
||||
size?: number | number,
|
||||
) {
|
||||
return this.proxy.get<{ data: RecentlyModel[] }>({
|
||||
params: {
|
||||
before,
|
||||
after,
|
||||
size,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
41
packages/api-client/controllers/say.ts
Normal file
41
packages/api-client/controllers/say.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { IRequestAdapter } from '~/interfaces/adapter'
|
||||
import { IController } from '~/interfaces/controller'
|
||||
import { IRequestHandler } from '~/interfaces/request'
|
||||
import { SayModel } from '~/models/say'
|
||||
import { autoBind } from '~/utils/auto-bind'
|
||||
|
||||
import { HTTPClient } from '../core'
|
||||
import { BaseCrudController } from './base'
|
||||
|
||||
declare module '../core/client' {
|
||||
interface HTTPClient<
|
||||
T extends IRequestAdapter = IRequestAdapter,
|
||||
ResponseWrapper = unknown,
|
||||
> {
|
||||
say: SayController<ResponseWrapper>
|
||||
}
|
||||
}
|
||||
|
||||
export class SayController<ResponseWrapper>
|
||||
extends BaseCrudController<SayModel, ResponseWrapper>
|
||||
implements IController
|
||||
{
|
||||
base = 'says'
|
||||
name = 'say'
|
||||
|
||||
constructor(protected client: HTTPClient) {
|
||||
super(client)
|
||||
autoBind(this)
|
||||
}
|
||||
|
||||
public get proxy(): IRequestHandler<ResponseWrapper> {
|
||||
return this.client.proxy(this.base)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取随机一条
|
||||
*/
|
||||
getRandom() {
|
||||
return this.proxy.random.get<{ data: SayModel | null }>()
|
||||
}
|
||||
}
|
||||
109
packages/api-client/controllers/search.ts
Normal file
109
packages/api-client/controllers/search.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { IRequestAdapter } from '~/interfaces/adapter'
|
||||
import { IController } from '~/interfaces/controller'
|
||||
import { IRequestHandler, RequestProxyResult } from '~/interfaces/request'
|
||||
import { PaginateResult } from '~/models/base'
|
||||
import { NoteModel } from '~/models/note'
|
||||
import { PostModel } from '~/models/post'
|
||||
import { autoBind } from '~/utils/auto-bind'
|
||||
|
||||
import { PageModel } from '..'
|
||||
import { HTTPClient } from '../core'
|
||||
|
||||
declare module '../core/client' {
|
||||
interface HTTPClient<
|
||||
T extends IRequestAdapter = IRequestAdapter,
|
||||
ResponseWrapper = unknown,
|
||||
> {
|
||||
search: SearchController<ResponseWrapper>
|
||||
}
|
||||
}
|
||||
|
||||
export type SearchType = 'post' | 'note'
|
||||
|
||||
export type SearchOption = {
|
||||
orderBy?: string
|
||||
order?: number
|
||||
rawAlgolia?: boolean
|
||||
}
|
||||
export class SearchController<ResponseWrapper> implements IController {
|
||||
base = 'search'
|
||||
name = 'search'
|
||||
|
||||
constructor(private readonly client: HTTPClient) {
|
||||
autoBind(this)
|
||||
}
|
||||
|
||||
get proxy(): IRequestHandler<ResponseWrapper> {
|
||||
return this.client.proxy(this.base)
|
||||
}
|
||||
|
||||
search(
|
||||
type: 'note',
|
||||
keyword: string,
|
||||
options?: Omit<SearchOption, 'rawAlgolia'>,
|
||||
): Promise<
|
||||
RequestProxyResult<
|
||||
PaginateResult<
|
||||
Pick<NoteModel, 'modified' | 'id' | 'title' | 'created' | 'nid'>
|
||||
>,
|
||||
ResponseWrapper
|
||||
>
|
||||
>
|
||||
search(
|
||||
type: 'post',
|
||||
keyword: string,
|
||||
options?: Omit<SearchOption, 'rawAlgolia'>,
|
||||
): Promise<
|
||||
RequestProxyResult<
|
||||
PaginateResult<
|
||||
Pick<
|
||||
PostModel,
|
||||
'modified' | 'id' | 'title' | 'created' | 'slug' | 'category'
|
||||
>
|
||||
>,
|
||||
ResponseWrapper
|
||||
>
|
||||
>
|
||||
search(
|
||||
type: SearchType,
|
||||
keyword: string,
|
||||
options: Omit<SearchOption, 'rawAlgolia'> = {},
|
||||
): any {
|
||||
return this.proxy(type).get({
|
||||
params: { keyword, ...options },
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 从 algolya 搜索
|
||||
* https://www.algolia.com/doc/api-reference/api-methods/search/
|
||||
* @param keyword
|
||||
* @param options
|
||||
* @returns
|
||||
*/
|
||||
searchByAlgolia(keyword: string, options?: SearchOption) {
|
||||
return this.proxy('algolia').get<
|
||||
RequestProxyResult<
|
||||
PaginateResult<
|
||||
| (Pick<
|
||||
PostModel,
|
||||
'modified' | 'id' | 'title' | 'created' | 'slug' | 'category'
|
||||
> & { type: 'post' })
|
||||
| (Pick<
|
||||
NoteModel,
|
||||
'id' | 'created' | 'id' | 'modified' | 'title' | 'nid'
|
||||
> & { type: 'note' })
|
||||
| (Pick<
|
||||
PageModel,
|
||||
'id' | 'title' | 'created' | 'modified' | 'slug'
|
||||
> & { type: 'page' })
|
||||
> & {
|
||||
/**
|
||||
* @see: algoliasearch <https://www.algolia.com/doc/api-reference/api-methods/search/>
|
||||
*/
|
||||
raw?: any
|
||||
},
|
||||
ResponseWrapper
|
||||
>
|
||||
>({ params: { keyword, ...options } })
|
||||
}
|
||||
}
|
||||
32
packages/api-client/controllers/severless.ts
Normal file
32
packages/api-client/controllers/severless.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { IRequestAdapter } from '~/interfaces/adapter'
|
||||
import { IController } from '~/interfaces/controller'
|
||||
import { IRequestHandler } from '~/interfaces/request'
|
||||
import { autoBind } from '~/utils/auto-bind'
|
||||
|
||||
import { HTTPClient } from '../core'
|
||||
|
||||
declare module '../core/client' {
|
||||
interface HTTPClient<
|
||||
T extends IRequestAdapter = IRequestAdapter,
|
||||
ResponseWrapper = unknown,
|
||||
> {
|
||||
serverless: ServerlessController<ResponseWrapper>
|
||||
}
|
||||
}
|
||||
|
||||
export class ServerlessController<ResponseWrapper> implements IController {
|
||||
base = 'serverless'
|
||||
name = 'serverless'
|
||||
|
||||
constructor(protected client: HTTPClient) {
|
||||
autoBind(this)
|
||||
}
|
||||
|
||||
get proxy(): IRequestHandler<ResponseWrapper> {
|
||||
return this.client.proxy(this.base)
|
||||
}
|
||||
|
||||
getByReferenceAndName<T = unknown>(reference: string, name: string) {
|
||||
return this.proxy(reference)(name).get<T>()
|
||||
}
|
||||
}
|
||||
36
packages/api-client/controllers/snippet.ts
Normal file
36
packages/api-client/controllers/snippet.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { IRequestAdapter } from '~/interfaces/adapter'
|
||||
import { IController } from '~/interfaces/controller'
|
||||
import { IRequestHandler } from '~/interfaces/request'
|
||||
import { autoBind } from '~/utils/auto-bind'
|
||||
|
||||
import { HTTPClient } from '../core'
|
||||
|
||||
declare module '../core/client' {
|
||||
interface HTTPClient<
|
||||
T extends IRequestAdapter = IRequestAdapter,
|
||||
ResponseWrapper = unknown,
|
||||
> {
|
||||
snippet: SnippetController<ResponseWrapper>
|
||||
}
|
||||
}
|
||||
|
||||
export class SnippetController<ResponseWrapper> implements IController {
|
||||
base = 'snippets'
|
||||
name = 'snippet'
|
||||
|
||||
constructor(protected client: HTTPClient) {
|
||||
autoBind(this)
|
||||
}
|
||||
|
||||
get proxy(): IRequestHandler<ResponseWrapper> {
|
||||
return this.client.proxy(this.base)
|
||||
}
|
||||
|
||||
// getById(id: string) {
|
||||
// return this.proxy(id).get<Omit<SnippetModel, 'data'>>()
|
||||
// }
|
||||
|
||||
getByReferenceAndName<T = unknown>(reference: string, name: string) {
|
||||
return this.proxy(reference)(name).get<T>()
|
||||
}
|
||||
}
|
||||
38
packages/api-client/controllers/topic.ts
Normal file
38
packages/api-client/controllers/topic.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { IRequestAdapter } from '~/interfaces/adapter'
|
||||
import { IController } from '~/interfaces/controller'
|
||||
import { IRequestHandler } from '~/interfaces/request'
|
||||
import { TopicModel } from '~/models/topic'
|
||||
import { autoBind } from '~/utils/auto-bind'
|
||||
|
||||
import { HTTPClient } from '../core'
|
||||
import { BaseCrudController } from './base'
|
||||
|
||||
declare module '../core/client' {
|
||||
interface HTTPClient<
|
||||
T extends IRequestAdapter = IRequestAdapter,
|
||||
ResponseWrapper = unknown,
|
||||
> {
|
||||
topic: TopicController<ResponseWrapper>
|
||||
}
|
||||
}
|
||||
|
||||
export class TopicController<ResponseWrapper>
|
||||
extends BaseCrudController<TopicModel, ResponseWrapper>
|
||||
implements IController
|
||||
{
|
||||
base = 'topics'
|
||||
name = 'topic'
|
||||
|
||||
constructor(protected client: HTTPClient) {
|
||||
super(client)
|
||||
autoBind(this)
|
||||
}
|
||||
|
||||
public get proxy(): IRequestHandler<ResponseWrapper> {
|
||||
return this.client.proxy(this.base)
|
||||
}
|
||||
|
||||
getTopicBySlug(slug: string) {
|
||||
return this.proxy.slug(slug).get<TopicModel>()
|
||||
}
|
||||
}
|
||||
62
packages/api-client/controllers/user.ts
Normal file
62
packages/api-client/controllers/user.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { IRequestAdapter } from '~/interfaces/adapter'
|
||||
import { IController } from '~/interfaces/controller'
|
||||
import { IRequestHandler } from '~/interfaces/request'
|
||||
import { TLogin, UserModel } from '~/models/user'
|
||||
import { autoBind } from '~/utils/auto-bind'
|
||||
|
||||
import { HTTPClient } from '../core'
|
||||
|
||||
declare module '../core/client' {
|
||||
interface HTTPClient<
|
||||
T extends IRequestAdapter = IRequestAdapter,
|
||||
ResponseWrapper = unknown,
|
||||
> {
|
||||
user: UserController<ResponseWrapper>
|
||||
master: UserController<ResponseWrapper>
|
||||
}
|
||||
}
|
||||
|
||||
export class UserController<ResponseWrapper> implements IController {
|
||||
constructor(private readonly client: HTTPClient) {
|
||||
autoBind(this)
|
||||
}
|
||||
|
||||
base = 'master'
|
||||
|
||||
name = ['user', 'master']
|
||||
|
||||
public get proxy(): IRequestHandler<ResponseWrapper> {
|
||||
return this.client.proxy(this.base)
|
||||
}
|
||||
|
||||
getMasterInfo() {
|
||||
return this.proxy.get<UserModel>()
|
||||
}
|
||||
|
||||
login(username: string, password: string) {
|
||||
return this.proxy.login.post<TLogin>({
|
||||
data: {
|
||||
username,
|
||||
password,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
loginWithToken(token?: string) {
|
||||
return this.proxy.login.put<{ token: string }>({
|
||||
params: token
|
||||
? {
|
||||
token: `bearer ${token.replace(/^Bearer\s/i, '')}`,
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
}
|
||||
|
||||
checkTokenValid(token: string) {
|
||||
return this.proxy.check_logged.get<{ ok: number; isGuest: boolean }>({
|
||||
params: {
|
||||
token: `bearer ${token.replace(/^Bearer\s/i, '')}`,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
43
packages/api-client/core/attach-request.ts
Normal file
43
packages/api-client/core/attach-request.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { HTTPClient } from '.'
|
||||
|
||||
export function attachRequestMethod<T extends HTTPClient<any, any>>(target: T) {
|
||||
Object.defineProperty(target, '$$get', {
|
||||
value(url: string, options?: any) {
|
||||
// HINT: method get only accept search params;
|
||||
const { params = {}, ...rest } = options
|
||||
const qs = handleSearchParams(params)
|
||||
|
||||
return target.instance.get(`${url}${qs ? `${`?${qs}`}` : ''}`, rest)
|
||||
},
|
||||
})
|
||||
;(['put', 'post', 'patch', 'delete'] as const).forEach((method) => {
|
||||
Object.defineProperty(target, `$$${method}`, {
|
||||
value(path: string, options?: any) {
|
||||
return target.instance[method](path, options)
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
// FIXME: only support string value
|
||||
function handleSearchParams(obj: URLSearchParams | Record<string, string>) {
|
||||
if (!obj && typeof obj !== 'object') {
|
||||
throw new TypeError('params must be object.')
|
||||
}
|
||||
|
||||
if (obj instanceof URLSearchParams) {
|
||||
return obj.toString()
|
||||
}
|
||||
const search = new URLSearchParams()
|
||||
|
||||
Object.entries<any>(obj).forEach(([k, v]) => {
|
||||
if (
|
||||
typeof v === 'undefined' ||
|
||||
Object.prototype.toString.call(v) === '[object Null]'
|
||||
) {
|
||||
return
|
||||
}
|
||||
search.set(k, v)
|
||||
})
|
||||
|
||||
return search.toString()
|
||||
}
|
||||
252
packages/api-client/core/client.ts
Normal file
252
packages/api-client/core/client.ts
Normal file
@@ -0,0 +1,252 @@
|
||||
import {
|
||||
IAdaptorRequestResponseType,
|
||||
IRequestAdapter,
|
||||
} from '~/interfaces/adapter'
|
||||
import { ClientOptions } from '~/interfaces/client'
|
||||
import { IController } from '~/interfaces/controller'
|
||||
import { RequestOptions } from '~/interfaces/instance'
|
||||
import { IRequestHandler, Method } from '~/interfaces/request'
|
||||
import { Class } from '~/interfaces/types'
|
||||
import { isPlainObject } from '~/utils'
|
||||
import { camelcaseKeys } from '~/utils/camelcase-keys'
|
||||
import { resolveFullPath } from '~/utils/path'
|
||||
|
||||
import { allContollerNames } from '../controllers'
|
||||
import { attachRequestMethod } from './attach-request'
|
||||
import { RequestError } from './error'
|
||||
|
||||
const methodPrefix = '_$'
|
||||
export type { HTTPClient }
|
||||
class HTTPClient<
|
||||
T extends IRequestAdapter = IRequestAdapter,
|
||||
ResponseWrapper = unknown,
|
||||
> {
|
||||
private readonly _proxy: IRequestHandler<ResponseWrapper>
|
||||
|
||||
constructor(
|
||||
private readonly _endpoint: string,
|
||||
private _adaptor: T,
|
||||
private options: Omit<ClientOptions, 'controllers'> = {},
|
||||
) {
|
||||
this._endpoint = _endpoint
|
||||
.replace(/\/*$/, '')
|
||||
.replace('localhost', '127.0.0.1')
|
||||
this._proxy = this.buildRoute(this)()
|
||||
options.transformResponse =
|
||||
options.transformResponse || ((data) => camelcaseKeys(data))
|
||||
|
||||
this.initGetClient()
|
||||
|
||||
attachRequestMethod(this)
|
||||
}
|
||||
|
||||
private initGetClient() {
|
||||
for (const name of allContollerNames) {
|
||||
Object.defineProperty(this, name, {
|
||||
get() {
|
||||
const client = Reflect.get(this, `${methodPrefix}${name}`)
|
||||
if (!client) {
|
||||
throw new ReferenceError(
|
||||
`${
|
||||
name.charAt(0).toUpperCase() + name.slice(1)
|
||||
} Client not inject yet, please inject with client.injectClients(...)`,
|
||||
)
|
||||
}
|
||||
return client
|
||||
},
|
||||
configurable: false,
|
||||
enumerable: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public injectControllers(...Controller: Class<IController>[]): void
|
||||
public injectControllers(Controller: Class<IController>[]): void
|
||||
public injectControllers(Controller: any, ...rest: any[]) {
|
||||
Controller = Array.isArray(Controller) ? Controller : [Controller, ...rest]
|
||||
for (const Client of Controller) {
|
||||
const cl = new Client(this)
|
||||
|
||||
if (Array.isArray(cl.name)) {
|
||||
for (const name of cl.name) {
|
||||
attach.call(this, name, cl)
|
||||
}
|
||||
} else {
|
||||
attach.call(this, cl.name, cl)
|
||||
}
|
||||
}
|
||||
|
||||
function attach(this: any, name: string, cl: IController) {
|
||||
Object.defineProperty(this, `${methodPrefix}${name.toLowerCase()}`, {
|
||||
get() {
|
||||
return cl
|
||||
},
|
||||
enumerable: false,
|
||||
configurable: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
get endpoint() {
|
||||
return this._endpoint
|
||||
}
|
||||
|
||||
get instance() {
|
||||
return this._adaptor
|
||||
}
|
||||
|
||||
public request(options: {
|
||||
url: string
|
||||
method?: string
|
||||
data?: any
|
||||
params?: any
|
||||
}) {
|
||||
return (this as any)[`$$${String(options.method || 'get').toLowerCase()}`](
|
||||
options.url,
|
||||
options,
|
||||
) as Promise<IAdaptorRequestResponseType<any>>
|
||||
}
|
||||
|
||||
public get proxy() {
|
||||
return this._proxy
|
||||
}
|
||||
|
||||
private buildRoute(manager: this): () => IRequestHandler<ResponseWrapper> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
const noop = () => {}
|
||||
const methods = ['get', 'post', 'delete', 'patch', 'put']
|
||||
const reflectors = [
|
||||
'toString',
|
||||
'valueOf',
|
||||
'inspect',
|
||||
'constructor',
|
||||
Symbol.toPrimitive,
|
||||
]
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
const that = this
|
||||
|
||||
return () => {
|
||||
const route = ['']
|
||||
|
||||
const handler: any = {
|
||||
get(target: any, name: Method) {
|
||||
if (reflectors.includes(name))
|
||||
return (withBase?: boolean) => {
|
||||
if (withBase) {
|
||||
const path = resolveFullPath(that.endpoint, route.join('/'))
|
||||
route.length = 0
|
||||
return path
|
||||
} else {
|
||||
const path = route.join('/')
|
||||
route.length = 0
|
||||
return path.startsWith('/') ? path : `/${path}`
|
||||
}
|
||||
}
|
||||
if (methods.includes(name)) {
|
||||
return async (options: RequestOptions) => {
|
||||
const url = resolveFullPath(that.endpoint, route.join('/'))
|
||||
route.length = 0
|
||||
let res: Record<string, any> & { data: any }
|
||||
try {
|
||||
res = await manager.request({
|
||||
method: name,
|
||||
...options,
|
||||
url,
|
||||
})
|
||||
} catch (e: any) {
|
||||
let message = e.message
|
||||
let code =
|
||||
e.code ||
|
||||
e.status ||
|
||||
e.statusCode ||
|
||||
e.response?.status ||
|
||||
e.response?.statusCode ||
|
||||
e.response?.code ||
|
||||
500
|
||||
|
||||
if (that.options.getCodeMessageFromException) {
|
||||
const errorInfo = that.options.getCodeMessageFromException(e)
|
||||
message = errorInfo.message || message
|
||||
code = errorInfo.code || code
|
||||
}
|
||||
|
||||
throw that.options.customThrowResponseError
|
||||
? that.options.customThrowResponseError(e)
|
||||
: new RequestError(message, code, url, e)
|
||||
}
|
||||
|
||||
const data = res.data
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
|
||||
const transform =
|
||||
(Array.isArray(data) || isPlainObject(data)) &&
|
||||
that.options.transformResponse
|
||||
? that.options.transformResponse(data)
|
||||
: data
|
||||
|
||||
if (transform && typeof transform === 'object') {
|
||||
Object.defineProperty(transform, '$raw', {
|
||||
get() {
|
||||
return res
|
||||
},
|
||||
enumerable: false,
|
||||
configurable: false,
|
||||
})
|
||||
|
||||
// attach request config onto response
|
||||
|
||||
Object.defineProperty(transform, '$request', {
|
||||
get() {
|
||||
return {
|
||||
url,
|
||||
method: name,
|
||||
options,
|
||||
}
|
||||
},
|
||||
enumerable: false,
|
||||
})
|
||||
}
|
||||
|
||||
return transform
|
||||
}
|
||||
}
|
||||
route.push(name)
|
||||
return new Proxy(noop, handler)
|
||||
},
|
||||
// @ts-ignore
|
||||
apply(target: any, _, args) {
|
||||
route.push(...args.filter((x: string) => x !== null))
|
||||
return new Proxy(noop, handler)
|
||||
},
|
||||
}
|
||||
|
||||
return new Proxy(noop, handler) as any
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function createClient<T extends IRequestAdapter>(adapter: T) {
|
||||
return <
|
||||
ResponseWrapper = T extends { responseWrapper: infer Type }
|
||||
? Type extends undefined
|
||||
? unknown
|
||||
: Type
|
||||
: unknown,
|
||||
>(
|
||||
endpoint: string,
|
||||
options?: ClientOptions,
|
||||
) => {
|
||||
const client = new HTTPClient<T, ResponseWrapper>(
|
||||
endpoint,
|
||||
adapter,
|
||||
options,
|
||||
)
|
||||
const { controllers } = options || {}
|
||||
if (controllers) {
|
||||
client.injectControllers(controllers)
|
||||
}
|
||||
return client
|
||||
}
|
||||
}
|
||||
10
packages/api-client/core/error.ts
Normal file
10
packages/api-client/core/error.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export class RequestError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public status: number,
|
||||
public path: string,
|
||||
public raw: any,
|
||||
) {
|
||||
super(message)
|
||||
}
|
||||
}
|
||||
2
packages/api-client/core/index.ts
Normal file
2
packages/api-client/core/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './client'
|
||||
export * from './error'
|
||||
9
packages/api-client/dtos/comment.ts
Normal file
9
packages/api-client/dtos/comment.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export class CommentDto {
|
||||
author!: string
|
||||
|
||||
text!: string
|
||||
|
||||
mail!: string
|
||||
|
||||
url?: string
|
||||
}
|
||||
10
packages/api-client/index.ts
Normal file
10
packages/api-client/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { createClient } from './core'
|
||||
|
||||
export * from './controllers'
|
||||
export { createClient, RequestError } from './core'
|
||||
export type { HTTPClient } from './core'
|
||||
export * from './models'
|
||||
export { camelcaseKeys as simpleCamelcaseKeys } from './utils/camelcase-keys'
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default createClient
|
||||
36
packages/api-client/interfaces/adapter.ts
Normal file
36
packages/api-client/interfaces/adapter.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { RequestOptions } from './instance'
|
||||
|
||||
export type IAdaptorRequestResponseType<P> = Promise<
|
||||
Record<string, any> & { data: P }
|
||||
>
|
||||
|
||||
export type IRequestAdapter<T = any, Response = undefined> = Readonly<
|
||||
(Response extends undefined ? {} : { responseWrapper: Response }) & {
|
||||
default: T
|
||||
|
||||
get<P = unknown>(
|
||||
url: string,
|
||||
options?: Omit<RequestOptions, 'data'>,
|
||||
): IAdaptorRequestResponseType<P>
|
||||
|
||||
post<P = unknown>(
|
||||
url: string,
|
||||
options: Partial<RequestOptions>,
|
||||
): IAdaptorRequestResponseType<P>
|
||||
|
||||
patch<P = unknown>(
|
||||
url: string,
|
||||
options: Partial<RequestOptions>,
|
||||
): IAdaptorRequestResponseType<P>
|
||||
|
||||
delete<P = unknown>(
|
||||
url: string,
|
||||
options?: Omit<RequestOptions, 'data'>,
|
||||
): IAdaptorRequestResponseType<P>
|
||||
|
||||
put<P = unknown>(
|
||||
url: string,
|
||||
options: Partial<RequestOptions>,
|
||||
): IAdaptorRequestResponseType<P>
|
||||
}
|
||||
>
|
||||
15
packages/api-client/interfaces/client.ts
Normal file
15
packages/api-client/interfaces/client.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { IController } from './controller'
|
||||
import { Class } from './types'
|
||||
|
||||
interface IClientOptions {
|
||||
controllers: Class<IController>[]
|
||||
getCodeMessageFromException: <T = Error>(
|
||||
error: T,
|
||||
) => {
|
||||
message?: string | undefined | null
|
||||
code?: number | undefined | null
|
||||
}
|
||||
customThrowResponseError: <T extends Error = Error>(err: any) => T
|
||||
transformResponse: <T = any>(data: any) => T
|
||||
}
|
||||
export type ClientOptions = Partial<IClientOptions>
|
||||
5
packages/api-client/interfaces/controller.ts
Normal file
5
packages/api-client/interfaces/controller.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface IController {
|
||||
base: string
|
||||
|
||||
name: string | string[]
|
||||
}
|
||||
8
packages/api-client/interfaces/instance.ts
Normal file
8
packages/api-client/interfaces/instance.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export interface RequestOptions {
|
||||
method?: string
|
||||
data?: Record<string, any>
|
||||
params?: Record<string, any> | URLSearchParams
|
||||
headers?: Record<string, string>
|
||||
|
||||
[key: string]: any
|
||||
}
|
||||
1
packages/api-client/interfaces/options.ts
Normal file
1
packages/api-client/interfaces/options.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type SortOrder = 'asc' | 'desc'
|
||||
4
packages/api-client/interfaces/params.ts
Normal file
4
packages/api-client/interfaces/params.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface PaginationParams {
|
||||
size?: number
|
||||
page?: number
|
||||
}
|
||||
83
packages/api-client/interfaces/request.ts
Normal file
83
packages/api-client/interfaces/request.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { RequestOptions } from './instance'
|
||||
|
||||
type NoStringIndex<T> = { [K in keyof T as string extends K ? never : K]: T[K] }
|
||||
|
||||
export type Method = 'get' | 'delete' | 'post' | 'put' | 'patch'
|
||||
|
||||
export interface IRequestHandler<ResponseWrapper> {
|
||||
(path?: string | number): IRequestHandler<ResponseWrapper>
|
||||
// @ts-ignore
|
||||
get<P = unknown>(
|
||||
options?: Omit<NoStringIndex<RequestOptions>, 'data'>,
|
||||
): RequestProxyResult<P, ResponseWrapper>
|
||||
// @ts-ignore
|
||||
post<P = unknown>(
|
||||
options?: RequestOptions,
|
||||
): RequestProxyResult<P, ResponseWrapper>
|
||||
// @ts-ignore
|
||||
patch<P = unknown>(
|
||||
options?: RequestOptions,
|
||||
): RequestProxyResult<P, ResponseWrapper>
|
||||
// @ts-ignore
|
||||
delete<P = unknown>(
|
||||
options?: Omit<NoStringIndex<RequestOptions>, 'data'>,
|
||||
): RequestProxyResult<P, ResponseWrapper>
|
||||
// @ts-ignore
|
||||
put<P = unknown>(
|
||||
options?: RequestOptions,
|
||||
): RequestProxyResult<P, ResponseWrapper>
|
||||
// @ts-ignore
|
||||
toString(withBase?: boolean): string
|
||||
// @ts-ignore
|
||||
valueOf(withBase?: boolean): string
|
||||
[key: string]: IRequestHandler<ResponseWrapper>
|
||||
}
|
||||
|
||||
export type RequestProxyResult<
|
||||
T,
|
||||
ResponseWrapper,
|
||||
R = ResponseWrapper extends unknown
|
||||
? { data: T; [key: string]: any }
|
||||
: ResponseWrapper extends { data: T }
|
||||
? ResponseWrapper
|
||||
: Omit<ResponseWrapper, 'data'> & { data: T },
|
||||
> = Promise<ResponseProxyExtraRaw<T, R, ResponseWrapper>>
|
||||
|
||||
type CamelToSnake<T extends string, P extends string = ''> = string extends T
|
||||
? string
|
||||
: T extends `${infer C0}${infer R}`
|
||||
? CamelToSnake<
|
||||
R,
|
||||
`${P}${C0 extends Lowercase<C0> ? '' : '_'}${Lowercase<C0>}`
|
||||
>
|
||||
: P
|
||||
|
||||
type CamelKeysToSnake<T> = {
|
||||
[K in keyof T as CamelToSnake<Extract<K, string>>]: T[K]
|
||||
}
|
||||
|
||||
type ResponseWrapperType<Response, RawData, T> = {
|
||||
$raw: Response extends { data: infer T }
|
||||
? Response
|
||||
: Response extends unknown
|
||||
? {
|
||||
[i: string]: any
|
||||
data: RawData extends unknown ? CamelKeysToSnake<T> : RawData
|
||||
}
|
||||
: Response
|
||||
$request: {
|
||||
path: string
|
||||
method: string
|
||||
[k: string]: string
|
||||
}
|
||||
}
|
||||
|
||||
export type ResponseProxyExtraRaw<
|
||||
T,
|
||||
RawData = unknown,
|
||||
Response = unknown,
|
||||
> = T extends object
|
||||
? T & ResponseWrapperType<Response, RawData, T>
|
||||
: T extends unknown
|
||||
? T & ResponseWrapperType<Response, RawData, T>
|
||||
: unknown
|
||||
3
packages/api-client/interfaces/types.ts
Normal file
3
packages/api-client/interfaces/types.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export type Class<T> = new (...args: any[]) => T
|
||||
|
||||
export type SelectFields<T extends string> = `${'+' | '-' | ''}${T}`[]
|
||||
71
packages/api-client/models/aggregate.ts
Normal file
71
packages/api-client/models/aggregate.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { CategoryModel } from './category'
|
||||
import { NoteModel } from './note'
|
||||
import { PageModel } from './page'
|
||||
import { PostModel } from './post'
|
||||
import { SayModel } from './say'
|
||||
import { SeoOptionModel } from './setting'
|
||||
import { UserModel } from './user'
|
||||
|
||||
export interface AggregateRoot {
|
||||
user: UserModel
|
||||
seo: SeoOptionModel
|
||||
url: Url
|
||||
categories: CategoryModel[]
|
||||
pageMeta: Pick<PageModel, 'title' | 'id' | 'slug' | 'order'>[] | null
|
||||
}
|
||||
|
||||
export interface Url {
|
||||
wsUrl: string
|
||||
serverUrl: string
|
||||
webUrl: string
|
||||
}
|
||||
|
||||
export interface AggregateTop {
|
||||
notes: Pick<NoteModel, 'id' | 'title' | 'created' | 'nid'>[]
|
||||
posts: Pick<PostModel, 'id' | 'slug' | 'created' | 'title' | 'category'>[]
|
||||
says: SayModel[]
|
||||
}
|
||||
|
||||
export enum TimelineType {
|
||||
Post,
|
||||
Note,
|
||||
}
|
||||
|
||||
export interface TimelineData {
|
||||
notes?: Pick<
|
||||
NoteModel,
|
||||
| 'id'
|
||||
| 'nid'
|
||||
| 'title'
|
||||
| 'weather'
|
||||
| 'mood'
|
||||
| 'created'
|
||||
| 'modified'
|
||||
| 'hasMemory'
|
||||
>[]
|
||||
|
||||
posts?: (Pick<
|
||||
PostModel,
|
||||
'id' | 'title' | 'slug' | 'created' | 'modified' | 'category'
|
||||
> & { url: string })[]
|
||||
}
|
||||
|
||||
export interface AggregateStat {
|
||||
allComments: number
|
||||
categories: number
|
||||
comments: number
|
||||
linkApply: number
|
||||
links: number
|
||||
notes: number
|
||||
pages: number
|
||||
posts: number
|
||||
says: number
|
||||
recently: number
|
||||
unreadComments: number
|
||||
online: number
|
||||
todayMaxOnline: string
|
||||
todayOnlineTotal: string
|
||||
callTime: number
|
||||
uv: number
|
||||
todayIpAccessCount: number
|
||||
}
|
||||
45
packages/api-client/models/base.ts
Normal file
45
packages/api-client/models/base.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
export interface Count {
|
||||
read: number
|
||||
like: number
|
||||
}
|
||||
|
||||
export interface Image {
|
||||
height: number
|
||||
width: number
|
||||
type: string
|
||||
accent?: string
|
||||
src: string
|
||||
}
|
||||
|
||||
export interface Pager {
|
||||
total: number
|
||||
size: number
|
||||
currentPage: number
|
||||
totalPage: number
|
||||
hasPrevPage: boolean
|
||||
hasNextPage: boolean
|
||||
}
|
||||
|
||||
export interface PaginateResult<T> {
|
||||
data: T[]
|
||||
pagination: Pager
|
||||
}
|
||||
|
||||
export interface BaseModel {
|
||||
created: string
|
||||
id: string
|
||||
}
|
||||
|
||||
export interface BaseCommentIndexModel extends BaseModel {
|
||||
commentsIndex?: number
|
||||
|
||||
allowComment: boolean
|
||||
}
|
||||
export interface TextBaseModel extends BaseCommentIndexModel {
|
||||
title: string
|
||||
text: string
|
||||
images?: Image[]
|
||||
modified: string | null
|
||||
|
||||
meta?: Record<string, any>
|
||||
}
|
||||
25
packages/api-client/models/category.ts
Normal file
25
packages/api-client/models/category.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { BaseModel } from './base'
|
||||
import { PostModel } from './post'
|
||||
|
||||
export enum CategoryType {
|
||||
Category,
|
||||
Tag,
|
||||
}
|
||||
|
||||
export interface CategoryModel extends BaseModel {
|
||||
type: CategoryType
|
||||
count: number
|
||||
slug: string
|
||||
name: string
|
||||
}
|
||||
export type CategoryWithChildrenModel = CategoryModel & {
|
||||
children: Pick<PostModel, 'id' | 'title' | 'slug' | 'modified' | 'created'>[]
|
||||
}
|
||||
|
||||
export type CategoryEntries = {
|
||||
entries: Record<string, CategoryWithChildrenModel>
|
||||
}
|
||||
export interface TagModel {
|
||||
count: number
|
||||
name: string
|
||||
}
|
||||
41
packages/api-client/models/comment.ts
Normal file
41
packages/api-client/models/comment.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { BaseModel } from './base'
|
||||
import { CategoryModel } from './category'
|
||||
|
||||
export enum RefType {
|
||||
Page = 'Page',
|
||||
Post = 'Post',
|
||||
Note = 'Note',
|
||||
}
|
||||
export interface CommentModel extends BaseModel {
|
||||
refType: RefType
|
||||
ref: string
|
||||
state: number
|
||||
commentsIndex: number
|
||||
author: string
|
||||
text: string
|
||||
mail?: string
|
||||
url?: string
|
||||
ip?: string
|
||||
agent?: string
|
||||
key: string
|
||||
pin?: boolean
|
||||
|
||||
avatar: string
|
||||
parent?: CommentModel | string
|
||||
children: CommentModel[]
|
||||
|
||||
isWhispers?: boolean
|
||||
}
|
||||
export interface CommentRef {
|
||||
id: string
|
||||
categoryId?: string
|
||||
slug: string
|
||||
title: string
|
||||
category?: CategoryModel
|
||||
}
|
||||
|
||||
export enum CommentState {
|
||||
Unread,
|
||||
Read,
|
||||
Junk,
|
||||
}
|
||||
14
packages/api-client/models/index.ts
Normal file
14
packages/api-client/models/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export * from './aggregate'
|
||||
export * from './base'
|
||||
export * from './category'
|
||||
export * from './comment'
|
||||
export * from './link'
|
||||
export * from './note'
|
||||
export * from './page'
|
||||
export * from './post'
|
||||
export * from './project'
|
||||
export * from './recently'
|
||||
export * from './say'
|
||||
export * from './setting'
|
||||
export * from './snippet'
|
||||
export * from './user'
|
||||
25
packages/api-client/models/link.ts
Normal file
25
packages/api-client/models/link.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { BaseModel } from './base'
|
||||
|
||||
export enum LinkType {
|
||||
Friend,
|
||||
Collection,
|
||||
}
|
||||
|
||||
export enum LinkState {
|
||||
Pass,
|
||||
Audit,
|
||||
Outdate,
|
||||
Banned,
|
||||
Reject,
|
||||
}
|
||||
|
||||
export interface LinkModel extends BaseModel {
|
||||
name: string
|
||||
url: string
|
||||
avatar: string
|
||||
description?: string
|
||||
type: LinkType
|
||||
state: LinkState
|
||||
hide: boolean
|
||||
email: string
|
||||
}
|
||||
40
packages/api-client/models/note.ts
Normal file
40
packages/api-client/models/note.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { TextBaseModel } from './base'
|
||||
import { TopicModel } from './topic'
|
||||
|
||||
export interface NoteModel extends TextBaseModel {
|
||||
hide: boolean
|
||||
count: {
|
||||
read: number
|
||||
like: number
|
||||
}
|
||||
|
||||
mood?: string
|
||||
weather?: string
|
||||
hasMemory?: boolean
|
||||
|
||||
secret?: Date
|
||||
password?: string | null
|
||||
nid: number
|
||||
music?: NoteMusicRecord[]
|
||||
location?: string
|
||||
|
||||
coordinates?: Coordinate
|
||||
topic?: TopicModel
|
||||
topicId?: string
|
||||
}
|
||||
|
||||
export interface NoteMusicRecord {
|
||||
type: string
|
||||
id: string
|
||||
}
|
||||
|
||||
export interface Coordinate {
|
||||
latitude: number
|
||||
longitude: number
|
||||
}
|
||||
|
||||
export interface NoteWrappedPayload {
|
||||
data: NoteModel
|
||||
next?: Partial<NoteModel>
|
||||
prev?: Partial<NoteModel>
|
||||
}
|
||||
20
packages/api-client/models/page.ts
Normal file
20
packages/api-client/models/page.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { TextBaseModel } from './base'
|
||||
|
||||
export enum EnumPageType {
|
||||
'md' = 'md',
|
||||
'html' = 'html',
|
||||
'frame' = 'frame',
|
||||
}
|
||||
export interface PageModel extends TextBaseModel {
|
||||
created: string
|
||||
|
||||
slug: string
|
||||
|
||||
subtitle?: string
|
||||
|
||||
order?: number
|
||||
|
||||
type?: EnumPageType
|
||||
|
||||
options?: object
|
||||
}
|
||||
28
packages/api-client/models/post.ts
Normal file
28
packages/api-client/models/post.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Count, Image, TextBaseModel } from './base'
|
||||
import { CategoryModel } from './category'
|
||||
|
||||
export interface PostModel extends TextBaseModel {
|
||||
summary?: string
|
||||
copyright: boolean
|
||||
tags: string[]
|
||||
count: Count
|
||||
text: string
|
||||
title: string
|
||||
slug: string
|
||||
categoryId: string
|
||||
images: Image[]
|
||||
category: CategoryModel
|
||||
pin?: string | null
|
||||
pinOrder?: number
|
||||
related?: Pick<
|
||||
PostModel,
|
||||
| 'id'
|
||||
| 'category'
|
||||
| 'categoryId'
|
||||
| 'created'
|
||||
| 'modified'
|
||||
| 'title'
|
||||
| 'slug'
|
||||
| 'summary'
|
||||
>[]
|
||||
}
|
||||
12
packages/api-client/models/project.ts
Normal file
12
packages/api-client/models/project.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { BaseModel } from './base'
|
||||
|
||||
export interface ProjectModel extends BaseModel {
|
||||
name: string
|
||||
previewUrl?: string
|
||||
docUrl?: string
|
||||
projectUrl?: string
|
||||
images?: string[]
|
||||
description: string
|
||||
avatar?: string
|
||||
text: string
|
||||
}
|
||||
27
packages/api-client/models/recently.ts
Normal file
27
packages/api-client/models/recently.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { BaseModel } from './base'
|
||||
|
||||
export enum RecentlyRefTypes {
|
||||
Post = 'Post',
|
||||
Note = 'Note',
|
||||
Page = 'Page',
|
||||
}
|
||||
|
||||
export type RecentlyRefType = {
|
||||
title: string
|
||||
url: string
|
||||
}
|
||||
export interface RecentlyModel extends BaseModel {
|
||||
content: string
|
||||
|
||||
ref?: RecentlyRefType & { [key: string]: any }
|
||||
refId?: string
|
||||
refType?: RecentlyRefTypes
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
project?: string
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
language?: string
|
||||
}
|
||||
7
packages/api-client/models/say.ts
Normal file
7
packages/api-client/models/say.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { BaseModel } from './base'
|
||||
|
||||
export interface SayModel extends BaseModel {
|
||||
text: string
|
||||
source?: string
|
||||
author?: string
|
||||
}
|
||||
68
packages/api-client/models/setting.ts
Normal file
68
packages/api-client/models/setting.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
export declare class SeoOptionModel {
|
||||
title: string
|
||||
description: string
|
||||
icon?: string
|
||||
keywords?: string[]
|
||||
}
|
||||
export declare class UrlOptionModel {
|
||||
webUrl: string
|
||||
adminUrl: string
|
||||
serverUrl: string
|
||||
wsUrl: string
|
||||
}
|
||||
declare class MailOptionModel {
|
||||
port: number
|
||||
host: string
|
||||
}
|
||||
export declare class MailOptionsModel {
|
||||
enable: boolean
|
||||
user: string
|
||||
pass: string
|
||||
options?: MailOptionModel
|
||||
}
|
||||
export declare class CommentOptionsModel {
|
||||
antiSpam: boolean
|
||||
spamKeywords?: string[]
|
||||
blockIps?: string[]
|
||||
disableNoChinese?: boolean
|
||||
}
|
||||
export declare class BackupOptionsModel {
|
||||
enable: boolean
|
||||
secretId?: string
|
||||
secretKey?: string
|
||||
bucket?: string
|
||||
region: string
|
||||
}
|
||||
export declare class BaiduSearchOptionsModel {
|
||||
enable: boolean
|
||||
token?: string
|
||||
}
|
||||
export declare class AlgoliaSearchOptionsModel {
|
||||
enable: boolean
|
||||
apiKey?: string
|
||||
appId?: string
|
||||
indexName?: string
|
||||
}
|
||||
|
||||
export declare class AdminExtraModel {
|
||||
background?: string
|
||||
|
||||
gaodemapKey?: string
|
||||
title?: string
|
||||
/**
|
||||
* 是否开启后台反代访问
|
||||
*/
|
||||
enableAdminProxy?: boolean
|
||||
}
|
||||
|
||||
export interface IConfig {
|
||||
seo: SeoOptionModel
|
||||
url: UrlOptionModel
|
||||
mailOptions: MailOptionsModel
|
||||
commentOptions: CommentOptionsModel
|
||||
backupOptions: BackupOptionsModel
|
||||
baiduSearchOptions: BaiduSearchOptionsModel
|
||||
algoliaSearchOptions: AlgoliaSearchOptionsModel
|
||||
adminExtra: AdminExtraModel
|
||||
}
|
||||
export declare type IConfigKeys = keyof IConfig
|
||||
19
packages/api-client/models/snippet.ts
Normal file
19
packages/api-client/models/snippet.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { BaseModel } from './base'
|
||||
|
||||
export enum SnippetType {
|
||||
JSON = 'json',
|
||||
Function = 'function',
|
||||
Text = 'text',
|
||||
YAML = 'yaml',
|
||||
}
|
||||
export interface SnippetModel<T = unknown> extends BaseModel {
|
||||
type: SnippetType
|
||||
private: boolean
|
||||
raw: string
|
||||
name: string
|
||||
reference: string
|
||||
comment?: string
|
||||
metatype?: string
|
||||
schema?: string
|
||||
data: T
|
||||
}
|
||||
9
packages/api-client/models/topic.ts
Normal file
9
packages/api-client/models/topic.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { BaseModel } from './base'
|
||||
|
||||
export interface TopicModel extends BaseModel {
|
||||
description?: string
|
||||
introduce: string
|
||||
name: string
|
||||
slug: string
|
||||
icon?: string
|
||||
}
|
||||
27
packages/api-client/models/user.ts
Normal file
27
packages/api-client/models/user.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { BaseModel } from './base'
|
||||
|
||||
export interface UserModel extends BaseModel {
|
||||
introduce: string
|
||||
mail: string
|
||||
url: string
|
||||
name: string
|
||||
socialIds: Record<string, string>
|
||||
username: string
|
||||
modified: string
|
||||
v: number
|
||||
lastLoginTime: string
|
||||
lastLoginIp?: string
|
||||
avatar: string
|
||||
postID: string
|
||||
}
|
||||
|
||||
export type TLogin = {
|
||||
token: string
|
||||
expiresIn: number
|
||||
// 登陆足迹
|
||||
lastLoginTime: null | string
|
||||
lastLoginIp?: null | string
|
||||
} & Pick<
|
||||
UserModel,
|
||||
'name' | 'username' | 'created' | 'url' | 'mail' | 'avatar' | 'id'
|
||||
>
|
||||
98
packages/api-client/package.json
Normal file
98
packages/api-client/package.json
Normal file
@@ -0,0 +1,98 @@
|
||||
{
|
||||
"name": "@mx-space/api-client",
|
||||
"version": "1.0.0-beta.1",
|
||||
"type": "module",
|
||||
"description": "A api client for mx-space server@next",
|
||||
"author": "Innei",
|
||||
"license": "MIT",
|
||||
"main": "dist/index.cjs",
|
||||
"module": "dist/index.js",
|
||||
"types": "types/index.d.ts",
|
||||
"unpkg": "dist/index.umd.min.js",
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
".": [
|
||||
"./types/index.d.ts"
|
||||
],
|
||||
"./adaptors/*": [
|
||||
"./types/adaptors/*.d.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./types/index.d.ts",
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.cjs"
|
||||
},
|
||||
"./package.json": "./package.json",
|
||||
"./adaptors/*": {
|
||||
"types": "./types/adaptors/*.d.ts",
|
||||
"import": {
|
||||
"type": "./types/adaptors/*.d.ts",
|
||||
"default": "./dist/adaptors/*.js"
|
||||
},
|
||||
"require": {
|
||||
"type": "./types/adaptors/*.d.ts",
|
||||
"default": "./dist/adaptors/*.cjs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
}
|
||||
},
|
||||
"bump": {
|
||||
"before": [
|
||||
"git pull --rebase",
|
||||
"pnpm i",
|
||||
"npm run package"
|
||||
],
|
||||
"after": [
|
||||
"npm publish --access=public"
|
||||
]
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"prettier --ignore-path ./.prettierignore --write ",
|
||||
"eslint --cache"
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
"pnpm": ">=6"
|
||||
},
|
||||
"scripts": {
|
||||
"prebuild": "rm -rf lib && rm -rf esm",
|
||||
"build": "tsc --build tsconfig.build.json && tsc --build tsconfig.cjs.json",
|
||||
"postbuild": "tsc-alias -p tsconfig.build.json && tsc-alias -p tsconfig.cjs.json && npm run types",
|
||||
"types": "rm -rf types && tsc --build tsconfig.types.json && tsc-alias -p tsconfig.types.json",
|
||||
"package": "NODE_ENV=production npm run build && rollup -c",
|
||||
"prepackage": "rm -rf dist",
|
||||
"test": "vitest",
|
||||
"dev": "vite"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "22.0.2",
|
||||
"@rollup/plugin-node-resolve": "14.0.1",
|
||||
"@rollup/plugin-typescript": "8.5.0",
|
||||
"@types/cors": "2.8.12",
|
||||
"@types/express": "4.17.15",
|
||||
"@types/lodash": "4.14.186",
|
||||
"abort-controller": "3.0.0",
|
||||
"axios": "*",
|
||||
"camelcase-keys": "*",
|
||||
"cors": "2.8.5",
|
||||
"dts-bundle-generator": "7.0.0",
|
||||
"express": "4.18.2",
|
||||
"isomorphic-unfetch": "3.1.0",
|
||||
"ky": "0.31.3",
|
||||
"lodash": "4.17.21",
|
||||
"node-fetch": "3.2.10",
|
||||
"rollup": "2.79.1",
|
||||
"rollup-plugin-peer-deps-external": "2.2.4",
|
||||
"rollup-plugin-terser": "7.0.2",
|
||||
"tsc-alias": "1.7.0",
|
||||
"umi-request": "1.4.0"
|
||||
}
|
||||
}
|
||||
1668
packages/api-client/pnpm-lock.yaml
generated
Normal file
1668
packages/api-client/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
105
packages/api-client/readme.md
Normal file
105
packages/api-client/readme.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# MApi Client
|
||||
|
||||
这是一个适用于 MServer v3 的 JS SDK,封装了常用接口请求方法以及返回类型的声明,以快速开发前端应用。
|
||||
|
||||
## 迁移到 v1
|
||||
|
||||
不再提供 camelcase-keys 的 re-export,此库不再依赖 camelcase-keys 库,如有需要可自行安装。
|
||||
|
||||
```diff
|
||||
- import { camelcaseKeysDeep, camelcaseKeys } from '@mx-space/api-client'
|
||||
+ import { simpleCamelcaseKeys as camelcaseKeysDeep } from '@mx-space/api-client'
|
||||
```
|
||||
|
||||
## 如何使用
|
||||
|
||||
此 SDK 框架无关,不捆绑任何一个网络请求库,只需要提供适配器。你需要手动传入符合接口标准的适配器。
|
||||
|
||||
此项目提供 `axios` 和 `umi-request` 两个适配器。
|
||||
|
||||
以 `axios` 为例。
|
||||
|
||||
```ts
|
||||
import {
|
||||
AggregateController,
|
||||
CategoryController,
|
||||
NoteController,
|
||||
PostController,
|
||||
allControllers, // ...
|
||||
createClient,
|
||||
} from '@mx-space/api-client'
|
||||
import { axiosAdaptor } from '@mx-space/api-client/adaptors/axios'
|
||||
|
||||
const endpoint = 'https://api.innei.dev/v2'
|
||||
const client = createClient(axiosAdaptor)(endpoint)
|
||||
|
||||
// `default` is AxiosInstance
|
||||
// you can do anything else on this
|
||||
// interceptor or re-configure
|
||||
const $axios = axiosAdaptor.default
|
||||
// re-config (optional)
|
||||
$axios.defaults.timeout = 10000
|
||||
// set interceptors (optional)
|
||||
$axios.interceptors.request.use(
|
||||
(config) => {
|
||||
const token = getToken()
|
||||
if (token) {
|
||||
config.headers!['Authorization'] = 'bearer ' + getToken()
|
||||
}
|
||||
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
if (__DEV__) {
|
||||
console.log(error.message)
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
},
|
||||
)
|
||||
|
||||
// inject controller first.
|
||||
client.injectControllers([
|
||||
PostController,
|
||||
NoteController,
|
||||
AggregateController,
|
||||
CategoryController,
|
||||
])
|
||||
|
||||
// or you can inject allControllers
|
||||
client.injectControllers(allControllers)
|
||||
|
||||
// then you can request `post` `note` and `aggregate` controller
|
||||
|
||||
client.post.post.getList(page, 10, { year }).then((data) => {
|
||||
// do anything
|
||||
})
|
||||
```
|
||||
|
||||
**为什么要手动注入控制器**
|
||||
|
||||
按需加载,可以减少打包体积 (Tree Shake)
|
||||
|
||||
**为什么不依赖请求库**
|
||||
|
||||
可以防止项目中出现两个请求库,减少打包体积
|
||||
|
||||
**如果不使用 axios,应该如何编写适配器**
|
||||
|
||||
参考 `src/adaptors/axios.ts` 和 `src/adaptors/umi-request.ts`
|
||||
|
||||
**如何使用 proxy 来访问 sdk 内未包含的请求**
|
||||
|
||||
如请求 `GET /notes/something/other/123456/info`,可以使用
|
||||
|
||||
```ts
|
||||
client.note.proxy.something.other('123456').info.get()
|
||||
```
|
||||
|
||||
**从 proxy 获取请求地址但不发出**
|
||||
|
||||
```ts
|
||||
client.note.proxy.something.other('123456').info.toString() // /notes/something/other/123456/info
|
||||
|
||||
client.note.proxy.something.other('123456').info.toString(true) // http://localhost:2333/notes/something/other/123456/info
|
||||
```
|
||||
150
packages/api-client/rollup.config.js
Normal file
150
packages/api-client/rollup.config.js
Normal file
@@ -0,0 +1,150 @@
|
||||
// @ts-check
|
||||
import { execSync } from 'child_process'
|
||||
import globby from 'globby'
|
||||
import path, { resolve } from 'path'
|
||||
import peerDepsExternal from 'rollup-plugin-peer-deps-external'
|
||||
import { terser } from 'rollup-plugin-terser'
|
||||
|
||||
import commonjs from '@rollup/plugin-commonjs'
|
||||
import { nodeResolve } from '@rollup/plugin-node-resolve'
|
||||
import typescript from '@rollup/plugin-typescript'
|
||||
|
||||
const packageJson = require('./package.json')
|
||||
|
||||
const umdName = packageJson.name
|
||||
|
||||
const globals = {
|
||||
...packageJson.devDependencies,
|
||||
// @ts-ignore
|
||||
...(packageJson.dependencies || []),
|
||||
}
|
||||
|
||||
const dir = 'dist'
|
||||
|
||||
/**
|
||||
* @type {Partial<import('rollup').RollupOptions>}
|
||||
*/
|
||||
const baseRollupConfig = {
|
||||
plugins: [
|
||||
nodeResolve(),
|
||||
commonjs({ include: 'node_modules/**' }),
|
||||
typescript({ tsconfig: './tsconfig.json', declaration: false }),
|
||||
|
||||
// @ts-ignore
|
||||
peerDepsExternal(),
|
||||
],
|
||||
external: [...Object.keys(globals), 'lodash', 'lodash-es'],
|
||||
treeshake: true,
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {import('rollup').RollupOptions[]}
|
||||
*/
|
||||
const buildAdaptorConfig = () => {
|
||||
const paths = globby.sync('./adaptors/*.ts')
|
||||
const filename = (path_) => path.parse(path_.split('/').pop()).name
|
||||
|
||||
return paths.map((path) => {
|
||||
const libName = filename(path)
|
||||
execSync(
|
||||
`npx dts-bundle-generator -o dist/adaptors/${libName}.d.ts ${resolve(
|
||||
__dirname,
|
||||
'adaptors/',
|
||||
)}/${libName}.ts` + ` --external-types ${libName}`,
|
||||
)
|
||||
|
||||
return {
|
||||
input: path,
|
||||
output: [
|
||||
{
|
||||
file: `${dir}/adaptors/${libName}.umd.js`,
|
||||
format: 'umd',
|
||||
sourcemap: true,
|
||||
name: umdName,
|
||||
},
|
||||
{
|
||||
file: `${dir}/adaptors/${libName}.umd.min.js`,
|
||||
format: 'umd',
|
||||
sourcemap: true,
|
||||
name: umdName,
|
||||
plugins: [terser()],
|
||||
},
|
||||
{
|
||||
file: `${dir}/adaptors/${libName}.cjs`,
|
||||
format: 'cjs',
|
||||
sourcemap: true,
|
||||
},
|
||||
{
|
||||
file: `${dir}/adaptors/${libName}.min.cjs`,
|
||||
format: 'cjs',
|
||||
sourcemap: true,
|
||||
plugins: [terser()],
|
||||
},
|
||||
{
|
||||
file: `${dir}/adaptors/${libName}.js`,
|
||||
format: 'es',
|
||||
sourcemap: true,
|
||||
},
|
||||
{
|
||||
file: `${dir}/adaptors/${libName}.min.js`,
|
||||
format: 'es',
|
||||
sourcemap: true,
|
||||
plugins: [terser()],
|
||||
},
|
||||
],
|
||||
...baseRollupConfig,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {import('rollup').RollupOptions[]}
|
||||
*/
|
||||
const config = [
|
||||
{
|
||||
input: './index.ts',
|
||||
|
||||
output: [
|
||||
{
|
||||
file: `${dir}/index.umd.js`,
|
||||
format: 'umd',
|
||||
sourcemap: true,
|
||||
name: umdName,
|
||||
},
|
||||
{
|
||||
file: `${dir}/index.umd.min.js`,
|
||||
format: 'umd',
|
||||
sourcemap: true,
|
||||
name: umdName,
|
||||
plugins: [terser()],
|
||||
},
|
||||
{
|
||||
file: `${dir}/index.cjs`,
|
||||
format: 'cjs',
|
||||
sourcemap: true,
|
||||
},
|
||||
{
|
||||
file: `${dir}/index.min.cjs`,
|
||||
format: 'cjs',
|
||||
sourcemap: true,
|
||||
plugins: [terser()],
|
||||
},
|
||||
{
|
||||
file: `${dir}/index.js`,
|
||||
format: 'es',
|
||||
sourcemap: true,
|
||||
},
|
||||
{
|
||||
file: `${dir}/index.min.js`,
|
||||
format: 'es',
|
||||
sourcemap: true,
|
||||
plugins: [terser()],
|
||||
},
|
||||
],
|
||||
...baseRollupConfig,
|
||||
},
|
||||
...buildAdaptorConfig(),
|
||||
]
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default config
|
||||
3
packages/api-client/test.d.ts
vendored
Normal file
3
packages/api-client/test.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
import 'vitest/globals'
|
||||
|
||||
export {}
|
||||
4
packages/api-client/tsconfig.build.json
Normal file
4
packages/api-client/tsconfig.build.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["__tests__/**/*.ts"]
|
||||
}
|
||||
7
packages/api-client/tsconfig.cjs.json
Normal file
7
packages/api-client/tsconfig.cjs.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "./tsconfig.build.json",
|
||||
"compilerOptions": {
|
||||
"module": "CommonJS",
|
||||
"outDir": "./lib"
|
||||
}
|
||||
}
|
||||
22
packages/api-client/tsconfig.json
Normal file
22
packages/api-client/tsconfig.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"outDir": "./esm",
|
||||
"baseUrl": ".",
|
||||
"jsx": "react",
|
||||
"target": "ES2020",
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"strict": true,
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"sourceMap": true,
|
||||
"paths": {
|
||||
"~/*": ["*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["esm/*", "build/*", "node_modules/*", "lib/*"]
|
||||
}
|
||||
15
packages/api-client/tsconfig.types.json
Normal file
15
packages/api-client/tsconfig.types.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"rootDir": ".",
|
||||
"outDir": "types",
|
||||
"declaration": true,
|
||||
"declarationMap": false,
|
||||
"isolatedModules": false,
|
||||
"noEmit": false,
|
||||
"allowJs": false,
|
||||
"emitDeclarationOnly": true
|
||||
},
|
||||
"exclude": ["__tests__/**/*", "**/*.test.ts"]
|
||||
}
|
||||
48
packages/api-client/utils/auto-bind.ts
Normal file
48
packages/api-client/utils/auto-bind.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
// @ts-nocheck
|
||||
// @copy: https://github.com/sindresorhus/auto-bind/blob/main/index.js
|
||||
|
||||
// Gets all non-builtin properties up the prototype chain.
|
||||
const getAllProperties = (object) => {
|
||||
const properties = new Set()
|
||||
|
||||
do {
|
||||
for (const key of Reflect.ownKeys(object)) {
|
||||
properties.add([object, key])
|
||||
}
|
||||
} while (
|
||||
(object = Reflect.getPrototypeOf(object)) &&
|
||||
object !== Object.prototype
|
||||
)
|
||||
|
||||
return properties
|
||||
}
|
||||
|
||||
export function autoBind(self, { include, exclude } = {}) {
|
||||
const filter = (key) => {
|
||||
const match = (pattern) =>
|
||||
typeof pattern === 'string' ? key === pattern : pattern.test(key)
|
||||
|
||||
if (include) {
|
||||
return include.some(match)
|
||||
}
|
||||
|
||||
if (exclude) {
|
||||
return !exclude.some(match)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
for (const [object, key] of getAllProperties(self.constructor.prototype)) {
|
||||
if (key === 'constructor' || !filter(key)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const descriptor = Reflect.getOwnPropertyDescriptor(object, key)
|
||||
if (descriptor && typeof descriptor.value === 'function') {
|
||||
self[key] = self[key].bind(self)
|
||||
}
|
||||
}
|
||||
|
||||
return self
|
||||
}
|
||||
26
packages/api-client/utils/camelcase-keys.ts
Normal file
26
packages/api-client/utils/camelcase-keys.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { isPlainObject } from '.'
|
||||
|
||||
/**
|
||||
* A simple camelCase function that only handles strings, but not handling symbol, date, or other complex case.
|
||||
* If you need to handle more complex cases, please use camelcase-keys package.
|
||||
*/
|
||||
export const camelcaseKeys = <T = any>(obj: any): T => {
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map((x) => camelcaseKeys(x)) as any
|
||||
}
|
||||
|
||||
if (isPlainObject(obj)) {
|
||||
return Object.keys(obj).reduce((result: any, key) => {
|
||||
result[camelcase(key)] = camelcaseKeys(obj[key])
|
||||
return result
|
||||
}, {}) as any
|
||||
}
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
export function camelcase(str: string) {
|
||||
return str.replace(/([-_][a-z])/gi, ($1) => {
|
||||
return $1.toUpperCase().replace('-', '').replace('_', '')
|
||||
})
|
||||
}
|
||||
53
packages/api-client/utils/index.ts
Normal file
53
packages/api-client/utils/index.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { SortOrder } from '~/interfaces/options'
|
||||
|
||||
export const isPlainObject = (obj: any) =>
|
||||
isObject(obj) &&
|
||||
Object.prototype.toString.call(obj) === '[object Object]' &&
|
||||
Object.getPrototypeOf(obj) === Object.prototype
|
||||
|
||||
export const sortOrderToNumber = (order: SortOrder) => {
|
||||
return (
|
||||
{
|
||||
asc: 1,
|
||||
desc: -1,
|
||||
}[order] || 1
|
||||
)
|
||||
}
|
||||
const isObject = (obj: any) => obj && typeof obj === 'object'
|
||||
export const destructureData = (payload: any) => {
|
||||
if (typeof payload !== 'object') {
|
||||
return payload
|
||||
}
|
||||
if (payload === null) {
|
||||
return payload
|
||||
}
|
||||
|
||||
const data = payload.data
|
||||
|
||||
const dataIsPlainObject = isPlainObject(data)
|
||||
|
||||
if (dataIsPlainObject && Object.keys(payload).length === 1) {
|
||||
const d = Object.assign({}, data)
|
||||
// attach raw onto new data
|
||||
attachRawFromOneToAnthor(payload, d)
|
||||
return d
|
||||
}
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
export const attachRawFromOneToAnthor = (from: any, to: any) => {
|
||||
if (!from || !isObject(to)) {
|
||||
return
|
||||
}
|
||||
from.$raw &&
|
||||
Object.defineProperty(to, '$raw', {
|
||||
value: { ...from.$raw },
|
||||
enumerable: false,
|
||||
})
|
||||
from.$request &&
|
||||
Object.defineProperty(to, '$request', {
|
||||
value: { ...from.$request },
|
||||
enumerable: false,
|
||||
})
|
||||
}
|
||||
6
packages/api-client/utils/path.ts
Normal file
6
packages/api-client/utils/path.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export const resolveFullPath = (endpoint: string, path: string) => {
|
||||
if (!path.startsWith('/')) {
|
||||
path = `/${path}`
|
||||
}
|
||||
return `${endpoint}${path}`
|
||||
}
|
||||
14
packages/api-client/vitest.config.ts
Normal file
14
packages/api-client/vitest.config.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import tsPath from 'vite-tsconfig-paths'
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
|
||||
include: ['__tests__/**/*.(spec|test).ts'],
|
||||
},
|
||||
|
||||
// @ts-ignore
|
||||
plugins: [tsPath()],
|
||||
})
|
||||
1077
pnpm-lock.yaml
generated
1077
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
2
pnpm-workspace.yaml
Normal file
2
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
packages:
|
||||
- packages/*
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user