Skip to content

Commit

Permalink
Webhooks + Auto pagination (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
kylesurowiec authored Oct 25, 2022
1 parent 57dcb00 commit 7e32685
Show file tree
Hide file tree
Showing 73 changed files with 1,589 additions and 189 deletions.
12 changes: 1 addition & 11 deletions .github/workflows/eslint.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,3 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# ESLint is a tool for identifying and reporting on patterns
# found in ECMAScript/JavaScript code.
# More details at https://github.com/eslint/eslint
# and https://eslint.org

name: ESLint

on:
Expand All @@ -17,7 +8,7 @@ on:

jobs:
eslint:
name: Run eslint scanning
name: Lint
runs-on: ubuntu-latest
permissions:
contents: read
Expand All @@ -30,7 +21,6 @@ jobs:
- name: Install ESLint
run: |
npm install eslint@8.10.0
npm install @microsoft/eslint-formatter-sarif@2.1.7
- name: Run ESLint
run: npx eslint .
Expand Down
19 changes: 15 additions & 4 deletions README.MD
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<div>
<h1>Mindbody API</h1>
<p><b>Type safe library for interacting with Mindbody's Public API (v6) and Webhooks</b></p>
<img src="https://img.shields.io/github/package-json/v/splitpass/mindbody-api?color=blue&style=for-the-badge" alt="Latest version of splitpass/mindbody-api is 0.1.12">
<img src="https://img.shields.io/github/package-json/v/splitpass/mindbody-api?color=blue&style=for-the-badge" alt="Latest version of splitpass/mindbody-api is 0.2.0">
</div>
<br />

Expand Down Expand Up @@ -58,7 +58,7 @@ Config.setup({
Endpoints are logically separated based on the categories listed [here](https://developers.mindbodyonline.com/PublicDocumentation/V6#endpoints).

```ts
import { Class, Client, Staff } from 'mindbody-api-v6';
import { Class, Client, Staff, Webhooks } from 'mindbody-api-v6';

const classes = await Class.getClassSchedules({
siteID: '123',
Expand All @@ -79,23 +79,34 @@ const newClient = await Client.addClient({
*/
const staff = await Staff.getStaff({
siteID: '123',
// Automatically page through all results.
// Only applicable for paginated endpoints
autoPaginate: true,
});

/**
* Interact with Webhooks API
*
* https://developers.mindbodyonline.com/WebhooksDocumentation
*/
await Webhooks.Subscriptions.createSubscription({ ... });
```

### Types

All model definitions are exported under `MBType`. A full list of models can be found [here](https://developers.mindbodyonline.com/PublicDocumentation/V6#shared-resources). Additional models were added for easy access to complex types not listed in Mindbody's documentation

```ts
import { MBType } from 'mindbody-api-v6';
import type { MBType, MBWebhookType } from 'mindbody-api-v6';

const staff: MBType.Staff = ...;
const client: MBType.Client = ...;
// Webhook event types
const newClient: MBWebhookType.ClientCreated = ...;
```

## Roadmap

| Version | Features |
| ------- | ------------------------------ |
| 0.2.0 | Auto pagination, webhooks |
| 1.0.0 | Test coverage, type refinement |
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@
"node": ">=12.4"
},
"dependencies": {
"axios": "1.1.3"
"axios": "1.1.3",
"p-limit": "3.1.0"
},
"devDependencies": {
"@rollup/plugin-typescript": "^9.0.1",
Expand All @@ -61,4 +62,4 @@
"version": "pnpm build && git add .",
"postversion": "git push && git push --tags && rm -rf build/temp"
}
}
}
4 changes: 2 additions & 2 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions rollup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import typescript from '@rollup/plugin-typescript';
export default [
{
input: 'src/index.ts',
external: ['axios'],
external: ['axios', 'p-limit'],
output: [
{
file: 'dist/esm/index.mjs',
Expand All @@ -31,7 +31,7 @@ export default [
},
{
input: 'src/index.ts',
external: ['axios'],
external: ['axios', 'p-limit'],
output: [
{
file: 'dist/types.d.ts',
Expand Down
32 changes: 24 additions & 8 deletions src/http/BaseClient.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import type { AxiosInstance } from 'axios';
import type { ErrorResponse, Headers, TokenResponse } from '$http/types';
import type { WebhookErrorResponse } from '$webhooks/types/WebhookErrorResponse';

import axios, { AxiosError } from 'axios';
import Config from '$Config';
import MindbodyError from '$http/MindbodyError';
import * as TokenCache from '$http/TokenCache';

const BASE_URL = 'https://api.mindbodyonline.com/public/v6';
const API_BASE_URL = 'https://api.mindbodyonline.com/public/v6';
const WEBHOOKS_BASE_URL = 'https://mb-api.mindbodyonline.com/push/api/v1';
const TWENTY_FOUR_HOURS = 3600 * 1000 * 24;

export class BaseClient {
protected client: AxiosInstance;

protected constructor() {
this.client = axios.create({ baseURL: BASE_URL });
protected constructor(clientType: 'api-client' | 'webhooks-client') {
this.client = axios.create({
baseURL: clientType === 'api-client' ? API_BASE_URL : WEBHOOKS_BASE_URL,
});
this.client.interceptors.response.use(
res => res,
err => {
Expand All @@ -29,7 +33,10 @@ export class BaseClient {
);
}

const error = err.response.data as ErrorResponse;
const error = err.response.data as
| ErrorResponse
| WebhookErrorResponse;

throw new MindbodyError(error);
}

Expand All @@ -46,12 +53,21 @@ export class BaseClient {
return [this.client, headers];
}

protected basicHeaders(siteID: string): Headers {
return {
protected webhookRequest(): [AxiosInstance, Headers] {
return [this.client, this.basicHeaders()];
}

protected basicHeaders(siteID?: string): Headers {
const headers = {
'Content-Type': 'application/json',
'Api-Key': Config.getApiKey(),
SiteId: siteID,
};
} as Headers;

if (siteID != null) {
headers.SiteId = siteID;
}

return headers;
}

protected async authHeaders(siteID: string): Promise<Required<Headers>> {
Expand Down
38 changes: 33 additions & 5 deletions src/http/MindbodyClient.ts → src/http/MindbodyAPIClient.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type {
PaginatedResponse,
RequestArgsDelete,
RequestArgsGet,
RequestArgsGetOptionalParams,
Expand All @@ -7,17 +8,18 @@ import type {
import type { Kv } from '$http/types/Kv';

import { BaseClient } from '$http/BaseClient';
import { autoPager } from '$http/autoPager';

type Returnable = Kv | (string | number)[];
export type Returnable = Kv | (string | number)[];

export class MindbodyClient extends BaseClient {
private static instance?: MindbodyClient = new this();
export class MindbodyAPIClient extends BaseClient {
private static instance?: MindbodyAPIClient = new this();

private constructor() {
super();
super('api-client');
}

public static get(): MindbodyClient {
public static get(): MindbodyAPIClient {
return this.instance ?? (this.instance = new this());
}

Expand All @@ -35,6 +37,32 @@ export class MindbodyClient extends BaseClient {
return res.data;
}

public async getPaginated<R extends Returnable>(
endpoint: string,
args: (RequestArgsGet<Kv> | RequestArgsGetOptionalParams<Kv>) & {
objectIndexKey: string;
},
): Promise<PaginatedResponse<R>> {
const [client, headers] = await this.request(args.siteID);
const res = await client<PaginatedResponse<R>>(endpoint, {
method: 'GET',
headers,
params: args.params,
});

if (args.autoPaginate) {
return await autoPager({
client: client,
endpoint: endpoint,
headers: headers,
firstPage: res.data,
objectIndexKey: args.objectIndexKey,
});
}

return res.data;
}

public async post<R extends Returnable>(
endpoint: string,
args: RequestArgsPost<Kv>,
Expand Down
21 changes: 17 additions & 4 deletions src/http/MindbodyError.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
import type { ErrorCode, ErrorResponse } from '$http/types';
import type { WebhookErrorCode, WebhookErrorResponse } from '$webhooks/types';

export default class MindbodyError extends Error {
public code: ErrorCode;
public code: ErrorCode | WebhookErrorCode[];

constructor(errorResponse: ErrorResponse) {
constructor(errorResponse: ErrorResponse | WebhookErrorResponse) {
super();
this.message = errorResponse.Error.Message;
this.code = errorResponse.Error.Code;

if (Object.keys(errorResponse).includes('errors')) {
const error = errorResponse as WebhookErrorResponse;
this.code = error.errors.map(e => e.errorType);
this.message = error.errors
.map(e => e.errorMessage)
.join('. ')
.trim();
return;
}

const error = errorResponse as ErrorResponse;
this.code = error.Error.Code;
this.message = error.Error.Message;
}
}
69 changes: 69 additions & 0 deletions src/http/MindbodyWebhooksClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import type {
WebhooksRequestsArgsPatch,
WebhooksRequestsArgsPost,
} from '$http/types';
import type { Kv } from '$http/types/Kv';

import { BaseClient } from '$http/BaseClient';

type Returnable = Kv | (string | number)[];

export class MindbodyWebhooksClient extends BaseClient {
private static instance?: MindbodyWebhooksClient = new this();

private constructor() {
super('webhooks-client');
}

public static get(): MindbodyWebhooksClient {
return this.instance ?? (this.instance = new this());
}

public async get<R extends Returnable>(endpoint: string): Promise<R> {
const [client, headers] = this.webhookRequest();
const res = await client<R>(endpoint, {
method: 'GET',
headers,
});

return res.data;
}

public async post<R extends Returnable>(
endpoint: string,
args: WebhooksRequestsArgsPost<Kv>,
): Promise<R> {
const [client, headers] = this.webhookRequest();
const res = await client<R>(endpoint, {
method: 'POST',
headers,
data: args,
});

return res.data;
}

public async patch<R extends Returnable>(
endpoint: string,
args: WebhooksRequestsArgsPatch<Kv>,
): Promise<R> {
const [client, headers] = this.webhookRequest();
const res = await client<R>(endpoint, {
method: 'PATCH',
headers,
data: args,
});

return res.data;
}

public async delete<R extends Returnable>(endpoint: string): Promise<R> {
const [client, headers] = this.webhookRequest();
const res = await client.delete<R>(endpoint, {
method: 'DELETE',
headers,
});

return res.data;
}
}
Loading

0 comments on commit 7e32685

Please sign in to comment.