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:
chenos 2022-05-27 00:00:59 +08:00 committed by GitHub
parent 735581d20d
commit 4412efc145
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 463 additions and 213 deletions

View File

@ -34,7 +34,6 @@ Request Body
Response 200 (application/json) Response 200 (application/json)
{ {
"data": {}, "data": {},
"meta": {}
} }
``` ```
@ -51,7 +50,6 @@ Request Body
Response 200 (application/json) Response 200 (application/json)
{ {
"data": {}, "data": {},
"meta": {}
} }
``` ```
@ -75,8 +73,7 @@ Response 200 (application/json)
"user": { "user": {
"id": 1 "id": 1
} }
}, }
"meta": {}
} }
``` ```
@ -117,8 +114,7 @@ Response 200 (application/json)
"id": 2 "id": 2
} }
} }
], ]
"meta": {}
} }
``` ```

View File

@ -37,8 +37,8 @@
## boolean ## boolean
- $truthy - $isTruthy
- $falsy - $isFalsy
## date ## date

View File

@ -280,13 +280,7 @@ Response 200 (application/json)
{ {
data: { data: {
id: 1 id: 1
}, }
meta: {
count: 1
page: 1,
pageSize: 1,
totalPage: 1
},
} }
``` ```

View File

@ -52,15 +52,67 @@ mock.onGet('users:get').reply(200, {
await api.request({ url: 'users:get' }); 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 ## Auth
```ts ```ts
// Pass token directly // sign in and remember the current token
api.auth.token = '123'; api.auth.signIn({ email, password });
// Or sign in via signIn // sign out and delete the token
api.auth.signIn();
// Log out and delete the token cache
api.auth.signOut(); 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 ## Request

View File

@ -1,5 +1,31 @@
# Release Notes # 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 ## 2022/05/23 ~ v0.7.0-alpha.59
- feat(docs): add alert message - feat(docs): add alert message

View File

@ -34,7 +34,6 @@ Request Body
Response 200 (application/json) Response 200 (application/json)
{ {
"data": {}, "data": {},
"meta": {}
} }
``` ```
@ -50,8 +49,7 @@ Request Body
Response 200 (application/json) Response 200 (application/json)
{ {
"data": {}, "data": {}
"meta": {}
} }
``` ```
@ -75,8 +73,7 @@ Response 200 (application/json)
"user": { "user": {
"id": 1 "id": 1
} }
}, }
"meta": {}
} }
``` ```
@ -117,8 +114,7 @@ Response 200 (application/json)
"id": 2 "id": 2
} }
} }
], ]
"meta": {}
} }
``` ```
@ -141,6 +137,3 @@ Response 200 (application/json)
### `remove` ### `remove`
### `toggle` ### `toggle`

View File

@ -37,8 +37,8 @@
## boolean ## boolean
- $truthy - $isTruly
- $falsy - $isFalsy
## date ## date

View File

@ -281,12 +281,6 @@ Response 200 (application/json)
data: { data: {
id: 1 id: 1
}, },
meta: {
count: 1
page: 1,
pageSize: 1,
totalPage: 1
},
} }
``` ```

View File

@ -52,15 +52,67 @@ mock.onGet('users:get').reply(200, {
await api.request({ url: 'users:get' }); 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 ## Auth
```ts ```ts
// 直接传 token // 登录并记录 token
api.auth.token = '123'; api.auth.signIn({ email, password });
// 或者通过 signIn 登录 // 注销并删除 token
api.auth.signIn();
// 注销并删除 token 缓存
api.auth.signOut(); 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 ## Request

View File

@ -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 ## 2022/05/23 ~ v0.7.0-alpha.62
- feat(docs): add alert message - feat(docs): add alert message

View File

@ -2,6 +2,9 @@ import { APIClient } from '@nocobase/client';
const apiClient = new APIClient({ const apiClient = new APIClient({
baseURL: process.env.API_BASE_URL, baseURL: process.env.API_BASE_URL,
headers: {
'X-Hostname': window?.location?.hostname,
},
}); });
export default apiClient; export default apiClient;

View File

@ -18,6 +18,7 @@
"@formily/antd": "2.0.20", "@formily/antd": "2.0.20",
"@formily/core": "2.0.20", "@formily/core": "2.0.20",
"@formily/react": "2.0.20", "@formily/react": "2.0.20",
"@nocobase/sdk": "0.7.0-alpha.81",
"@nocobase/utils": "0.7.0-alpha.81", "@nocobase/utils": "0.7.0-alpha.81",
"ahooks": "^3.0.5", "ahooks": "^3.0.5",
"antd": "~4.19.5", "antd": "~4.19.5",

View File

@ -1,9 +1,8 @@
import { useFieldSchema } from '@formily/react'; import { useFieldSchema } from '@formily/react';
import { useCookieState } from 'ahooks';
import { Spin } from 'antd'; import { Spin } from 'antd';
import React, { createContext, useContext } from 'react'; import React, { createContext, useContext } from 'react';
import { Redirect } from 'react-router-dom'; import { Redirect } from 'react-router-dom';
import { useRequest } from '../api-client'; import { useAPIClient, useRequest } from '../api-client';
import { useCollection } from '../collection-manager'; import { useCollection } from '../collection-manager';
import { useRecordIsOwn } from '../record-provider'; import { useRecordIsOwn } from '../record-provider';
import { SchemaComponentOptions, useDesignable } from '../schema-component'; import { SchemaComponentOptions, useDesignable } from '../schema-component';
@ -22,7 +21,7 @@ export const ACLProvider = (props) => {
export const ACLRolesCheckProvider = (props) => { export const ACLRolesCheckProvider = (props) => {
const { setDesignable } = useDesignable(); const { setDesignable } = useDesignable();
const [roleName, setRoleName] = useCookieState('currentRoleName'); const api = useAPIClient();
const result = useRequest( const result = useRequest(
{ {
url: 'roles:check', url: 'roles:check',
@ -32,8 +31,8 @@ export const ACLRolesCheckProvider = (props) => {
if (!data?.data?.allowConfigure && !data?.data?.allowAll) { if (!data?.data?.allowConfigure && !data?.data?.allowAll) {
setDesignable(false); setDesignable(false);
} }
if (data?.data?.role !== roleName) { if (data?.data?.role !== api.auth.role) {
setRoleName(data?.data?.role); api.auth.setRole(data?.data?.role);
} }
}, },
}, },

View File

@ -3,10 +3,11 @@ import enUS from 'antd/lib/locale/en_US';
import zhCN from 'antd/lib/locale/zh_CN'; import zhCN from 'antd/lib/locale/zh_CN';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useRequest } from '../api-client'; import { useAPIClient, useRequest } from '../api-client';
export function AntdConfigProvider(props) { export function AntdConfigProvider(props) {
const { remoteLocale, ...others } = props; const { remoteLocale, ...others } = props;
const api = useAPIClient();
const { i18n } = useTranslation(); const { i18n } = useTranslation();
const { loading } = useRequest( const { loading } = useRequest(
{ {
@ -14,8 +15,9 @@ export function AntdConfigProvider(props) {
}, },
{ {
onSuccess(data) { onSuccess(data) {
const locale = localStorage.getItem('NOCOBASE_LANG'); const locale = api.auth.locale;
if (data?.data?.lang && !locale) { if (data?.data?.lang && !locale) {
api.auth.setLocale(data?.data?.lang);
i18n.changeLanguage(data?.data?.lang); i18n.changeLanguage(data?.data?.lang);
} }
}, },

View File

@ -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 { 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>>; 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);
}
} }

View File

@ -7,7 +7,7 @@ const log = require('debug')('i18next');
export const i18n = i18next.createInstance(); export const i18n = i18next.createInstance();
i18n.use(initReactI18next).init({ i18n.use(initReactI18next).init({
lng: localStorage.getItem('NOCOBASE_LANG') || 'en-US', lng: localStorage.getItem('NOCOBASE_LOCALE') || 'en-US',
// debug: true, // debug: true,
defaultNS: 'client', defaultNS: 'client',
// parseMissingKeyHandler: (key) => { // parseMissingKeyHandler: (key) => {
@ -29,9 +29,9 @@ function setMomentLng(language) {
moment.locale(lng); moment.locale(lng);
} }
setMomentLng(localStorage.getItem('NOCOBASE_LANG')); setMomentLng(localStorage.getItem('NOCOBASE_LOCALE'));
i18n.on('languageChanged', (lng) => { i18n.on('languageChanged', (lng) => {
localStorage.setItem('NOCOBASE_LANG', lng); localStorage.setItem('NOCOBASE_LOCALE', lng);
setMomentLng(lng); setMomentLng(lng);
}); });

View File

@ -36,7 +36,7 @@ export const CurrentUser = () => {
<Menu.Item <Menu.Item
onClick={async () => { onClick={async () => {
await api.resource('users').signout(); await api.resource('users').signout();
api.setBearerToken(null); api.auth.setToken(null);
history.push('/signin'); history.push('/signin');
}} }}
> >

View File

@ -34,6 +34,7 @@ export const LanguageSettings = () => {
appLang: lang, appLang: lang,
}, },
}); });
api.auth.setLocale(lang);
await i18n.changeLanguage(lang); await i18n.changeLanguage(lang);
window.location.reload(); window.location.reload();
}} }}

View File

@ -68,13 +68,8 @@ const useSignin = () => {
return { return {
async run() { async run() {
await form.submit(); await form.submit();
const response = await api.resource('users').signin({ await api.auth.signIn(form.values);
values: form.values,
});
if (response?.data?.data?.token) {
api.setBearerToken(response?.data?.data?.token);
history.push(redirect || '/admin'); history.push(redirect || '/admin');
}
}, },
}; };
}; };

View File

@ -1,4 +1,3 @@
import { useCookieState } from 'ahooks';
import { Menu, Select } from 'antd'; import { Menu, Select } from 'antd';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -30,9 +29,6 @@ export const SwitchRole = () => {
const api = useAPIClient(); const api = useAPIClient();
const roles = useCurrentRoles(); const roles = useCurrentRoles();
const { t } = useTranslation(); const { t } = useTranslation();
const [roleName, setRoleName] = useCookieState('currentRoleName', {
defaultValue: roles?.find((role) => role.default)?.name,
});
if (roles.length <= 1) { if (roles.length <= 1) {
return null; return null;
} }
@ -47,9 +43,9 @@ export const SwitchRole = () => {
value: 'name', value: 'name',
}} }}
options={roles} options={roles}
value={roleName} value={api.auth.role}
onChange={async (roleName) => { onChange={async (roleName) => {
setRoleName(roleName); api.auth.setRole(roleName);
await api.resource('users').setDefaultRole({ values: { roleName } }); await api.resource('users').setDefaultRole({ values: { roleName } });
window.location.href = '/'; window.location.href = '/';
}} }}

View File

@ -46,11 +46,14 @@ function getUmiConfig() {
function resolveNocobasePackagesAlias(config) { function resolveNocobasePackagesAlias(config) {
const clientSrc = resolve(process.cwd(), './packages/core/client/src'); const clientSrc = resolve(process.cwd(), './packages/core/client/src');
const utilsSrc = resolve(process.cwd(), './packages/core/utils/src'); const utilsSrc = resolve(process.cwd(), './packages/core/utils/src');
const sdkSrc = resolve(process.cwd(), './packages/core/sdk/src');
if (existsSync(clientSrc)) { if (existsSync(clientSrc)) {
config.module.rules.get('ts-in-node_modules').include.add(clientSrc); config.module.rules.get('ts-in-node_modules').include.add(clientSrc);
config.resolve.alias.set('@nocobase/client', clientSrc); config.resolve.alias.set('@nocobase/client', clientSrc);
config.module.rules.get('ts-in-node_modules').include.add(utilsSrc); config.module.rules.get('ts-in-node_modules').include.add(utilsSrc);
config.resolve.alias.set('@nocobase/utils', 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);
} }
} }

View File

@ -12,7 +12,8 @@
"module": "es/index.js", "module": "es/index.js",
"typings": "es/index.d.ts", "typings": "es/index.d.ts",
"dependencies": { "dependencies": {
"axios": "^0.26.1" "axios": "^0.26.1",
"qs": "^6.10.1"
}, },
"devDependencies": { "devDependencies": {
"axios-mock-adapter": "^1.20.0" "axios-mock-adapter": "^1.20.0"

View File

@ -22,15 +22,51 @@ export interface IResource {
[key: string]: (params?: ActionParams) => Promise<any>; [key: string]: (params?: ActionParams) => Promise<any>;
} }
class Auth { export class Auth {
protected api: APIClient; protected api: APIClient;
protected token: string;
protected role: string; protected options = {
token: null,
locale: null,
role: null,
};
constructor(api: APIClient) { constructor(api: APIClient) {
this.api = api; this.api = api;
this.api.axios.interceptors.request.use((config) => { this.locale = this.getLocale();
config.headers['X-Hostname'] = window.location.hostname; 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) { if (this.role) {
config.headers['X-Role'] = this.role; config.headers['X-Role'] = this.role;
} }
@ -38,26 +74,48 @@ class Auth {
config.headers['Authorization'] = `Bearer ${this.token}`; config.headers['Authorization'] = `Bearer ${this.token}`;
} }
return config; 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) { 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) { 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({ const response = await this.api.request({
method: 'post', method: 'post',
url: 'users:signin', url: 'users:signin',
data: values, data: values,
}); });
const data = response?.data?.data; const data = response?.data?.data;
this.token = data; this.setToken(data?.token);
return data; return response;
} }
async signOut() { async signOut() {
@ -65,25 +123,78 @@ class Auth {
method: 'post', method: 'post',
url: 'users:signout', 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 { export class APIClient {
axios: AxiosInstance; axios: AxiosInstance;
auth: Auth; auth: Auth;
storage: Storage;
constructor(instance?: AxiosInstance | AxiosRequestConfig) { constructor(instance?: AxiosInstance | (AxiosRequestConfig & ExtendedOptions)) {
if (typeof instance === 'function') { if (typeof instance === 'function') {
this.axios = instance; this.axios = instance;
} else { } 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.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) => { this.axios.interceptors.request.use((config) => {
config.paramsSerializer = (params) => { config.paramsSerializer = (params) => {
return qs.stringify(params, { return qs.stringify(params, {

View 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');
});
});

View File

@ -17596,6 +17596,13 @@ q@^1.1.2, q@^1.5.1:
resolved "https://registry.npmjs.org/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" resolved "https://registry.npmjs.org/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= 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: qs@^6.4.0, qs@^6.5.2, qs@^6.9.4:
version "6.10.1" version "6.10.1"
resolved "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz#4931482fa8d647a5aab799c5271d2133b981fb6a" resolved "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz#4931482fa8d647a5aab799c5271d2133b981fb6a"