Merge pull request #754 from directus/sdk

v9 JS SDK
This commit is contained in:
Rijk van Zanten
2020-11-16 04:31:32 +01:00
committed by GitHub
49 changed files with 2515 additions and 34254 deletions

1
.gitignore vendored
View File

@@ -10,3 +10,4 @@ lerna-debug.log
dist
*.sublime-settings
*.db
.nyc_output

View File

@@ -24,7 +24,7 @@ router.get(
);
router.post(
'/hash',
'/hash/generate',
asyncHandler(async (req, res) => {
if (!req.body?.string) {
throw new InvalidPayloadException(`"string" is required`);

View File

@@ -24,7 +24,7 @@ export async function login(credentials: LoginCredentials) {
// Refresh the token 10 seconds before the access token expires. This means the user will stay
// logged in without any noticable hickups or delays
setTimeout(() => refresh(), response.data.data.expires * 1000 + 10 * 1000);
setTimeout(() => refresh(), response.data.data.expires - 10000);
appStore.state.authenticated = true;

604
docs/reference/sdk-js.md Normal file
View File

@@ -0,0 +1,604 @@
# SDK JS
The JS SDK is a small wrapper around [Axios](https://npmjs.com/axios) that makes it a little easier to use the Directus API from a JavaScript powered project.
## Installation
```bash
npm install @directus/sdk-js
```
## Usage
```js
import DirectusSDK from '@directus/sdk-js';
const directus = new DirectusSDK('https://api.example.com/');
```
**NOTE** All methods return promises. Make sure to await methods, for example:
```js
import DirectusSDK from '@directus/sdk-js';
const directus = new DirectusSDK('https://api.example.com/');
async function getData() {
// Wait for login to be done...
await directus.auth.login({ email: 'admin@example.com', password: 'password' });
// ... before fetching items
return await directus.items('articles').read();
}
getData();
```
## Reference
### Global
#### Initialize
```js
import DirectusSDK from '@directus/sdk-js';
const directus = new DirectusSDK('https://api.example.com/');
```
The SDK accepts a second optional `options` parameter:
```js
import DirectusSDK from '@directus/sdk-js';
const directus = new DirectusSDK('https://api.example.com/', {
auth: {
storage: new MemoryStore(), // Storage adapter where refresh tokens are stored in JSON mode
mode: 'json', // What login mode to use. One of `json`, `cookie`
autoRefresh: true, // Whether or not to automatically refresh the access token on login
}
});
```
#### Get / set API URL
```js
// Get the used API base URL
console.log(directus.url);
// => https://api.example.com/
// Set the API base URL
directus.url = 'https://api2.example.com';
```
#### Access to Axios
You can tap into the Axios instance used directly through `directus.axios`.
---
### Items
#### Create
##### Single Item
```js
directus.items('articles').create({
title: 'My New Article'
});
```
##### Multiple Items
```js
directus.items('articles').create([
{
title: 'My First Article'
},
{
title: 'My Second Article'
},
]);
```
#### Read
##### All
```js
directus.items('articles').read();
```
##### By Query
```js
directus.items('articles').read({
search: 'Directus',
filter: {
date_published: {
_gte: '$NOW'
}
}
});
```
##### By Primary Key(s)
```js
// One
directus.items('articles').read(15);
// Multiple
directus.items('articles').read([15, 42]);
```
Supports optional query:
```js
// One
directus.items('articles').read(15, { fields: ['title'] });
// Multiple
directus.items('articles').read([15, 42], { fields: ['title'] });
```
#### Update
##### One or More Item(s), Single Value
```js
// One
directus.items('articles').update(15, {
title: 'An Updated title'
});
// Multiple
directus.items('articles').update([15, 42], {
title: 'An Updated title'
});
```
Supports optional query:
```js
directus.items('articles').update(
15,
{ title: 'An Updated title' },
{ fields: ['title'] }
);
directus.items('articles').update(
[15, 42],
{ title: 'An Updated title' },
{ fields: ['title'] }
);
```
##### Multiple Items, Multiple Values
```js
directus.items('articles').update([
{
id: 15,
title: 'Article 15',
},
{
id: 42,
title: 'Article 42',
},
]);
```
Supports optional query:
```js
directus.items('articles').update([
{
id: 15,
title: 'Article 15',
},
{
id: 42,
title: 'Article 42',
},
], { fields: ['title'] });
```
##### Multiple Items by Query, Single Value
```js
directus.items('articles').update(
{
archived: true
},
{
filter: {
publish_date: {
_gte: '$NOW'
}
}
}
);
```
#### Delete
```js
// One
directus.items('articles').delete(15);
// Multiple
directus.items('articles').delete([15, 42]);
```
---
### Activity
#### Read Activity
##### All
```js
directus.activity.read();
```
##### By Query
```js
directus.activity.read({
filter: {
action: {
_eq: 'create'
}
}
});
```
##### By Primary Key(s)
```js
// One
directus.activity.read(15);
// Multiple
directus.activity.read([15, 42]);
```
Supports optional query:
```js
// One
directus.activity.read(15, { fields: ['action'] });
// Multiple
directus.activity.read([15, 42], { fields: ['action'] });
```
#### Create a Comment
```js
directus.activity.comments.create({
collection: 'articles',
item: 15,
comment: 'Hello, world!'
});
```
#### Update a comment
```js
directus.activity.comments.update(31, { comment: 'Howdy, world!' });
```
Note: The passed key is the primary key of the comment
#### Delete a comment
```js
directus.activity.comments.delete(31);
```
Note: The passed key is the primary key of the comment
---
### Auth
#### Configuration
Note: these configuration options are passed in the top level SDK constructor.
##### mode
`cookie` or `json`. When in cookie mode, the API will set the refresh token in a `httpOnly` secure cookie that can't be accessed from client side JS. This is the most secure way to connect to the API from a public front-end website.
When you can't rely on cookies, or need more control over handling the storage of the cookie, use `json` mode. This will return the refresh token like "regular" in the payload. You can use the `storage` option (see below) to control where the refresh token is stored / read from
##### storage
When using `json` for mode, the refresh token needs to be stored somewhere. The `storage` option allows you to plug in any object that has an async `setItem()` and `getItem()` method. This allows you to plugin things like [`localforage`](https://github.com/localForage/localForage) directly:
```js
import localforage from 'localforage';
import DirectusSDK from '@directus/sdk-js';
const directus = new DirectusSDK('https://api.example.com', { storage: localforage });
```
##### autoRefresh
Whether or not to automatically call `refresh()` when the access token is about to expire. Defaults to `true`
#### Get / Set Token
```
directus.auth.token = 'abc.def.ghi';
```
#### Login
```js
directus.auth.login({ email: 'admin@example.com', password: 'd1r3ctu5' });
```
#### Refresh
Note: if you have autoRefresh enabled, you most likely won't need to use this manually.
```js
directus.auth.refresh();
```
#### Logout
```js
directus.auth.logout();
```
#### Request a Password Reset
```js
directus.auth.password.request('admin@example.com');
```
#### Reset a Password
```js
directus.auth.password.reset('abc.def.ghi', 'n3w-p455w0rd');
```
Note: the token passed in the first parameter is sent in an email to the user when using `request()`
---
### Collections
```js
directus.collections;
```
Same methods as `directus.items(collection)`.
---
### Fields
```js
directus.fields;
```
Same methods as `directus.items(collection)`.
---
### Files
```js
directus.files;
```
Same methods as `directus.items(collection)`.
---
### Folders
```js
directus.folders;
```
Same methods as `directus.items(collection)`.
---
### Permissions
```js
directus.permissions;
```
Same methods as `directus.items(collection)`.
---
### Presets
```js
directus.presets;
```
Same methods as `directus.items(collection)`.
---
### Relations
```js
directus.relations;
```
Same methods as `directus.items(collection)`.
---
### Revisions
```js
directus.revisions;
```
Same methods as `directus.items(collection)`.
---
### Roles
```js
directus.roles;
```
Same methods as `directus.items(collection)`.
---
### Server
#### Get the API Spec in OAS Format
```js
directus.server.specs.oas();
```
#### Ping the Server
```js
directus.server.ping()
```
#### Get Server/Project Info
```js
directus.server.info();
```
---
### Settings
```js
directus.settings;
```
Same methods as `directus.items(collection)`.
---
### Users
```js
directus.users;
```
Same methods as `directus.items(collection)`, and:
#### Invite a New User
```js
directus.users.invite('admin@example.com', 'fe38136e-52f7-4622-8498-112b8a32a1e2');
```
The second parameter is the role of the user
#### Accept a User Invite
```js
directus.users.acceptInvite('abc.def.ghi', 'n3w-p455w0rd');
```
The provided token is sent to the user's email
#### Enable Two-Factor Authentication
```js
directus.users.tfa.enable('my-password');
```
#### Disable Two-Factor Authentication
```js
directus.users.tfa.disable('691402');
```
#### Get the Current User
```js
directus.users.me.read();
```
Supports optional query:
```js
directus.users.me.read({
fields: ['last_access']
});
```
#### Update the Current Users
```js
directus.users.me.update({ first_name: 'Admin' });
```
Supports optional query:
```js
directus.users.me.update(
{ first_name: 'Admin' },
{ fields: ['last_access'] }
);
```
---
### Utils
#### Get a Random String
```js
directus.utils.random.string();
```
Supports an optional `length`:
```js
directus.utils.random.string(32);
```
#### Generate a Hash for a Given Value
```js
directus.utils.hash.generate('My String');
```
#### Verify if a Hash is Valid
```js
directus.utils.hash.verify('My String', '$argon2i$v=19$m=4096,t=3,p=1$A5uogJh');
```
#### Sort Items in a Collection
```js
directus.utils.sort('articles', 15, 42);
```
This will move item 15 to the position of item 42, and move everything in between one "slot" up
#### Revert to a Previous Revision
```js
directus.utils.revert(451);
```
Note: the key passed is the primary key of the revision you'd like to apply

34251
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@
"scripts": {
"dev": "lerna run dev --stream --parallel",
"build": "lerna run build",
"release": "lerna publish --force-publish",
"release": "lerna run test && lerna publish --force-publish",
"cli": "cross-env NODE_ENV=development DOTENV_CONFIG_PATH=api/.env ts-node -r dotenv/config --script-mode --transpile-only api/src/cli/index.ts",
"postinstall": "npm run build"
},
@@ -62,6 +62,8 @@
"@types/qrcode": "^1.3.5",
"@types/semver": "^7.3.1",
"@types/sharp": "^0.25.1",
"@types/sinon": "^9.0.8",
"@types/sinon-chai": "^3.2.5",
"@types/uuid": "^8.0.0",
"@types/uuid-validate": "0.0.1",
"@types/webpack-env": "^1.15.2",
@@ -80,6 +82,7 @@
"autoprefixer": "^9.8.5",
"babel-loader": "^8.2.1",
"babel-preset-vue": "^2.0.2",
"chai": "^4.2.0",
"colors": "^1.4.0",
"concat-map": "0.0.1",
"copyfiles": "^2.4.0",
@@ -95,8 +98,10 @@
"lerna": "^3.22.1",
"lint-staged": "^10.3.0",
"lodash.camelcase": "^4.3.0",
"mocha": "^8.2.1",
"mockdate": "^3.0.2",
"npm-watch": "^0.7.0",
"nyc": "^15.1.0",
"prettier": "^2.1.1",
"raw-loader": "^4.0.1",
"react": "^16.13.1",
@@ -112,6 +117,9 @@
"rollup-plugin-typescript2": "^0.27.2",
"sass": "^1.26.10",
"sass-loader": "^9.0.2",
"sinon": "^9.2.0",
"sinon-chai": "^3.5.0",
"source-map-support": "^0.5.19",
"storybook-addon-themes": "^5.4.1",
"stylelint": "^13.6.1",
"stylelint-config-rational-order": "^0.1.2",
@@ -137,6 +145,7 @@
"@directus/app": "file:app",
"@directus/docs": "file:docs",
"@directus/format-title": "file:packages/format-title",
"@directus/sdk-js": "file:packages/sdk-js",
"@directus/specs": "file:packages/specs",
"create-directus-project": "file:packages/create-directus-project",
"directus": "file:api"

View File

@@ -0,0 +1,13 @@
root=true
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
indent_style = tab
indent_size = 4
trim_trailing_whitespace = true
[{package.json,*.yml,*.yaml}]
indent_style = space
indent_size = 2

View File

@@ -0,0 +1,38 @@
{
"name": "@directus/sdk-js",
"version": "9.0.0-rc.14",
"description": "",
"main": "index.js",
"scripts": {
"build": "tsc",
"test": "mocha -r ts-node/register/transpile-only -r source-map-support/register --recursive 'tests/**/*.ts'",
"coverage": "nyc npm test"
},
"keywords": [
"api",
"client",
"cms",
"directus",
"headless",
"javascript",
"node",
"sdk"
],
"author": "Rijk van Zanten <rijkvanzanten@me.com>",
"license": "MIT",
"dependencies": {
"axios": "^0.19.2"
},
"devDependencies": {
"chai": "^4.2.0",
"mocha": "^8.2.0",
"ts-node": "^9.0.0",
"typescript": "^4.0.3"
},
"nyc": {
"extension": [".ts"],
"include": ["src/**/*.ts"],
"exclude": ["**/*.d.ts"],
"all": true
}
}

21
packages/sdk-js/readme.md Normal file
View File

@@ -0,0 +1,21 @@
# Directus JS SDK
## Installation
```
npm install @directus/sdk-js
```
## Usage
```js
import DirectusSDK from '@directus/sdk-js';
const directus = new DirectusSDK('https://api.example.com/');
directus.items('articles').read(15);
```
## Docs
See [the docs](../../docs/reference/sdk-js.md)

View File

@@ -0,0 +1,42 @@
import { AxiosInstance } from 'axios';
import { ItemsHandler } from './items';
import { Query, PrimaryKey, Item, Response } from '../types';
export class ActivityHandler {
private axios: AxiosInstance;
private itemsHandler: ItemsHandler;
constructor(axios: AxiosInstance) {
this.axios = axios;
this.itemsHandler = new ItemsHandler('directus_activity', axios);
}
async read(query?: Query): Promise<Response<Item | Item[]>>;
async read(key: PrimaryKey, query?: Query): Promise<Response<Item>>;
async read(keys: PrimaryKey[], query?: Query): Promise<Response<Item | Item[]>>;
async read(
keysOrQuery?: PrimaryKey | PrimaryKey[] | Query,
query?: Query & { single: boolean }
): Promise<Response<Item | Item[]>> {
const result = await this.itemsHandler.read(keysOrQuery as any, query as any);
return result;
}
comments = {
create: async (payload: {
collection: string;
item: string;
comment: string;
}): Promise<Response<Item>> => {
const response = await this.axios.post('/activity/comments', payload);
return response.data;
},
update: async (key: PrimaryKey, payload: { comment: string }) => {
const response = await this.axios.patch(`/activity/comments/${key}`, payload);
return response.data;
},
delete: async (key: PrimaryKey) => {
await this.axios.delete(`/activity/comments/${key}`);
},
};
}

View File

@@ -0,0 +1,97 @@
import { AxiosInstance } from 'axios';
import { AuthStorage } from '../types';
export type LoginCredentials = {
email: string;
password: string;
otp?: string;
};
export type AuthOptions = {
mode: 'cookie' | 'json';
autoRefresh: boolean;
storage: AuthStorage;
};
export class AuthHandler {
private axios: AxiosInstance;
private storage: AuthStorage;
private mode: 'cookie' | 'json';
private autoRefresh: boolean;
constructor(axios: AxiosInstance, options: AuthOptions) {
this.axios = axios;
this.storage = options.storage;
this.mode = options.mode;
this.autoRefresh = options.autoRefresh;
if (this.autoRefresh) {
this.refresh();
}
}
get token() {
return this.axios.defaults.headers?.Authorization?.split(' ')[1] || null;
}
set token(val: string | null) {
this.axios.defaults.headers = {
...(this.axios.defaults.headers || {}),
Authorization: val ? `Bearer ${val}` : undefined,
};
}
async login(credentials: LoginCredentials) {
const response = await this.axios.post('/auth/login', { ...credentials, mode: this.mode });
this.token = response.data.data.access_token;
if (this.mode === 'json') {
await this.storage.setItem('directus_refresh_token', response.data.data.refresh_token);
}
if (this.autoRefresh) {
setTimeout(() => this.refresh(), response.data.data.expires - 10000);
}
return response.data;
}
async refresh() {
const payload: Record<string, any> = { mode: this.mode };
if (this.mode === 'json') {
const refreshToken = await this.storage.getItem('directus_refresh_token');
payload['refresh_token'] = refreshToken;
}
const response = await this.axios.post('/auth/refresh', payload);
this.token = response.data.data.access_token;
if (this.mode === 'json') {
await this.storage.setItem('directus_refresh_token', response.data.data.refresh_token);
}
if (this.autoRefresh) {
setTimeout(() => this.refresh(), response.data.data.expires - 10000);
}
return response.data;
}
async logout() {
await this.axios.post('/auth/logout');
this.token = null;
}
password = {
request: async (email: string) => {
await this.axios.post('/auth/password/request', { email });
},
reset: async (token: string, password: string) => {
await this.axios.post('/auth/password/reset', { token, password });
},
};
}

View File

@@ -0,0 +1,8 @@
import { AxiosInstance } from 'axios';
import { ItemsHandler } from './items';
export class CollectionsHandler extends ItemsHandler {
constructor(axios: AxiosInstance) {
super('directus_collections', axios);
}
}

View File

@@ -0,0 +1,8 @@
import { AxiosInstance } from 'axios';
import { ItemsHandler } from './items';
export class FieldsHandler extends ItemsHandler {
constructor(axios: AxiosInstance) {
super('directus_fields', axios);
}
}

View File

@@ -0,0 +1,8 @@
import { AxiosInstance } from 'axios';
import { ItemsHandler } from './items';
export class FilesHandler extends ItemsHandler {
constructor(axios: AxiosInstance) {
super('directus_files', axios);
}
}

View File

@@ -0,0 +1,8 @@
import { AxiosInstance } from 'axios';
import { ItemsHandler } from './items';
export class FoldersHandler extends ItemsHandler {
constructor(axios: AxiosInstance) {
super('directus_folders', axios);
}
}

View File

@@ -0,0 +1,16 @@
export * from './items';
export * from './server';
export * from './utils';
export * from './activity';
export * from './folders';
export * from './permissions';
export * from './presets';
export * from './relations';
export * from './revisions';
export * from './roles';
export * from './users';
export * from './settings';
export * from './files';
export * from './collections';
export * from './fields';
export * from './auth';

View File

@@ -0,0 +1,102 @@
import { Query, Item, Payload, Response, PrimaryKey } from '../types';
import { AxiosInstance } from 'axios';
export class ItemsHandler {
axios: AxiosInstance;
private endpoint: string;
constructor(collection: string, axios: AxiosInstance) {
this.axios = axios;
this.endpoint = collection.startsWith('directus_')
? `/${collection.substring(9)}/`
: `/items/${collection}/`;
}
async create(payload: Payload, query?: Query): Promise<Response<Item>>;
async create(payloads: Payload[], query?: Query): Promise<Response<Item | Item[]>>;
async create(payloads: Payload | Payload[], query?: Query): Promise<Response<Item | Item[]>> {
const result = await this.axios.post(this.endpoint, payloads, {
params: query,
});
return result.data;
}
async read(query?: Query): Promise<Response<Item | Item[]>>;
async read(key: PrimaryKey, query?: Query): Promise<Response<Item>>;
async read(keys: PrimaryKey[], query?: Query): Promise<Response<Item | Item[]>>;
async read(
keysOrQuery?: PrimaryKey | PrimaryKey[] | Query,
query?: Query & { single: boolean }
): Promise<Response<Item | Item[]>> {
let keys: PrimaryKey | PrimaryKey[] | null = null;
if (
keysOrQuery &&
(Array.isArray(keysOrQuery) ||
typeof keysOrQuery === 'string' ||
typeof keysOrQuery === 'number')
) {
keys = keysOrQuery;
}
let params: Query = {};
if (query) {
params = query;
} else if (
!query &&
typeof keysOrQuery === 'object' &&
Array.isArray(keysOrQuery) === false
) {
params = keysOrQuery as Query;
}
let endpoint = this.endpoint;
if (keys) {
endpoint += keys;
}
const result = await this.axios.get(endpoint, { params });
return result.data;
}
async update(key: PrimaryKey, payload: Payload, query?: Query): Promise<Response<Item>>;
async update(keys: PrimaryKey[], payload: Payload, query?: Query): Promise<Response<Item[]>>;
async update(payload: Payload[], query?: Query): Promise<Response<Item[]>>;
async update(payload: Payload, query: Query): Promise<Response<Item[]>>;
async update(
keyOrPayload: PrimaryKey | PrimaryKey[] | Payload | Payload[],
payloadOrQuery?: Payload | Query,
query?: Query
): Promise<Response<Item | Item[]>> {
if (
typeof keyOrPayload === 'string' ||
typeof keyOrPayload === 'number' ||
(Array.isArray(keyOrPayload) &&
(keyOrPayload as any[]).every((key) => ['string', 'number'].includes(typeof key)))
) {
const key = keyOrPayload as PrimaryKey | PrimaryKey[];
const payload = payloadOrQuery as Payload;
const result = await this.axios.patch(`${this.endpoint}${key}`, payload, {
params: query,
});
return result.data;
} else {
const result = await this.axios.patch(`${this.endpoint}`, keyOrPayload, {
params: payloadOrQuery,
});
return result.data;
}
}
async delete(key: PrimaryKey): Promise<void>;
async delete(keys: PrimaryKey[]): Promise<void>;
async delete(keys: PrimaryKey | PrimaryKey[]): Promise<void> {
await this.axios.delete(`${this.endpoint}${keys}`);
}
}

View File

@@ -0,0 +1,8 @@
import { AxiosInstance } from 'axios';
import { ItemsHandler } from './items';
export class PermissionsHandler extends ItemsHandler {
constructor(axios: AxiosInstance) {
super('directus_permissions', axios);
}
}

View File

@@ -0,0 +1,8 @@
import { AxiosInstance } from 'axios';
import { ItemsHandler } from './items';
export class PresetsHandler extends ItemsHandler {
constructor(axios: AxiosInstance) {
super('directus_presets', axios);
}
}

View File

@@ -0,0 +1,8 @@
import { AxiosInstance } from 'axios';
import { ItemsHandler } from './items';
export class RelationsHandler extends ItemsHandler {
constructor(axios: AxiosInstance) {
super('directus_relations', axios);
}
}

View File

@@ -0,0 +1,8 @@
import { AxiosInstance } from 'axios';
import { ItemsHandler } from './items';
export class RevisionsHandler extends ItemsHandler {
constructor(axios: AxiosInstance) {
super('directus_revisions', axios);
}
}

View File

@@ -0,0 +1,8 @@
import { AxiosInstance } from 'axios';
import { ItemsHandler } from './items';
export class RolesHandler extends ItemsHandler {
constructor(axios: AxiosInstance) {
super('directus_roles', axios);
}
}

View File

@@ -0,0 +1,26 @@
import { AxiosInstance } from 'axios';
export class ServerHandler {
private axios: AxiosInstance;
constructor(axios: AxiosInstance) {
this.axios = axios;
}
specs = {
oas: async () => {
const result = await this.axios.get('/server/specs/oas');
return result.data;
},
};
async ping() {
await this.axios.get('/server/ping');
return 'pong';
}
async info() {
const result = await this.axios.get('/server/info');
return result.data;
}
}

View File

@@ -0,0 +1,8 @@
import { AxiosInstance } from 'axios';
import { ItemsHandler } from './items';
export class SettingsHandler extends ItemsHandler {
constructor(axios: AxiosInstance) {
super('directus_settings', axios);
}
}

View File

@@ -0,0 +1,37 @@
import { AxiosInstance } from 'axios';
import { ItemsHandler } from './items';
import { Query, Payload } from '../types';
export class UsersHandler extends ItemsHandler {
constructor(axios: AxiosInstance) {
super('directus_users', axios);
}
async invite(email: string | string[], role: string) {
await this.axios.post('/users/invite', { email, role });
}
async acceptInvite(token: string, password: string) {
await this.axios.post('/users/invite/accept', { token, password });
}
tfa = {
enable: async (password: string) => {
await this.axios.post('/users/tfa/enable', { password });
},
disable: async (otp: string) => {
await this.axios.post('/users/tfa/disable', { otp });
},
};
me = {
read: async (query?: Query) => {
const response = await this.axios.get('/users/me', { params: query });
return response.data;
},
update: async (payload: Payload, query?: Query) => {
const response = await this.axios.patch('/users/me', payload, { params: query });
return response.data;
},
};
}

View File

@@ -0,0 +1,36 @@
import { AxiosInstance } from 'axios';
import { PrimaryKey } from '../types';
export class UtilsHandler {
private axios: AxiosInstance;
constructor(axios: AxiosInstance) {
this.axios = axios;
}
random = {
string: async (length: number = 32) => {
const result = await this.axios.get('/utils/random/string', { params: { length } });
return result.data;
},
};
hash = {
generate: async (string: string) => {
const result = await this.axios.post('/utils/hash/generate', { string });
return result.data;
},
verify: async (string: string, hash: string) => {
const result = await this.axios.post('/utils/hash/verify', { string, hash });
return result.data;
},
};
async sort(collection: string, item: PrimaryKey, to: PrimaryKey) {
await this.axios.post(`/utils/sort/${collection}`, { item, to });
}
async revert(revision: PrimaryKey) {
await this.axios.post(`/utils/revert/${revision}`);
}
}

View File

@@ -0,0 +1,121 @@
import axios, { AxiosInstance } from 'axios';
import {
ItemsHandler,
ServerHandler,
UtilsHandler,
ActivityHandler,
FoldersHandler,
PermissionsHandler,
PresetsHandler,
RolesHandler,
UsersHandler,
SettingsHandler,
FilesHandler,
CollectionsHandler,
FieldsHandler,
AuthHandler,
RelationsHandler,
AuthOptions,
RevisionsHandler,
} from './handlers';
import { MemoryStore } from './utils';
export default class DirectusSDK {
axios: AxiosInstance;
private authOptions: AuthOptions;
constructor(url: string, options?: { auth: Partial<AuthOptions> }) {
this.axios = axios.create({
baseURL: url,
});
this.authOptions = {
storage:
options?.auth?.storage !== undefined ? options.auth.storage : new MemoryStore(),
mode: options?.auth?.mode !== undefined ? options.auth.mode : 'cookie',
autoRefresh: options?.auth?.autoRefresh !== undefined ? options.auth.autoRefresh : true,
};
}
// Global helpers
////////////////////////////////////////////////////////////////////////////////////////////////
get url() {
return this.axios.defaults.baseURL!;
}
set url(val: string) {
this.axios.defaults.baseURL = val;
}
// Handlers
////////////////////////////////////////////////////////////////////////////////////////////////
items(collection: string) {
if (collection.startsWith('directus_')) {
throw new Error(`You can't read the "${collection}" collection directly.`);
}
return new ItemsHandler(collection, this.axios);
}
get activity() {
return new ActivityHandler(this.axios);
}
get auth() {
return new AuthHandler(this.axios, this.authOptions);
}
get collections() {
return new CollectionsHandler(this.axios);
}
get fields() {
return new FieldsHandler(this.axios);
}
get files() {
return new FilesHandler(this.axios);
}
get folders() {
return new FoldersHandler(this.axios);
}
get permissions() {
return new PermissionsHandler(this.axios);
}
get presets() {
return new PresetsHandler(this.axios);
}
get relations() {
return new RelationsHandler(this.axios);
}
get revisions() {
return new RevisionsHandler(this.axios);
}
get roles() {
return new RolesHandler(this.axios);
}
get server() {
return new ServerHandler(this.axios);
}
get settings() {
return new SettingsHandler(this.axios);
}
get users() {
return new UsersHandler(this.axios);
}
get utils() {
return new UtilsHandler(this.axios);
}
}

View File

@@ -0,0 +1,52 @@
export type Item = Record<string, any>;
export type Payload = Record<string, any>;
export type PrimaryKey = string | number;
export enum Meta {
TOTAL_COUNT = 'total_count',
FILTER_COUNT = 'filter_count',
}
export type Response<T> = {
data: T | null;
meta?: Record<Meta, number>;
};
export type Query = {
fields?: string | string[];
sort?: string;
filter?: Filter;
limit?: number;
offset?: number;
page?: number;
single?: boolean;
meta?: Meta[];
search?: string;
export?: 'json' | 'csv';
deep?: Record<string, Query>;
};
export type Filter = {
[keyOrOperator: string]: Filter | string | boolean | number | string[];
};
export type FilterOperator =
| '_eq'
| '_neq'
| '_contains'
| '_ncontains'
| '_in'
| '_nin'
| '_gt'
| '_gte'
| '_lt'
| '_lte'
| '_null'
| '_nnull'
| '_empty'
| '_nempty';
export type AuthStorage = {
getItem: (key: string) => Promise<any>;
setItem: (key: string, value: any) => Promise<any>;
};

View File

@@ -0,0 +1 @@
export * from './memory-store';

View File

@@ -0,0 +1,13 @@
import { AuthStorage } from '../types';
export class MemoryStore implements AuthStorage {
private values: Record<string, any> = {};
async getItem(key: string) {
return this.values[key];
}
async setItem(key: string, value: any) {
this.values[key] = value;
}
}

View File

@@ -0,0 +1,94 @@
import { ActivityHandler } from '../../src/handlers/activity';
import { ItemsHandler } from '../../src/handlers/items';
import axios, { AxiosInstance } from 'axios';
import sinon, { SinonSandbox } from 'sinon';
import chai, { expect } from 'chai';
import sinonChai from 'sinon-chai';
chai.use(sinonChai);
describe('ActivityHandler', () => {
let sandbox: SinonSandbox;
let axiosInstance: AxiosInstance;
let handler: ActivityHandler;
beforeEach(() => {
sandbox = sinon.createSandbox();
axiosInstance = axios.create();
handler = new ActivityHandler(axiosInstance);
});
afterEach(() => {
sandbox.restore();
});
describe('constructor', () => {
it('Initializes ItemHandler instance', () => {
expect(handler['itemsHandler']).to.be.instanceOf(ItemsHandler);
});
});
describe('read', () => {
it('Calls ItemsHandler#read with the provided params', async () => {
const stub = sandbox
.stub(handler['itemsHandler'], 'read')
.returns(Promise.resolve({ data: {} }));
await handler.read();
expect(stub).to.have.been.calledWith();
await handler.read(15);
expect(stub).to.have.been.calledWith(15);
await handler.read([15, 41]);
expect(stub).to.have.been.calledWith([15, 41]);
await handler.read([15, 41], { fields: ['title'] });
expect(stub).to.have.been.calledWith([15, 41], { fields: ['title'] });
});
});
describe('comments.create', () => {
it('Calls the /activity/comments endpoint', async () => {
const stub = sandbox
.stub(handler['axios'], 'post')
.returns(Promise.resolve({ data: {} }));
await handler.comments.create({
collection: 'articles',
item: '15',
comment: 'Hello World',
});
expect(stub).to.have.been.calledWith('/activity/comments', {
collection: 'articles',
item: '15',
comment: 'Hello World',
});
});
});
describe('comments.update', () => {
it('Calls the /activity/comments/:id endpoint', async () => {
const stub = sandbox
.stub(handler['axios'], 'patch')
.returns(Promise.resolve({ data: {} }));
await handler.comments.update(15, { comment: 'Hello Update' });
expect(stub).to.have.been.calledWith('/activity/comments/15', {
comment: 'Hello Update',
});
});
});
describe('comments.delete', () => {
it('Calls the /activity/comments/:id endpoint', async () => {
const stub = sandbox.stub(handler['axios'], 'delete').returns(Promise.resolve());
await handler.comments.delete(15);
expect(stub).to.have.been.calledWith('/activity/comments/15');
});
});
});

View File

@@ -0,0 +1,235 @@
import { AuthHandler } from '../../src/handlers';
import axios, { AxiosInstance } from 'axios';
import sinon, { SinonSandbox, SinonFakeTimers } from 'sinon';
import chai, { expect } from 'chai';
import sinonChai from 'sinon-chai';
import { MemoryStore } from '../../src/utils/memory-store';
chai.use(sinonChai);
const mockResponse = {
data: {
access_token: 'abc.def.ghi',
refresh_token: 'jkl.mno.pqr',
expires: 900000,
},
};
Object.freeze(mockResponse);
describe('AuthHandler', () => {
let sandbox: SinonSandbox;
let axiosInstance: AxiosInstance;
let handler: AuthHandler;
let clock: SinonFakeTimers;
beforeEach(() => {
sandbox = sinon.createSandbox();
axiosInstance = axios.create();
handler = new AuthHandler(axiosInstance, {
mode: 'json',
autoRefresh: false,
storage: new MemoryStore(),
});
clock = sinon.useFakeTimers();
});
afterEach(() => {
sandbox.restore();
clock.restore();
});
describe('token', () => {
it('Sets token as auth header in Axios', () => {
handler.token = 'test';
expect(handler['axios'].defaults.headers.Authorization).to.equal('Bearer test');
});
it('Deletes the defaults auth header to undefined when token is set to a falsey value', () => {
handler['axios'].defaults.headers.Authorization = 'Bearer test';
handler.token = null;
expect(handler['axios'].defaults.headers.Authorization).to.be.undefined;
});
it('Gets the token from Axios default header', () => {
handler['axios'].defaults.headers.Authorization = 'Bearer test';
expect(handler.token).to.equal('test');
});
it('Returns null if headers do not exist, or if token is not set', () => {
handler['axios'].defaults.headers = null;
expect(handler.token).to.be.null;
handler['axios'].defaults.headers = { Authorization: null };
expect(handler.token).to.be.null;
handler['axios'].defaults.headers.Authorization = 'Invalid';
expect(handler.token).to.be.null;
});
it('Preserves the other existing default headers', () => {
handler['axios'].defaults.headers = {
Test: 'example',
};
handler.token = 'Rijk';
expect(handler['axios'].defaults.headers.Test).to.exist;
});
it('Defaults to {} if no default headers exist yet', () => {
handler['axios'].defaults.headers = null;
handler.token = 'Rijk';
expect(handler['axios'].defaults.headers.Authorization).to.exist;
});
});
describe('login', () => {
it('Calls the /auth/login endpoint', async () => {
const stub = sandbox.stub(handler['axios'], 'post').resolves({ data: mockResponse });
await handler.login({ email: 'test@example.com', password: 'test' });
expect(stub).to.have.been.calledWith('/auth/login', {
email: 'test@example.com',
password: 'test',
mode: 'json',
});
});
it('Sets the token after retrieval', async () => {
sandbox.stub(handler['axios'], 'post').resolves({ data: mockResponse });
await handler.login({ email: 'test@example.com', password: 'test' });
expect(handler['token']).to.equal('abc.def.ghi');
});
it('Adds the refresh token to the passed store in JSON mode', async () => {
sandbox.stub(handler['axios'], 'post').resolves({ data: mockResponse });
const testStore = new MemoryStore();
const stub = sandbox.stub(testStore, 'setItem');
handler['storage'] = testStore;
handler['mode'] = 'json';
await handler.login({ email: 'test@example.com', password: 'test' });
expect(stub).to.have.been.calledWith('directus_refresh_token', 'jkl.mno.pqr');
});
it('Does not attempt to set the refresh token in cookie mode', async () => {
sandbox.stub(handler['axios'], 'post').resolves({ data: mockResponse });
const testStore = new MemoryStore();
const stub = sandbox.stub(testStore, 'setItem');
handler['mode'] = 'cookie';
await handler.login({ email: 'test@example.com', password: 'test' });
expect(stub).to.not.have.been.called;
});
it('Calls refresh 10 seconds before expiry time when in autoRefresh mode', async () => {
sandbox.stub(handler['axios'], 'post').resolves({ data: mockResponse });
handler['autoRefresh'] = true;
const stub = sandbox.stub(handler, 'refresh').resolves();
await handler.login({ email: 'test@example.com', password: 'test' });
clock.tick(885000); // 15 seconds before expiry time
expect(stub).to.not.have.been.called;
clock.tick(6000); // add +6s
expect(stub).to.have.been.called;
});
it('Does not call refresh when not in auto refresh mode', async () => {
sandbox.stub(handler['axios'], 'post').resolves({ data: mockResponse });
handler['autoRefresh'] = false;
const stub = sandbox.stub(handler, 'refresh').resolves();
await handler.login({ email: 'test@example.com', password: 'test' });
clock.tick(910000);
expect(stub).to.not.have.been.called;
});
});
describe('refresh', () => {
it('Calls the /auth/refresh endpoint without refresh token when in cookie mode', async () => {
const stub = sandbox.stub(handler['axios'], 'post').resolves({ data: mockResponse });
handler['mode'] = 'cookie';
await handler.refresh();
expect(stub).to.have.been.calledWith('/auth/refresh', { mode: 'cookie' });
});
it('Passes the refresh token from the store when using JSON mode', async () => {
const stub = sandbox.stub(handler['axios'], 'post').resolves({ data: mockResponse });
handler['mode'] = 'json';
const testStore = new MemoryStore();
testStore['values'].directus_refresh_token = 'test-token';
handler['storage'] = testStore;
await handler.refresh();
expect(stub).to.have.been.calledWith('/auth/refresh', {
mode: 'json',
refresh_token: 'test-token',
});
});
it('Sets the token on refresh', async () => {
sandbox.stub(handler['axios'], 'post').resolves({ data: mockResponse });
handler.token = 'before';
await handler.refresh();
expect(handler.token).to.equal('abc.def.ghi');
});
it('Adds the refresh token to the passed store in JSON mode', async () => {
sandbox.stub(handler['axios'], 'post').resolves({ data: mockResponse });
const testStore = new MemoryStore();
const stub = sandbox.stub(testStore, 'setItem');
handler['storage'] = testStore;
handler['mode'] = 'json';
await handler.refresh();
expect(stub).to.have.been.calledWith('directus_refresh_token', 'jkl.mno.pqr');
});
it('Calls itself 10 seconds before expiry when autoRefresh is enabled', async () => {
sandbox.stub(handler['axios'], 'post').resolves({ data: mockResponse });
handler['autoRefresh'] = true;
const spy = sandbox.spy(handler, 'refresh');
await handler.refresh();
clock.tick(910000);
expect(spy).to.have.been.calledTwice;
});
});
describe('logout', () => {
it('Calls the /auth/logout endpoint', async () => {
const stub = sandbox.stub(handler['axios'], 'post').resolves();
await handler.logout();
expect(stub).to.have.been.calledWith('/auth/logout');
});
it('Nullifies the token', async () => {
sandbox.stub(handler['axios'], 'post').resolves();
handler.token = 'test-token';
await handler.logout();
expect(handler.token).to.be.null;
});
});
describe('password.request', () => {
it('Calls the /auth/password/request endpoint', async () => {
const stub = sandbox.stub(handler['axios'], 'post').resolves();
await handler.password.request('admin@example.com');
expect(stub).to.have.been.calledWith('/auth/password/request', {
email: 'admin@example.com',
});
});
});
describe('password.reset', () => {
it('Calls the /auth/password/reset endpoint', async () => {
const stub = sandbox.stub(handler['axios'], 'post').resolves();
await handler.password.reset('abc.def.ghi', 'p455w0rd');
expect(stub).to.have.been.calledWith('/auth/password/reset', {
token: 'abc.def.ghi',
password: 'p455w0rd',
});
});
});
});

View File

@@ -0,0 +1,27 @@
import { CollectionsHandler, ItemsHandler } from '../../src/handlers';
import axios, { AxiosInstance } from 'axios';
import sinon, { SinonSandbox } from 'sinon';
import chai, { expect } from 'chai';
import sinonChai from 'sinon-chai';
chai.use(sinonChai);
describe('CollectionsHandler', () => {
let sandbox: SinonSandbox;
let axiosInstance: AxiosInstance;
let handler: CollectionsHandler;
beforeEach(() => {
sandbox = sinon.createSandbox();
axiosInstance = axios.create();
handler = new CollectionsHandler(axiosInstance);
});
afterEach(() => {
sandbox.restore();
});
it('Extends ItemsHandler', () => {
expect(handler).to.be.instanceOf(ItemsHandler);
});
});

View File

@@ -0,0 +1,27 @@
import { FieldsHandler, ItemsHandler } from '../../src/handlers';
import axios, { AxiosInstance } from 'axios';
import sinon, { SinonSandbox } from 'sinon';
import chai, { expect } from 'chai';
import sinonChai from 'sinon-chai';
chai.use(sinonChai);
describe('FieldsHandler', () => {
let sandbox: SinonSandbox;
let axiosInstance: AxiosInstance;
let handler: FieldsHandler;
beforeEach(() => {
sandbox = sinon.createSandbox();
axiosInstance = axios.create();
handler = new FieldsHandler(axiosInstance);
});
afterEach(() => {
sandbox.restore();
});
it('Extends ItemsHandler', () => {
expect(handler).to.be.instanceOf(ItemsHandler);
});
});

View File

@@ -0,0 +1,27 @@
import { FilesHandler, ItemsHandler } from '../../src/handlers';
import axios, { AxiosInstance } from 'axios';
import sinon, { SinonSandbox } from 'sinon';
import chai, { expect } from 'chai';
import sinonChai from 'sinon-chai';
chai.use(sinonChai);
describe('FilesHandler', () => {
let sandbox: SinonSandbox;
let axiosInstance: AxiosInstance;
let handler: FilesHandler;
beforeEach(() => {
sandbox = sinon.createSandbox();
axiosInstance = axios.create();
handler = new FilesHandler(axiosInstance);
});
afterEach(() => {
sandbox.restore();
});
it('Extends ItemsHandler', () => {
expect(handler).to.be.instanceOf(ItemsHandler);
});
});

View File

@@ -0,0 +1,27 @@
import { FoldersHandler, ItemsHandler } from '../../src/handlers';
import axios, { AxiosInstance } from 'axios';
import sinon, { SinonSandbox } from 'sinon';
import chai, { expect } from 'chai';
import sinonChai from 'sinon-chai';
chai.use(sinonChai);
describe('FoldersHandler', () => {
let sandbox: SinonSandbox;
let axiosInstance: AxiosInstance;
let handler: FoldersHandler;
beforeEach(() => {
sandbox = sinon.createSandbox();
axiosInstance = axios.create();
handler = new FoldersHandler(axiosInstance);
});
afterEach(() => {
sandbox.restore();
});
it('Extends ItemsHandler', () => {
expect(handler).to.be.instanceOf(ItemsHandler);
});
});

View File

@@ -0,0 +1,164 @@
import { ItemsHandler } from '../../src/handlers/items';
import axios, { AxiosInstance } from 'axios';
import sinon, { SinonSandbox } from 'sinon';
import chai, { expect } from 'chai';
import sinonChai from 'sinon-chai';
chai.use(sinonChai);
describe('ItemsHandler', () => {
let sandbox: SinonSandbox;
let axiosInstance: AxiosInstance;
let handler: ItemsHandler;
beforeEach(() => {
sandbox = sinon.createSandbox();
axiosInstance = axios.create();
handler = new ItemsHandler('test', axiosInstance);
});
afterEach(() => {
sandbox.restore();
});
describe('constructor', () => {
it('Sets the correct endpoint', () => {
const handler = new ItemsHandler('test', axiosInstance);
expect(handler['endpoint']).to.equal('/items/test/');
const handler2 = new ItemsHandler('directus_activity', axiosInstance);
expect(handler2['endpoint']).to.equal('/activity/');
});
});
describe('create', () => {
it('Calls the /items/:collection endpoint when creating a single item', async () => {
const stub = sandbox.stub(handler['axios'], 'post').resolves({ data: '' });
await handler.create({ title: 'test' });
expect(stub).to.have.been.calledWith(
'/items/test/',
{ title: 'test' },
{ params: undefined }
);
});
it('Calls the /items/:collection endpoint when creating multiple items', async () => {
const stub = sandbox.stub(handler['axios'], 'post').resolves({ data: '' });
await handler.create([{ title: 'test' }, { title: 'another test' }]);
expect(stub).to.have.been.calledWith(
'/items/test/',
[{ title: 'test' }, { title: 'another test' }],
{ params: undefined }
);
});
it('Passes the query params', async () => {
const stub = sandbox.stub(handler['axios'], 'post').resolves({ data: '' });
await handler.create({ title: 'test' }, { fields: ['test'] });
expect(stub).to.have.been.calledWith(
'/items/test/',
{ title: 'test' },
{ params: { fields: ['test'] } }
);
await handler.create([{ title: 'test' }, { title: 'another test' }], {
fields: ['test'],
});
expect(stub).to.have.been.calledWith(
'/items/test/',
[{ title: 'test' }, { title: 'another test' }],
{ params: { fields: ['test'] } }
);
});
});
describe('read', () => {
it('Reads all with no params', async () => {
const stub = sandbox.stub(handler['axios'], 'get').resolves({ data: '' });
await handler.read();
expect(stub).to.have.been.calledWith('/items/test/', { params: {} });
});
it('Does not set the PK when only using query', async () => {
const stub = sandbox.stub(handler['axios'], 'get').resolves({ data: '' });
await handler.read({ fields: ['test'] });
expect(stub).to.have.been.calledWith('/items/test/', { params: { fields: ['test'] } });
});
it('Adds the PK when set', async () => {
const stub = sandbox.stub(handler['axios'], 'get').resolves({ data: '' });
await handler.read(15);
expect(stub).to.have.been.calledWith('/items/test/15');
});
it('Sets both pk and query', async () => {
const stub = sandbox.stub(handler['axios'], 'get').resolves({ data: '' });
await handler.read(15, { fields: ['test'] });
expect(stub).to.have.been.calledWith('/items/test/15', {
params: { fields: ['test'] },
});
});
});
describe('update', () => {
it('Updates a single item to a new value', async () => {
const stub = sandbox.stub(handler['axios'], 'patch').resolves({ data: '' });
await handler.update(15, { test: 'new value' });
expect(stub).to.have.been.calledWith(
'/items/test/15',
{ test: 'new value' },
{ params: undefined }
);
await handler.update(15, { test: 'new value' }, { fields: ['test'] });
expect(stub).to.have.been.calledWith(
'/items/test/15',
{ test: 'new value' },
{ params: { fields: ['test'] } }
);
});
it('Updates multiple items to a new value', async () => {
const stub = sandbox.stub(handler['axios'], 'patch').resolves({ data: '' });
await handler.update([15, 42], { test: 'new value' });
expect(stub).to.have.been.calledWith(
'/items/test/15,42',
{ test: 'new value' },
{ params: undefined }
);
await handler.update([15, 42], { test: 'new value' }, { fields: ['test'] });
expect(stub).to.have.been.calledWith(
'/items/test/15,42',
{ test: 'new value' },
{ params: { fields: ['test'] } }
);
});
it('Allows updating by query', async () => {
const stub = sandbox.stub(handler['axios'], 'patch').resolves({ data: '' });
await handler.update({ archived: true }, { filter: {} });
expect(stub).to.have.been.calledWith(
'/items/test/',
{ archived: true },
{ params: { filter: {} } }
);
});
});
describe('delete', () => {
it('Deletes a single item', async () => {
const stub = sandbox.stub(handler['axios'], 'delete').resolves();
await handler.delete(15);
expect(stub).to.have.been.calledWith('/items/test/15');
});
it('Deletes multiple items', async () => {
const stub = sandbox.stub(handler['axios'], 'delete').resolves();
await handler.delete([15, 42]);
expect(stub).to.have.been.calledWith('/items/test/15,42');
});
});
});

View File

@@ -0,0 +1,27 @@
import { PermissionsHandler, ItemsHandler } from '../../src/handlers';
import axios, { AxiosInstance } from 'axios';
import sinon, { SinonSandbox } from 'sinon';
import chai, { expect } from 'chai';
import sinonChai from 'sinon-chai';
chai.use(sinonChai);
describe('PermissionsHandler', () => {
let sandbox: SinonSandbox;
let axiosInstance: AxiosInstance;
let handler: PermissionsHandler;
beforeEach(() => {
sandbox = sinon.createSandbox();
axiosInstance = axios.create();
handler = new PermissionsHandler(axiosInstance);
});
afterEach(() => {
sandbox.restore();
});
it('Extends ItemsHandler', () => {
expect(handler).to.be.instanceOf(ItemsHandler);
});
});

View File

@@ -0,0 +1,27 @@
import { PresetsHandler, ItemsHandler } from '../../src/handlers';
import axios, { AxiosInstance } from 'axios';
import sinon, { SinonSandbox } from 'sinon';
import chai, { expect } from 'chai';
import sinonChai from 'sinon-chai';
chai.use(sinonChai);
describe('PresetsHandler', () => {
let sandbox: SinonSandbox;
let axiosInstance: AxiosInstance;
let handler: PresetsHandler;
beforeEach(() => {
sandbox = sinon.createSandbox();
axiosInstance = axios.create();
handler = new PresetsHandler(axiosInstance);
});
afterEach(() => {
sandbox.restore();
});
it('Extends ItemsHandler', () => {
expect(handler).to.be.instanceOf(ItemsHandler);
});
});

View File

@@ -0,0 +1,27 @@
import { RelationsHandler, ItemsHandler } from '../../src/handlers';
import axios, { AxiosInstance } from 'axios';
import sinon, { SinonSandbox } from 'sinon';
import chai, { expect } from 'chai';
import sinonChai from 'sinon-chai';
chai.use(sinonChai);
describe('RelationsHandler', () => {
let sandbox: SinonSandbox;
let axiosInstance: AxiosInstance;
let handler: RelationsHandler;
beforeEach(() => {
sandbox = sinon.createSandbox();
axiosInstance = axios.create();
handler = new RelationsHandler(axiosInstance);
});
afterEach(() => {
sandbox.restore();
});
it('Extends ItemsHandler', () => {
expect(handler).to.be.instanceOf(ItemsHandler);
});
});

View File

@@ -0,0 +1,27 @@
import { RevisionsHandler, ItemsHandler } from '../../src/handlers';
import axios, { AxiosInstance } from 'axios';
import sinon, { SinonSandbox } from 'sinon';
import chai, { expect } from 'chai';
import sinonChai from 'sinon-chai';
chai.use(sinonChai);
describe('RevisionsHandler', () => {
let sandbox: SinonSandbox;
let axiosInstance: AxiosInstance;
let handler: RevisionsHandler;
beforeEach(() => {
sandbox = sinon.createSandbox();
axiosInstance = axios.create();
handler = new RevisionsHandler(axiosInstance);
});
afterEach(() => {
sandbox.restore();
});
it('Extends ItemsHandler', () => {
expect(handler).to.be.instanceOf(ItemsHandler);
});
});

View File

@@ -0,0 +1,27 @@
import { PresetsHandler, ItemsHandler } from '../../src/handlers';
import axios, { AxiosInstance } from 'axios';
import sinon, { SinonSandbox } from 'sinon';
import chai, { expect } from 'chai';
import sinonChai from 'sinon-chai';
chai.use(sinonChai);
describe('PresetsHandler', () => {
let sandbox: SinonSandbox;
let axiosInstance: AxiosInstance;
let handler: PresetsHandler;
beforeEach(() => {
sandbox = sinon.createSandbox();
axiosInstance = axios.create();
handler = new PresetsHandler(axiosInstance);
});
afterEach(() => {
sandbox.restore();
});
it('Extends ItemsHandler', () => {
expect(handler).to.be.instanceOf(ItemsHandler);
});
});

View File

@@ -0,0 +1,51 @@
import { ServerHandler } from '../../src/handlers/server';
import axios, { AxiosInstance } from 'axios';
import sinon, { SinonSandbox } from 'sinon';
import chai, { expect } from 'chai';
import sinonChai from 'sinon-chai';
chai.use(sinonChai);
describe('ServerHandler', () => {
let sandbox: SinonSandbox;
let axiosInstance: AxiosInstance;
let handler: ServerHandler;
beforeEach(() => {
sandbox = sinon.createSandbox();
axiosInstance = axios.create();
handler = new ServerHandler(axiosInstance);
});
afterEach(() => {
sandbox.restore();
});
describe('specs.oas', () => {
it('Calls the /server/specs/oas endpoint', async () => {
const stub = sandbox
.stub(handler['axios'], 'get')
.returns(Promise.resolve({ data: '' }));
await handler.specs.oas();
expect(stub).to.have.been.calledWith('/server/specs/oas');
});
});
describe('ping', () => {
it('Calls the /server/ping endpoint', async () => {
const stub = sandbox.stub(handler['axios'], 'get').returns(Promise.resolve('pong'));
await handler.ping();
expect(stub).to.have.been.calledWith('/server/ping');
});
});
describe('info', () => {
it('Calls the /server/info endpoint', async () => {
const stub = sandbox
.stub(handler['axios'], 'get')
.returns(Promise.resolve({ data: '' }));
await handler.info();
expect(stub).to.have.been.calledWith('/server/info');
});
});
});

View File

@@ -0,0 +1,27 @@
import { SettingsHandler, ItemsHandler } from '../../src/handlers';
import axios, { AxiosInstance } from 'axios';
import sinon, { SinonSandbox } from 'sinon';
import chai, { expect } from 'chai';
import sinonChai from 'sinon-chai';
chai.use(sinonChai);
describe('SettingsHandler', () => {
let sandbox: SinonSandbox;
let axiosInstance: AxiosInstance;
let handler: SettingsHandler;
beforeEach(() => {
sandbox = sinon.createSandbox();
axiosInstance = axios.create();
handler = new SettingsHandler(axiosInstance);
});
afterEach(() => {
sandbox.restore();
});
it('Extends ItemsHandler', () => {
expect(handler).to.be.instanceOf(ItemsHandler);
});
});

View File

@@ -0,0 +1,118 @@
import { UsersHandler, ItemsHandler } from '../../src/handlers';
import axios, { AxiosInstance } from 'axios';
import sinon, { SinonSandbox } from 'sinon';
import chai, { expect } from 'chai';
import sinonChai from 'sinon-chai';
chai.use(sinonChai);
describe('UsersHandler', () => {
let sandbox: SinonSandbox;
let axiosInstance: AxiosInstance;
let handler: UsersHandler;
beforeEach(() => {
sandbox = sinon.createSandbox();
axiosInstance = axios.create();
handler = new UsersHandler(axiosInstance);
});
afterEach(() => {
sandbox.restore();
});
it('Extends ItemsHandler', () => {
expect(handler).to.be.instanceOf(ItemsHandler);
});
describe('invite', () => {
it('Calls the /users/invite endpoint', async () => {
const stub = sandbox.stub(handler.axios, 'post').resolves(Promise.resolve());
await handler.invite('admin@example.com', 'abc');
expect(stub).to.have.been.calledWith('/users/invite', {
email: 'admin@example.com',
role: 'abc',
});
await handler.invite(['admin@example.com', 'user@example.com'], 'abc');
expect(stub).to.have.been.calledWith('/users/invite', {
email: ['admin@example.com', 'user@example.com'],
role: 'abc',
});
});
});
describe('acceptInvite', () => {
it('Calls the /users/invite/accept endpoint', async () => {
const stub = sandbox.stub(handler.axios, 'post').resolves(Promise.resolve());
await handler.acceptInvite('abc.def.ghi', 'p455w0rd');
expect(stub).to.have.been.calledWith('/users/invite/accept', {
token: 'abc.def.ghi',
password: 'p455w0rd',
});
});
});
describe('tfa.enable', () => {
it('Calls the /users/tfa/enable endpoint', async () => {
const stub = sandbox.stub(handler.axios, 'post').resolves(Promise.resolve());
await handler.tfa.enable('p455w0rd');
expect(stub).to.have.been.calledWith('/users/tfa/enable', {
password: 'p455w0rd',
});
});
});
describe('tfa.disable', () => {
it('Calls the /users/tfa/disable endpoint', async () => {
const stub = sandbox.stub(handler.axios, 'post').resolves(Promise.resolve());
await handler.tfa.disable('351851');
expect(stub).to.have.been.calledWith('/users/tfa/disable', {
otp: '351851',
});
});
});
describe('me.read', () => {
it('Calls the /users/me endpoint', async () => {
const stub = sandbox.stub(handler.axios, 'get').resolves(Promise.resolve({ data: {} }));
await handler.me.read();
expect(stub).to.have.been.calledWith('/users/me');
await handler.me.read({ fields: ['first_name'] });
expect(stub).to.have.been.calledWith('/users/me', {
params: { fields: ['first_name'] },
});
});
});
describe('me.update', () => {
it('Calls the /users/me endpoint', async () => {
const stub = sandbox
.stub(handler.axios, 'patch')
.resolves(Promise.resolve({ data: {} }));
await handler.me.update({ first_name: 'Rijk' });
expect(stub).to.have.been.calledWith(
'/users/me',
{ first_name: 'Rijk' },
{ params: undefined }
);
await handler.me.update({ first_name: 'Rijk' }, { fields: ['first_name'] });
expect(stub).to.have.been.calledWith(
'/users/me',
{ first_name: 'Rijk' },
{ params: { fields: ['first_name'] } }
);
});
});
});

View File

@@ -0,0 +1,104 @@
import { UtilsHandler } from '../../src/handlers/utils';
import axios, { AxiosInstance } from 'axios';
import sinon, { SinonSandbox } from 'sinon';
import chai, { expect } from 'chai';
import sinonChai from 'sinon-chai';
chai.use(sinonChai);
describe('UtilsHandler', () => {
let sandbox: SinonSandbox;
let axiosInstance: AxiosInstance;
let handler: UtilsHandler;
beforeEach(() => {
sandbox = sinon.createSandbox();
axiosInstance = axios.create();
handler = new UtilsHandler(axiosInstance);
});
afterEach(() => {
sandbox.restore();
});
describe('random.string', () => {
it('Calls the /utils/random/string endpoint', async () => {
const stub = sandbox
.stub(handler['axios'], 'get')
.returns(Promise.resolve({ data: '' }));
await handler.random.string();
expect(stub).to.have.been.calledWith('/utils/random/string', {
params: { length: 32 },
});
});
it('Passes the parameter to the length query param', async () => {
const stub = sandbox
.stub(handler['axios'], 'get')
.returns(Promise.resolve({ data: '' }));
await handler.random.string(15);
expect(stub).to.have.been.calledWith('/utils/random/string', {
params: { length: 15 },
});
});
});
describe('hash.generate', () => {
it('Calls the /utils/hash/generate endpoint', async () => {
const stub = sandbox
.stub(handler['axios'], 'post')
.returns(Promise.resolve({ data: '' }));
await handler.hash.generate('test');
expect(stub).to.have.been.calledWith('/utils/hash/generate');
});
it('Passes the parameter as string in body', async () => {
const stub = sandbox
.stub(handler['axios'], 'post')
.returns(Promise.resolve({ data: '' }));
await handler.hash.generate('test');
expect(stub).to.have.been.calledWith('/utils/hash/generate', { string: 'test' });
});
});
describe('hash.verify', () => {
it('Calls the /utils/hash/verify endpoint', async () => {
const stub = sandbox
.stub(handler['axios'], 'post')
.returns(Promise.resolve({ data: '' }));
await handler.hash.verify('test', '$argonHash');
expect(stub).to.have.been.calledWith('/utils/hash/verify');
});
it('Passes the parameters as string, hash in body', async () => {
const stub = sandbox
.stub(handler['axios'], 'post')
.returns(Promise.resolve({ data: '' }));
await handler.hash.verify('test', '$argonHash');
expect(stub).to.have.been.calledWith('/utils/hash/verify', {
string: 'test',
hash: '$argonHash',
});
});
});
describe('sort', () => {
it('Calls the /utils/sort/:collection endpoint', async () => {
const stub = sandbox
.stub(handler['axios'], 'post')
.returns(Promise.resolve({ data: '' }));
await handler.sort('articles', 10, 15);
expect(stub).to.have.been.calledWith('/utils/sort/articles', { item: 10, to: 15 });
});
});
describe('revert', () => {
it('Calls the /utils/revert/:revision endpoint', async () => {
const stub = sandbox
.stub(handler['axios'], 'post')
.returns(Promise.resolve({ data: '' }));
await handler.revert(25);
expect(stub).to.have.been.calledWith('/utils/revert/25');
});
});
});

View File

@@ -0,0 +1,135 @@
import DirectusSDK from '../src/index';
import {
ItemsHandler,
UtilsHandler,
ActivityHandler,
FoldersHandler,
PermissionsHandler,
PresetsHandler,
RolesHandler,
UsersHandler,
SettingsHandler,
FilesHandler,
AuthHandler,
CollectionsHandler,
FieldsHandler,
RelationsHandler,
RevisionsHandler,
ServerHandler,
} from '../src/handlers/';
import { expect } from 'chai';
import { MemoryStore } from '../src/utils';
describe('DirectusSDK', () => {
let directus: DirectusSDK;
beforeEach(() => (directus = new DirectusSDK('http://example.com')));
it('Initializes', () => {
expect(directus).to.be.instanceOf(DirectusSDK);
});
it('Sets the passed authOptions', () => {
const fakeStore = { async getItem() {}, async setItem() {} };
const directusWithOptions = new DirectusSDK('http://example.com', {
auth: {
autoRefresh: false,
storage: fakeStore,
mode: 'json',
},
});
expect(directusWithOptions['authOptions'].autoRefresh).to.be.false;
expect(directusWithOptions['authOptions'].mode).to.equal('json');
expect(directusWithOptions['authOptions'].storage).to.equal(fakeStore);
});
it('Defaults to the correct auth options', () => {
expect(directus['authOptions'].autoRefresh).to.be.true;
expect(directus['authOptions'].mode).to.equal('cookie');
expect(directus['authOptions'].storage).to.be.instanceOf(MemoryStore);
});
it('Gets / Sets URL', () => {
expect(directus.url).to.equal('http://example.com');
directus.url = 'http://different.example.com';
expect(directus.url).to.equal('http://different.example.com');
});
it('Syncs URL with Axios base instance', () => {
expect(directus.axios.defaults.baseURL).to.equal('http://example.com');
directus.url = 'http://different.example.com';
expect(directus.axios.defaults.baseURL).to.equal('http://different.example.com');
});
it('Returns ItemsHandler instance for #items', () => {
expect(directus.items('articles')).to.be.instanceOf(ItemsHandler);
});
it('Errors when trying to read a system collection directly', () => {
expect(() => directus.items('directus_files')).to.throw();
});
it('Returns ActivityHandler instance for #activity', () => {
expect(directus.activity).to.be.instanceOf(ActivityHandler);
});
it('Returns AuthHandler for #auth', () => {
expect(directus.auth).to.be.instanceOf(AuthHandler);
});
it('Returns CollectionsHandler for #collections', () => {
expect(directus.collections).to.be.instanceOf(CollectionsHandler);
});
it('Returns FieldsHandler for #fields', () => {
expect(directus.fields).to.be.instanceOf(FieldsHandler);
});
it('Returns FilesHandler for #users', () => {
expect(directus.files).to.be.instanceOf(FilesHandler);
});
it('Returns FoldersHandler for #folders', () => {
expect(directus.folders).to.be.instanceOf(FoldersHandler);
});
it('Returns PermissionsHandler for #permissions', () => {
expect(directus.permissions).to.be.instanceOf(PermissionsHandler);
});
it('Returns PresetsHandler for #presets', () => {
expect(directus.presets).to.be.instanceOf(PresetsHandler);
});
it('Returns RelationsHandler for #roles', () => {
expect(directus.relations).to.be.instanceOf(RelationsHandler);
});
it('Returns RevisionsHandler for #revisions', () => {
expect(directus.revisions).to.be.instanceOf(RevisionsHandler);
});
it('Returns RolesHandler for #roles', () => {
expect(directus.roles).to.be.instanceOf(RolesHandler);
});
it('Returns ServerHandler for #server', () => {
expect(directus.server).to.be.instanceOf(ServerHandler);
});
it('Returns SettingsHandler for #settings', () => {
expect(directus.settings).to.be.instanceOf(SettingsHandler);
});
it('Returns UsersHandler for #users', () => {
expect(directus.users).to.be.instanceOf(UsersHandler);
});
it('Returns UtilsHandler for #utils', () => {
expect(directus.utils).to.be.instanceOf(UtilsHandler);
});
});

View File

@@ -0,0 +1,19 @@
import { MemoryStore } from '../src/utils/';
import { expect } from 'chai';
describe('Utils', () => {
describe('MemoryStore', () => {
it('Gets values based on key', async () => {
const store = new MemoryStore();
store['values'].test = 'test';
const result = await store.getItem('test');
expect(result).to.equal('test');
});
it('Sets value based on key', async () => {
const store = new MemoryStore();
await store.setItem('test', 'test');
expect(store['values'].test).to.equal('test');
});
});
});

View File

@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"declaration": true,
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"sourceMap": true
},
"include": ["src"],
"exclude": ["node_modules", "tests", "dist"]
}