feat(client,sdk): improve api client (#425)
* feat(client,sdk): improve api client * feat: add test cases * docs: update doc * fix(sdk): cannot destructure property 'authClass' of 'instance' as it is undefined
This commit is contained in:
parent
735581d20d
commit
4412efc145
@ -34,7 +34,6 @@ Request Body
|
||||
Response 200 (application/json)
|
||||
{
|
||||
"data": {},
|
||||
"meta": {}
|
||||
}
|
||||
```
|
||||
|
||||
@ -51,7 +50,6 @@ Request Body
|
||||
Response 200 (application/json)
|
||||
{
|
||||
"data": {},
|
||||
"meta": {}
|
||||
}
|
||||
```
|
||||
|
||||
@ -75,8 +73,7 @@ Response 200 (application/json)
|
||||
"user": {
|
||||
"id": 1
|
||||
}
|
||||
},
|
||||
"meta": {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -117,8 +114,7 @@ Response 200 (application/json)
|
||||
"id": 2
|
||||
}
|
||||
}
|
||||
],
|
||||
"meta": {}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -37,8 +37,8 @@
|
||||
|
||||
## boolean
|
||||
|
||||
- $truthy
|
||||
- $falsy
|
||||
- $isTruthy
|
||||
- $isFalsy
|
||||
|
||||
## date
|
||||
|
||||
|
@ -280,13 +280,7 @@ Response 200 (application/json)
|
||||
{
|
||||
data: {
|
||||
id: 1
|
||||
},
|
||||
meta: {
|
||||
count: 1
|
||||
page: 1,
|
||||
pageSize: 1,
|
||||
totalPage: 1
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -52,15 +52,67 @@ mock.onGet('users:get').reply(200, {
|
||||
await api.request({ url: 'users:get' });
|
||||
```
|
||||
|
||||
## Storage
|
||||
|
||||
APIClient uses localStorage by default, you can also custom storage.
|
||||
|
||||
```ts
|
||||
import { Storage } from '@nocobase/sdk';
|
||||
|
||||
class MemoryStorage extends Storage {
|
||||
items = new Map();
|
||||
|
||||
clear() {
|
||||
this.items.clear();
|
||||
}
|
||||
|
||||
getItem(key: string) {
|
||||
return this.items.get(key);
|
||||
}
|
||||
|
||||
setItem(key: string, value: string) {
|
||||
return this.items.set(key, value);
|
||||
}
|
||||
|
||||
removeItem(key: string) {
|
||||
return this.items.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
const api = new APIClient({
|
||||
baseURL: 'https://localhost:8000/api',
|
||||
storageClass: CustomStorage,
|
||||
});
|
||||
```
|
||||
|
||||
## Auth
|
||||
|
||||
```ts
|
||||
// Pass token directly
|
||||
api.auth.token = '123';
|
||||
// Or sign in via signIn
|
||||
api.auth.signIn();
|
||||
// Log out and delete the token cache
|
||||
// sign in and remember the current token
|
||||
api.auth.signIn({ email, password });
|
||||
// sign out and delete the token
|
||||
api.auth.signOut();
|
||||
// set the token
|
||||
api.auth.setToken('123');
|
||||
// set the role (multiple roles)
|
||||
api.auth.setRole('admin');
|
||||
// set the locale (multiple languages)
|
||||
api.auth.setLocale('zh-CN');
|
||||
```
|
||||
|
||||
Custom Auth
|
||||
|
||||
```ts
|
||||
import { Auth } from '@nocobase/sdk';
|
||||
|
||||
class CustomAuth extends Auth {
|
||||
|
||||
}
|
||||
|
||||
const api = new APIClient({
|
||||
baseURL: 'https://localhost:8000/api',
|
||||
authClass: CustomAuth,
|
||||
});
|
||||
```
|
||||
|
||||
## Request
|
||||
|
@ -1,5 +1,31 @@
|
||||
# Release Notes
|
||||
|
||||
## 2022/05/26 ~ v0.7.0-alpha.82
|
||||
|
||||
- feat(client,sdk): improve api client
|
||||
|
||||
### Breaking Change
|
||||
|
||||
There are major changes to the `@nocobase/sdk` API, see details [JavaScript SDK](./development/http-api/javascript-sdk.md)
|
||||
|
||||
## 2022/05/25 ~ v0.7.0-alpha.81
|
||||
|
||||
- feat: add create-plugin command (#423)
|
||||
- fix: "typescript": "4.5.5"
|
||||
- docs: update documentation
|
||||
- fix(client): filter menu item schema by permissions
|
||||
- fix(database): cannot read properties of null (reading 'substring')
|
||||
- fix(client): add description
|
||||
- fix(client): clone schema before insert
|
||||
- feat(client): add a description to the junction collection field
|
||||
- fix(devtools): unexpected token '.'
|
||||
|
||||
## 2022/05/24 ~ v0.7.0-alpha.78
|
||||
|
||||
- fix(client): add RemoteDocumentTitleProvider
|
||||
- fix(client): incomplete calendar events
|
||||
- fix(plugin-users): add translations (#416)
|
||||
|
||||
## 2022/05/23 ~ v0.7.0-alpha.59
|
||||
|
||||
- feat(docs): add alert message
|
||||
|
@ -34,7 +34,6 @@ Request Body
|
||||
Response 200 (application/json)
|
||||
{
|
||||
"data": {},
|
||||
"meta": {}
|
||||
}
|
||||
```
|
||||
|
||||
@ -50,8 +49,7 @@ Request Body
|
||||
|
||||
Response 200 (application/json)
|
||||
{
|
||||
"data": {},
|
||||
"meta": {}
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
@ -75,8 +73,7 @@ Response 200 (application/json)
|
||||
"user": {
|
||||
"id": 1
|
||||
}
|
||||
},
|
||||
"meta": {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -117,8 +114,7 @@ Response 200 (application/json)
|
||||
"id": 2
|
||||
}
|
||||
}
|
||||
],
|
||||
"meta": {}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
@ -141,6 +137,3 @@ Response 200 (application/json)
|
||||
### `remove`
|
||||
|
||||
### `toggle`
|
||||
|
||||
|
||||
|
||||
|
@ -37,8 +37,8 @@
|
||||
|
||||
## boolean
|
||||
|
||||
- $truthy
|
||||
- $falsy
|
||||
- $isTruly
|
||||
- $isFalsy
|
||||
|
||||
## date
|
||||
|
||||
|
@ -281,12 +281,6 @@ Response 200 (application/json)
|
||||
data: {
|
||||
id: 1
|
||||
},
|
||||
meta: {
|
||||
count: 1
|
||||
page: 1,
|
||||
pageSize: 1,
|
||||
totalPage: 1
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -52,15 +52,67 @@ mock.onGet('users:get').reply(200, {
|
||||
await api.request({ url: 'users:get' });
|
||||
```
|
||||
|
||||
## Storage
|
||||
|
||||
APIClient 默认使用的 localStorage,你也可以自定义 Storage,如:
|
||||
|
||||
```ts
|
||||
import { Storage } from '@nocobase/sdk';
|
||||
|
||||
class MemoryStorage extends Storage {
|
||||
items = new Map();
|
||||
|
||||
clear() {
|
||||
this.items.clear();
|
||||
}
|
||||
|
||||
getItem(key: string) {
|
||||
return this.items.get(key);
|
||||
}
|
||||
|
||||
setItem(key: string, value: string) {
|
||||
return this.items.set(key, value);
|
||||
}
|
||||
|
||||
removeItem(key: string) {
|
||||
return this.items.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
const api = new APIClient({
|
||||
baseURL: 'https://localhost:8000/api',
|
||||
storageClass: CustomStorage,
|
||||
});
|
||||
```
|
||||
|
||||
## Auth
|
||||
|
||||
```ts
|
||||
// 直接传 token
|
||||
api.auth.token = '123';
|
||||
// 或者通过 signIn 登录
|
||||
api.auth.signIn();
|
||||
// 注销并删除 token 缓存
|
||||
// 登录并记录 token
|
||||
api.auth.signIn({ email, password });
|
||||
// 注销并删除 token
|
||||
api.auth.signOut();
|
||||
// 设置 token
|
||||
api.auth.setToken('123');
|
||||
// 设置 role(当需要多角色时)
|
||||
api.auth.setRole('admin');
|
||||
// 设置 locale(当需要多语言时)
|
||||
api.auth.setLocale('zh-CN');
|
||||
```
|
||||
|
||||
自定义 Auth
|
||||
|
||||
```ts
|
||||
import { Auth } from '@nocobase/sdk';
|
||||
|
||||
class CustomAuth extends Auth {
|
||||
|
||||
}
|
||||
|
||||
const api = new APIClient({
|
||||
baseURL: 'https://localhost:8000/api',
|
||||
authClass: CustomAuth,
|
||||
});
|
||||
```
|
||||
|
||||
## Request
|
||||
|
@ -1,5 +1,31 @@
|
||||
# 更新日志
|
||||
|
||||
## 2022/05/26 ~ v0.7.0-alpha.82
|
||||
|
||||
- feat(client,sdk): improve api client
|
||||
|
||||
### Breaking Change
|
||||
|
||||
There are major changes to the `APIClient` API, see details [JavaScript SDK](./development/http-api/javascript-sdk.md)
|
||||
|
||||
## 2022/05/25 ~ v0.7.0-alpha.81
|
||||
|
||||
- feat: add create-plugin command (#423)
|
||||
- fix: "typescript": "4.5.5"
|
||||
- docs: update documentation
|
||||
- fix(client): filter menu item schema by permissions
|
||||
- fix(database): cannot read properties of null (reading 'substring')
|
||||
- fix(client): add description
|
||||
- fix(client): clone schema before insert
|
||||
- feat(client): add a description to the junction collection field
|
||||
- fix(devtools): unexpected token '.'
|
||||
|
||||
## 2022/05/24 ~ v0.7.0-alpha.78
|
||||
|
||||
- fix(client): add RemoteDocumentTitleProvider
|
||||
- fix(client): incomplete calendar events
|
||||
- fix(plugin-users): add translations (#416)
|
||||
|
||||
## 2022/05/23 ~ v0.7.0-alpha.62
|
||||
|
||||
- feat(docs): add alert message
|
||||
|
@ -2,6 +2,9 @@ import { APIClient } from '@nocobase/client';
|
||||
|
||||
const apiClient = new APIClient({
|
||||
baseURL: process.env.API_BASE_URL,
|
||||
headers: {
|
||||
'X-Hostname': window?.location?.hostname,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiClient;
|
||||
|
@ -18,6 +18,7 @@
|
||||
"@formily/antd": "2.0.20",
|
||||
"@formily/core": "2.0.20",
|
||||
"@formily/react": "2.0.20",
|
||||
"@nocobase/sdk": "0.7.0-alpha.81",
|
||||
"@nocobase/utils": "0.7.0-alpha.81",
|
||||
"ahooks": "^3.0.5",
|
||||
"antd": "~4.19.5",
|
||||
|
@ -1,9 +1,8 @@
|
||||
import { useFieldSchema } from '@formily/react';
|
||||
import { useCookieState } from 'ahooks';
|
||||
import { Spin } from 'antd';
|
||||
import React, { createContext, useContext } from 'react';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import { useRequest } from '../api-client';
|
||||
import { useAPIClient, useRequest } from '../api-client';
|
||||
import { useCollection } from '../collection-manager';
|
||||
import { useRecordIsOwn } from '../record-provider';
|
||||
import { SchemaComponentOptions, useDesignable } from '../schema-component';
|
||||
@ -22,7 +21,7 @@ export const ACLProvider = (props) => {
|
||||
|
||||
export const ACLRolesCheckProvider = (props) => {
|
||||
const { setDesignable } = useDesignable();
|
||||
const [roleName, setRoleName] = useCookieState('currentRoleName');
|
||||
const api = useAPIClient();
|
||||
const result = useRequest(
|
||||
{
|
||||
url: 'roles:check',
|
||||
@ -32,8 +31,8 @@ export const ACLRolesCheckProvider = (props) => {
|
||||
if (!data?.data?.allowConfigure && !data?.data?.allowAll) {
|
||||
setDesignable(false);
|
||||
}
|
||||
if (data?.data?.role !== roleName) {
|
||||
setRoleName(data?.data?.role);
|
||||
if (data?.data?.role !== api.auth.role) {
|
||||
api.auth.setRole(data?.data?.role);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
@ -3,10 +3,11 @@ import enUS from 'antd/lib/locale/en_US';
|
||||
import zhCN from 'antd/lib/locale/zh_CN';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useRequest } from '../api-client';
|
||||
import { useAPIClient, useRequest } from '../api-client';
|
||||
|
||||
export function AntdConfigProvider(props) {
|
||||
const { remoteLocale, ...others } = props;
|
||||
const api = useAPIClient();
|
||||
const { i18n } = useTranslation();
|
||||
const { loading } = useRequest(
|
||||
{
|
||||
@ -14,8 +15,9 @@ export function AntdConfigProvider(props) {
|
||||
},
|
||||
{
|
||||
onSuccess(data) {
|
||||
const locale = localStorage.getItem('NOCOBASE_LANG');
|
||||
const locale = api.auth.locale;
|
||||
if (data?.data?.lang && !locale) {
|
||||
api.auth.setLocale(data?.data?.lang);
|
||||
i18n.changeLanguage(data?.data?.lang);
|
||||
}
|
||||
},
|
||||
|
@ -1,124 +1,6 @@
|
||||
import { observable } from '@formily/reactive';
|
||||
import { APIClient as APIClientSDK } from '@nocobase/sdk';
|
||||
import { Result } from 'ahooks/lib/useRequest/src/types';
|
||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||
import Cookies from 'js-cookie';
|
||||
import qs from 'qs';
|
||||
|
||||
export interface ActionParams {
|
||||
filterByTk?: any;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
type ResourceActionOptions<P = any> = {
|
||||
resource?: string;
|
||||
resourceOf?: any;
|
||||
action?: string;
|
||||
params?: P;
|
||||
};
|
||||
|
||||
export interface IResource {
|
||||
list?: (params?: ActionParams) => Promise<any>;
|
||||
get?: (params?: ActionParams) => Promise<any>;
|
||||
create?: (params?: ActionParams) => Promise<any>;
|
||||
update?: (params?: ActionParams) => Promise<any>;
|
||||
destroy?: (params?: ActionParams) => Promise<any>;
|
||||
[key: string]: (params?: ActionParams) => Promise<any>;
|
||||
}
|
||||
|
||||
export class APIClient {
|
||||
axios: AxiosInstance;
|
||||
|
||||
export class APIClient extends APIClientSDK {
|
||||
services: Record<string, Result<any, any>>;
|
||||
|
||||
tokenKey = 'NOCOBASE_TOKEN';
|
||||
|
||||
constructor(instance?: AxiosInstance | AxiosRequestConfig) {
|
||||
this.services = observable({});
|
||||
if (typeof instance === 'function') {
|
||||
this.axios = instance;
|
||||
} else {
|
||||
this.axios = axios.create(instance);
|
||||
}
|
||||
this.qsMiddleware();
|
||||
this.authMiddleware();
|
||||
}
|
||||
|
||||
qsMiddleware() {
|
||||
this.axios.interceptors.request.use((config) => {
|
||||
config.paramsSerializer = (params) => {
|
||||
return qs.stringify(params, {
|
||||
strictNullHandling: true,
|
||||
arrayFormat: 'brackets',
|
||||
});
|
||||
};
|
||||
return config;
|
||||
});
|
||||
}
|
||||
|
||||
// TODO
|
||||
authMiddleware() {
|
||||
this.axios.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem(this.tokenKey);
|
||||
config.headers['X-Locale'] = localStorage.getItem('NOCOBASE_LANG');
|
||||
config.headers['X-Hostname'] = window.location.hostname;
|
||||
if (token) {
|
||||
config.headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
const currentRoleName = Cookies.get('currentRoleName');
|
||||
if (currentRoleName) {
|
||||
config.headers['X-Role'] = currentRoleName;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
}
|
||||
|
||||
// TODO
|
||||
setBearerToken(token: any) {
|
||||
localStorage.setItem(this.tokenKey, token || '');
|
||||
Cookies.remove('currentRoleName');
|
||||
}
|
||||
|
||||
service(uid: string): Result<any, any> {
|
||||
return this.services[uid];
|
||||
}
|
||||
|
||||
request<T = any, R = AxiosResponse<T>, D = any>(config: AxiosRequestConfig<D> | ResourceActionOptions): Promise<R> {
|
||||
const { resource, resourceOf, action, params } = config as any;
|
||||
if (resource) {
|
||||
return this.resource(resource, resourceOf)[action](params);
|
||||
}
|
||||
return this.axios.request<T, R, D>(config);
|
||||
}
|
||||
|
||||
resource(name: string, of?: any): IResource {
|
||||
const target = {};
|
||||
const handler = {
|
||||
get: (_: any, actionName: string) => {
|
||||
let url = name.split('.').join(`/${of || '_'}/`);
|
||||
url += `:${actionName}`;
|
||||
const config: AxiosRequestConfig = { url };
|
||||
if (['get', 'list'].includes(actionName)) {
|
||||
config['method'] = 'get';
|
||||
} else {
|
||||
config['method'] = 'post';
|
||||
}
|
||||
return async (params?: ActionParams) => {
|
||||
const { values, filter, ...others } = params || {};
|
||||
config['params'] = others;
|
||||
if (filter) {
|
||||
if (typeof filter === 'string') {
|
||||
config['params']['filter'] = filter;
|
||||
} else {
|
||||
config['params']['filter'] = JSON.stringify(filter);
|
||||
}
|
||||
}
|
||||
if (config.method !== 'get') {
|
||||
config['data'] = values || {};
|
||||
}
|
||||
return await this.request(config);
|
||||
};
|
||||
},
|
||||
};
|
||||
return new Proxy(target, handler);
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ const log = require('debug')('i18next');
|
||||
export const i18n = i18next.createInstance();
|
||||
|
||||
i18n.use(initReactI18next).init({
|
||||
lng: localStorage.getItem('NOCOBASE_LANG') || 'en-US',
|
||||
lng: localStorage.getItem('NOCOBASE_LOCALE') || 'en-US',
|
||||
// debug: true,
|
||||
defaultNS: 'client',
|
||||
// parseMissingKeyHandler: (key) => {
|
||||
@ -29,9 +29,9 @@ function setMomentLng(language) {
|
||||
moment.locale(lng);
|
||||
}
|
||||
|
||||
setMomentLng(localStorage.getItem('NOCOBASE_LANG'));
|
||||
setMomentLng(localStorage.getItem('NOCOBASE_LOCALE'));
|
||||
|
||||
i18n.on('languageChanged', (lng) => {
|
||||
localStorage.setItem('NOCOBASE_LANG', lng);
|
||||
localStorage.setItem('NOCOBASE_LOCALE', lng);
|
||||
setMomentLng(lng);
|
||||
});
|
||||
|
@ -36,7 +36,7 @@ export const CurrentUser = () => {
|
||||
<Menu.Item
|
||||
onClick={async () => {
|
||||
await api.resource('users').signout();
|
||||
api.setBearerToken(null);
|
||||
api.auth.setToken(null);
|
||||
history.push('/signin');
|
||||
}}
|
||||
>
|
||||
|
@ -34,6 +34,7 @@ export const LanguageSettings = () => {
|
||||
appLang: lang,
|
||||
},
|
||||
});
|
||||
api.auth.setLocale(lang);
|
||||
await i18n.changeLanguage(lang);
|
||||
window.location.reload();
|
||||
}}
|
||||
|
@ -68,13 +68,8 @@ const useSignin = () => {
|
||||
return {
|
||||
async run() {
|
||||
await form.submit();
|
||||
const response = await api.resource('users').signin({
|
||||
values: form.values,
|
||||
});
|
||||
if (response?.data?.data?.token) {
|
||||
api.setBearerToken(response?.data?.data?.token);
|
||||
await api.auth.signIn(form.values);
|
||||
history.push(redirect || '/admin');
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { useCookieState } from 'ahooks';
|
||||
import { Menu, Select } from 'antd';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -30,9 +29,6 @@ export const SwitchRole = () => {
|
||||
const api = useAPIClient();
|
||||
const roles = useCurrentRoles();
|
||||
const { t } = useTranslation();
|
||||
const [roleName, setRoleName] = useCookieState('currentRoleName', {
|
||||
defaultValue: roles?.find((role) => role.default)?.name,
|
||||
});
|
||||
if (roles.length <= 1) {
|
||||
return null;
|
||||
}
|
||||
@ -47,9 +43,9 @@ export const SwitchRole = () => {
|
||||
value: 'name',
|
||||
}}
|
||||
options={roles}
|
||||
value={roleName}
|
||||
value={api.auth.role}
|
||||
onChange={async (roleName) => {
|
||||
setRoleName(roleName);
|
||||
api.auth.setRole(roleName);
|
||||
await api.resource('users').setDefaultRole({ values: { roleName } });
|
||||
window.location.href = '/';
|
||||
}}
|
||||
|
@ -46,11 +46,14 @@ function getUmiConfig() {
|
||||
function resolveNocobasePackagesAlias(config) {
|
||||
const clientSrc = resolve(process.cwd(), './packages/core/client/src');
|
||||
const utilsSrc = resolve(process.cwd(), './packages/core/utils/src');
|
||||
const sdkSrc = resolve(process.cwd(), './packages/core/sdk/src');
|
||||
if (existsSync(clientSrc)) {
|
||||
config.module.rules.get('ts-in-node_modules').include.add(clientSrc);
|
||||
config.resolve.alias.set('@nocobase/client', clientSrc);
|
||||
config.module.rules.get('ts-in-node_modules').include.add(utilsSrc);
|
||||
config.resolve.alias.set('@nocobase/utils', utilsSrc);
|
||||
config.module.rules.get('ts-in-node_modules').include.add(sdkSrc);
|
||||
config.resolve.alias.set('@nocobase/sdk', sdkSrc);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -12,7 +12,8 @@
|
||||
"module": "es/index.js",
|
||||
"typings": "es/index.d.ts",
|
||||
"dependencies": {
|
||||
"axios": "^0.26.1"
|
||||
"axios": "^0.26.1",
|
||||
"qs": "^6.10.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"axios-mock-adapter": "^1.20.0"
|
||||
|
@ -22,15 +22,51 @@ export interface IResource {
|
||||
[key: string]: (params?: ActionParams) => Promise<any>;
|
||||
}
|
||||
|
||||
class Auth {
|
||||
export class Auth {
|
||||
protected api: APIClient;
|
||||
protected token: string;
|
||||
protected role: string;
|
||||
|
||||
protected options = {
|
||||
token: null,
|
||||
locale: null,
|
||||
role: null,
|
||||
};
|
||||
|
||||
constructor(api: APIClient) {
|
||||
this.api = api;
|
||||
this.api.axios.interceptors.request.use((config) => {
|
||||
config.headers['X-Hostname'] = window.location.hostname;
|
||||
this.locale = this.getLocale();
|
||||
this.role = this.getRole();
|
||||
this.token = this.getToken();
|
||||
this.api.axios.interceptors.request.use(this.middleware.bind(this));
|
||||
}
|
||||
|
||||
get locale() {
|
||||
return this.getLocale();
|
||||
}
|
||||
|
||||
get role() {
|
||||
return this.getRole();
|
||||
}
|
||||
|
||||
get token() {
|
||||
return this.getToken();
|
||||
}
|
||||
|
||||
set locale(value) {
|
||||
this.setLocale(value);
|
||||
}
|
||||
|
||||
set role(value) {
|
||||
this.setRole(value);
|
||||
}
|
||||
|
||||
set token(value) {
|
||||
this.setToken(value);
|
||||
}
|
||||
|
||||
middleware(config: AxiosRequestConfig) {
|
||||
if (this.locale) {
|
||||
config.headers['X-Locale'] = this.locale;
|
||||
}
|
||||
if (this.role) {
|
||||
config.headers['X-Role'] = this.role;
|
||||
}
|
||||
@ -38,26 +74,48 @@ class Auth {
|
||||
config.headers['Authorization'] = `Bearer ${this.token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
}
|
||||
|
||||
getLocale() {
|
||||
return this.api.storage.getItem('NOCOBASE_LOCALE');
|
||||
}
|
||||
|
||||
setLocale(locale: string) {
|
||||
this.options.locale = locale;
|
||||
this.api.storage.setItem('NOCOBASE_LOCALE', locale || '');
|
||||
}
|
||||
|
||||
getToken() {
|
||||
return this.api.storage.getItem('NOCOBASE_TOKEN');
|
||||
}
|
||||
|
||||
setToken(token: string) {
|
||||
this.token = token;
|
||||
this.options.token = token;
|
||||
this.api.storage.setItem('NOCOBASE_TOKEN', token || '');
|
||||
if (!token) {
|
||||
this.setRole(null);
|
||||
this.setLocale(null);
|
||||
}
|
||||
}
|
||||
|
||||
getRole() {
|
||||
return this.api.storage.getItem('NOCOBASE_ROLE');
|
||||
}
|
||||
|
||||
setRole(role: string) {
|
||||
this.role = role;
|
||||
this.options.role = role;
|
||||
this.api.storage.setItem('NOCOBASE_ROLE', role || '');
|
||||
}
|
||||
|
||||
async signIn(values) {
|
||||
async signIn(values): Promise<AxiosResponse<any>> {
|
||||
const response = await this.api.request({
|
||||
method: 'post',
|
||||
url: 'users:signin',
|
||||
data: values,
|
||||
});
|
||||
const data = response?.data?.data;
|
||||
this.token = data;
|
||||
return data;
|
||||
this.setToken(data?.token);
|
||||
return response;
|
||||
}
|
||||
|
||||
async signOut() {
|
||||
@ -65,25 +123,78 @@ class Auth {
|
||||
method: 'post',
|
||||
url: 'users:signout',
|
||||
});
|
||||
this.token = null;
|
||||
this.setToken(null);
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class Storage {
|
||||
abstract clear(): void;
|
||||
abstract getItem(key: string): string | null;
|
||||
abstract removeItem(key: string): void;
|
||||
abstract setItem(key: string, value: string): void;
|
||||
}
|
||||
|
||||
export class MemoryStorage extends Storage {
|
||||
items = new Map();
|
||||
|
||||
clear() {
|
||||
this.items.clear();
|
||||
}
|
||||
|
||||
getItem(key: string) {
|
||||
return this.items.get(key);
|
||||
}
|
||||
|
||||
setItem(key: string, value: string) {
|
||||
return this.items.set(key, value);
|
||||
}
|
||||
|
||||
removeItem(key: string) {
|
||||
return this.items.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
interface ExtendedOptions {
|
||||
authClass?: any;
|
||||
storageClass?: any;
|
||||
}
|
||||
|
||||
export class APIClient {
|
||||
axios: AxiosInstance;
|
||||
auth: Auth;
|
||||
storage: Storage;
|
||||
|
||||
constructor(instance?: AxiosInstance | AxiosRequestConfig) {
|
||||
constructor(instance?: AxiosInstance | (AxiosRequestConfig & ExtendedOptions)) {
|
||||
if (typeof instance === 'function') {
|
||||
this.axios = instance;
|
||||
} else {
|
||||
this.axios = axios.create(instance);
|
||||
const { authClass, storageClass, ...others } = instance || {};
|
||||
this.axios = axios.create(others);
|
||||
this.initStorage(storageClass);
|
||||
if (authClass) {
|
||||
this.auth = new authClass(this);
|
||||
}
|
||||
}
|
||||
if (!this.storage) {
|
||||
this.initStorage();
|
||||
}
|
||||
if (!this.auth) {
|
||||
this.auth = new Auth(this);
|
||||
this.qsMiddleware();
|
||||
}
|
||||
this.paramsSerializer();
|
||||
}
|
||||
|
||||
qsMiddleware() {
|
||||
private initStorage(storage?: any) {
|
||||
if (storage) {
|
||||
this.storage = new storage(this);
|
||||
} else if (localStorage) {
|
||||
this.storage = localStorage;
|
||||
} else {
|
||||
this.storage = new MemoryStorage();
|
||||
}
|
||||
}
|
||||
|
||||
paramsSerializer() {
|
||||
this.axios.interceptors.request.use((config) => {
|
||||
config.paramsSerializer = (params) => {
|
||||
return qs.stringify(params, {
|
||||
|
116
packages/core/sdk/src/__tests__/api-client.test.ts
Normal file
116
packages/core/sdk/src/__tests__/api-client.test.ts
Normal file
@ -0,0 +1,116 @@
|
||||
import { AxiosResponse } from 'axios';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { APIClient, Storage } from '../';
|
||||
import { Auth } from '../APIClient';
|
||||
|
||||
describe('api-client', () => {
|
||||
test('instance', async () => {
|
||||
const api = new APIClient({
|
||||
baseURL: 'https://localhost:8000/api',
|
||||
});
|
||||
const mock = new MockAdapter(api.axios);
|
||||
mock.onGet('users:get').reply(200, {
|
||||
data: { id: 1, name: 'John Smith' },
|
||||
});
|
||||
const response = await api.request({ url: 'users:get' });
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.data).toMatchObject({
|
||||
data: { id: 1, name: 'John Smith' },
|
||||
});
|
||||
});
|
||||
|
||||
test('signIn', async () => {
|
||||
const api = new APIClient({
|
||||
baseURL: 'https://localhost:8000/api',
|
||||
});
|
||||
const mock = new MockAdapter(api.axios);
|
||||
mock.onPost('users:signin').reply(200, {
|
||||
data: { id: 1, name: 'John Smith', token: '123' },
|
||||
});
|
||||
const response = await api.auth.signIn({});
|
||||
expect(response.status).toBe(200);
|
||||
expect(api.auth.token).toBe('123');
|
||||
expect(localStorage.getItem('NOCOBASE_TOKEN')).toBe('123');
|
||||
});
|
||||
|
||||
test('resource action', async () => {
|
||||
const api = new APIClient({
|
||||
baseURL: 'https://localhost:8000/api',
|
||||
});
|
||||
const mock = new MockAdapter(api.axios);
|
||||
mock.onPost('users:test').reply(200, {
|
||||
data: { id: 1, name: 'John Smith', token: '123' },
|
||||
});
|
||||
const response = await api.resource('users').test();
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.data).toMatchObject({
|
||||
data: { id: 1, name: 'John Smith', token: '123' },
|
||||
});
|
||||
});
|
||||
|
||||
test('custom storage', async () => {
|
||||
const items = new Map();
|
||||
|
||||
class TestStorage extends Storage {
|
||||
clear() {
|
||||
items.clear();
|
||||
}
|
||||
getItem(key: string) {
|
||||
return items.get(key);
|
||||
}
|
||||
setItem(key: string, value: string) {
|
||||
return items.set(key, value);
|
||||
}
|
||||
removeItem(key: string) {
|
||||
return items.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
const api = new APIClient({
|
||||
baseURL: 'https://localhost:8000/api',
|
||||
storageClass: TestStorage,
|
||||
});
|
||||
|
||||
const mock = new MockAdapter(api.axios);
|
||||
mock.onPost('users:signin').reply(200, {
|
||||
data: { id: 1, name: 'John Smith', token: '123' },
|
||||
});
|
||||
|
||||
const response = await api.auth.signIn({});
|
||||
expect(response.status).toBe(200);
|
||||
expect(api.auth.token).toBe('123');
|
||||
expect(items.get('NOCOBASE_TOKEN')).toBe('123');
|
||||
});
|
||||
|
||||
test('custom auth', async () => {
|
||||
class TestAuth extends Auth {
|
||||
async signIn(values: any): Promise<AxiosResponse<any, any>> {
|
||||
const response = await this.api.request({
|
||||
method: 'post',
|
||||
url: 'users:test',
|
||||
data: values,
|
||||
});
|
||||
const data = response?.data?.data;
|
||||
this.setToken(data?.token);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
const api = new APIClient({
|
||||
baseURL: 'https://localhost:8000/api',
|
||||
authClass: TestAuth,
|
||||
});
|
||||
|
||||
expect(api.auth).toBeInstanceOf(TestAuth);
|
||||
|
||||
const mock = new MockAdapter(api.axios);
|
||||
mock.onPost('users:test').reply(200, {
|
||||
data: { id: 1, name: 'John Smith', token: '123' },
|
||||
});
|
||||
|
||||
const response = await api.auth.signIn({});
|
||||
expect(response.status).toBe(200);
|
||||
expect(api.auth.token).toBe('123');
|
||||
expect(localStorage.getItem('NOCOBASE_TOKEN')).toBe('123');
|
||||
});
|
||||
});
|
@ -17596,6 +17596,13 @@ q@^1.1.2, q@^1.5.1:
|
||||
resolved "https://registry.npmjs.org/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
|
||||
integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
|
||||
|
||||
qs@^6.10.1:
|
||||
version "6.10.3"
|
||||
resolved "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz#d6cde1b2ffca87b5aa57889816c5f81535e22e8e"
|
||||
integrity sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==
|
||||
dependencies:
|
||||
side-channel "^1.0.4"
|
||||
|
||||
qs@^6.4.0, qs@^6.5.2, qs@^6.9.4:
|
||||
version "6.10.1"
|
||||
resolved "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz#4931482fa8d647a5aab799c5271d2133b981fb6a"
|
||||
|
Loading…
Reference in New Issue
Block a user