diff --git a/docs/en-US/development/http-api/action-api.md b/docs/en-US/development/http-api/action-api.md
index d71f3a6f9..1445d05e5 100644
--- a/docs/en-US/development/http-api/action-api.md
+++ b/docs/en-US/development/http-api/action-api.md
@@ -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": {}
+ ]
}
```
diff --git a/docs/en-US/development/http-api/filter-operators.md b/docs/en-US/development/http-api/filter-operators.md
index 13e9f7beb..a31112c5c 100644
--- a/docs/en-US/development/http-api/filter-operators.md
+++ b/docs/en-US/development/http-api/filter-operators.md
@@ -37,8 +37,8 @@
## boolean
-- $truthy
-- $falsy
+- $isTruthy
+- $isFalsy
## date
diff --git a/docs/en-US/development/http-api/index.md b/docs/en-US/development/http-api/index.md
index ace76a369..79fc43e6c 100644
--- a/docs/en-US/development/http-api/index.md
+++ b/docs/en-US/development/http-api/index.md
@@ -280,13 +280,7 @@ Response 200 (application/json)
{
data: {
id: 1
- },
- meta: {
- count: 1
- page: 1,
- pageSize: 1,
- totalPage: 1
- },
+ }
}
```
diff --git a/docs/en-US/development/http-api/javascript-sdk.md b/docs/en-US/development/http-api/javascript-sdk.md
index 764510834..329441c51 100644
--- a/docs/en-US/development/http-api/javascript-sdk.md
+++ b/docs/en-US/development/http-api/javascript-sdk.md
@@ -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
diff --git a/docs/en-US/release-notes.md b/docs/en-US/release-notes.md
index 36e152603..b28f24831 100644
--- a/docs/en-US/release-notes.md
+++ b/docs/en-US/release-notes.md
@@ -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
diff --git a/docs/zh-CN/development/http-api/action-api.md b/docs/zh-CN/development/http-api/action-api.md
index a2208e2c3..26fd5e1b8 100644
--- a/docs/zh-CN/development/http-api/action-api.md
+++ b/docs/zh-CN/development/http-api/action-api.md
@@ -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`
-
-
-
diff --git a/docs/zh-CN/development/http-api/filter-operators.md b/docs/zh-CN/development/http-api/filter-operators.md
index b74cc80ae..332f28a50 100644
--- a/docs/zh-CN/development/http-api/filter-operators.md
+++ b/docs/zh-CN/development/http-api/filter-operators.md
@@ -37,8 +37,8 @@
## boolean
-- $truthy
-- $falsy
+- $isTruly
+- $isFalsy
## date
@@ -56,4 +56,4 @@
- $startsWith
- $notStartsWith
- $endWith
-- $notEndWith
\ No newline at end of file
+- $notEndWith
diff --git a/docs/zh-CN/development/http-api/index.md b/docs/zh-CN/development/http-api/index.md
index 328400898..a3117cf49 100644
--- a/docs/zh-CN/development/http-api/index.md
+++ b/docs/zh-CN/development/http-api/index.md
@@ -281,12 +281,6 @@ Response 200 (application/json)
data: {
id: 1
},
- meta: {
- count: 1
- page: 1,
- pageSize: 1,
- totalPage: 1
- },
}
```
diff --git a/docs/zh-CN/development/http-api/javascript-sdk.md b/docs/zh-CN/development/http-api/javascript-sdk.md
index d14cf9cff..34737c1b5 100644
--- a/docs/zh-CN/development/http-api/javascript-sdk.md
+++ b/docs/zh-CN/development/http-api/javascript-sdk.md
@@ -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
diff --git a/docs/zh-CN/release-notes.md b/docs/zh-CN/release-notes.md
index 5ab8061ea..d3f9fc273 100644
--- a/docs/zh-CN/release-notes.md
+++ b/docs/zh-CN/release-notes.md
@@ -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
diff --git a/packages/app/client/src/pages/apiClient.ts b/packages/app/client/src/pages/apiClient.ts
index 082319619..e80c0fa8e 100644
--- a/packages/app/client/src/pages/apiClient.ts
+++ b/packages/app/client/src/pages/apiClient.ts
@@ -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;
diff --git a/packages/core/client/package.json b/packages/core/client/package.json
index 24ae05c86..3f0f66fe2 100644
--- a/packages/core/client/package.json
+++ b/packages/core/client/package.json
@@ -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",
diff --git a/packages/core/client/src/acl/ACLProvider.tsx b/packages/core/client/src/acl/ACLProvider.tsx
index efea6de0f..f7d822623 100644
--- a/packages/core/client/src/acl/ACLProvider.tsx
+++ b/packages/core/client/src/acl/ACLProvider.tsx
@@ -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);
}
},
},
diff --git a/packages/core/client/src/antd-config-provider/index.tsx b/packages/core/client/src/antd-config-provider/index.tsx
index 07084837e..dd0a7d468 100644
--- a/packages/core/client/src/antd-config-provider/index.tsx
+++ b/packages/core/client/src/antd-config-provider/index.tsx
@@ -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);
}
},
diff --git a/packages/core/client/src/api-client/APIClient.ts b/packages/core/client/src/api-client/APIClient.ts
index 7735e731e..f154176d9 100644
--- a/packages/core/client/src/api-client/APIClient.ts
+++ b/packages/core/client/src/api-client/APIClient.ts
@@ -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
= {
- resource?: string;
- resourceOf?: any;
- action?: string;
- params?: P;
-};
-
-export interface IResource {
- list?: (params?: ActionParams) => Promise;
- get?: (params?: ActionParams) => Promise;
- create?: (params?: ActionParams) => Promise;
- update?: (params?: ActionParams) => Promise;
- destroy?: (params?: ActionParams) => Promise;
- [key: string]: (params?: ActionParams) => Promise;
-}
-
-export class APIClient {
- axios: AxiosInstance;
+export class APIClient extends APIClientSDK {
services: Record>;
-
- 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 {
- return this.services[uid];
- }
-
- request, D = any>(config: AxiosRequestConfig | ResourceActionOptions): Promise {
- const { resource, resourceOf, action, params } = config as any;
- if (resource) {
- return this.resource(resource, resourceOf)[action](params);
- }
- return this.axios.request(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);
- }
}
diff --git a/packages/core/client/src/i18n/i18n.ts b/packages/core/client/src/i18n/i18n.ts
index 3d90456f4..91df45724 100644
--- a/packages/core/client/src/i18n/i18n.ts
+++ b/packages/core/client/src/i18n/i18n.ts
@@ -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);
});
diff --git a/packages/core/client/src/user/CurrentUser.tsx b/packages/core/client/src/user/CurrentUser.tsx
index 1566a6247..c52173110 100644
--- a/packages/core/client/src/user/CurrentUser.tsx
+++ b/packages/core/client/src/user/CurrentUser.tsx
@@ -36,7 +36,7 @@ export const CurrentUser = () => {
{
await api.resource('users').signout();
- api.setBearerToken(null);
+ api.auth.setToken(null);
history.push('/signin');
}}
>
diff --git a/packages/core/client/src/user/LanguageSettings.tsx b/packages/core/client/src/user/LanguageSettings.tsx
index c66ae3194..e1a289be2 100644
--- a/packages/core/client/src/user/LanguageSettings.tsx
+++ b/packages/core/client/src/user/LanguageSettings.tsx
@@ -34,6 +34,7 @@ export const LanguageSettings = () => {
appLang: lang,
},
});
+ api.auth.setLocale(lang);
await i18n.changeLanguage(lang);
window.location.reload();
}}
diff --git a/packages/core/client/src/user/SigninPage.tsx b/packages/core/client/src/user/SigninPage.tsx
index 698dd6f1f..8cb87cb2f 100644
--- a/packages/core/client/src/user/SigninPage.tsx
+++ b/packages/core/client/src/user/SigninPage.tsx
@@ -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);
- history.push(redirect || '/admin');
- }
+ await api.auth.signIn(form.values);
+ history.push(redirect || '/admin');
},
};
};
diff --git a/packages/core/client/src/user/SwitchRole.tsx b/packages/core/client/src/user/SwitchRole.tsx
index c2a3fc39e..a8716f1b2 100644
--- a/packages/core/client/src/user/SwitchRole.tsx
+++ b/packages/core/client/src/user/SwitchRole.tsx
@@ -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 = '/';
}}
diff --git a/packages/core/devtools/umiConfig.js b/packages/core/devtools/umiConfig.js
index 0ee0f77e5..ff08758eb 100644
--- a/packages/core/devtools/umiConfig.js
+++ b/packages/core/devtools/umiConfig.js
@@ -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);
}
}
diff --git a/packages/core/sdk/package.json b/packages/core/sdk/package.json
index d75f14022..4fcc313b8 100644
--- a/packages/core/sdk/package.json
+++ b/packages/core/sdk/package.json
@@ -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"
diff --git a/packages/core/sdk/src/APIClient.ts b/packages/core/sdk/src/APIClient.ts
index ea67141db..a921c88d4 100644
--- a/packages/core/sdk/src/APIClient.ts
+++ b/packages/core/sdk/src/APIClient.ts
@@ -22,42 +22,100 @@ export interface IResource {
[key: string]: (params?: ActionParams) => Promise;
}
-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;
- if (this.role) {
- config.headers['X-Role'] = this.role;
- }
- if (this.token) {
- config.headers['Authorization'] = `Bearer ${this.token}`;
- }
- return config;
- });
+ 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;
+ }
+ if (this.token) {
+ 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> {
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);
+ }
}
- this.auth = new Auth(this);
- this.qsMiddleware();
+ if (!this.storage) {
+ this.initStorage();
+ }
+ if (!this.auth) {
+ this.auth = new Auth(this);
+ }
+ 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, {
diff --git a/packages/core/sdk/src/__tests__/api-client.test.ts b/packages/core/sdk/src/__tests__/api-client.test.ts
new file mode 100644
index 000000000..d15e346ca
--- /dev/null
+++ b/packages/core/sdk/src/__tests__/api-client.test.ts
@@ -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> {
+ 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');
+ });
+});
diff --git a/yarn.lock b/yarn.lock
index b1eb352b8..65ccab160 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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"