Skip to content

Commit

Permalink
feat(users): Add users collection to SDK (#52)
Browse files Browse the repository at this point in the history
Added a collection of helpful wrapper methods for interacting with the Users API.
  • Loading branch information
kwelch authored and kwelch-eb committed Jan 23, 2019
1 parent 8afff6d commit 4ac55ac
Show file tree
Hide file tree
Showing 12 changed files with 379 additions and 30 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ The official JavaScript SDK for the [Eventbrite v3 API](https://www.eventbrite.c

> NOTE: This library is still in **beta** as we flesh out the API of the SDK.
## ToC
## Table of Contents

* [Installation](#installation)
* [Quick Usage](#quick-usage)
Expand Down
3 changes: 2 additions & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

This SDK interface closely mirors the [Eventbrite v3 REST API](https://www.eventbrite.com/developer/v3/) endpoints that it wraps. The SDK provides many conveniences for making requests and processing responses to make it easier to use in the JavaScript environment.

## ToC
## Table of Contents

- [Including the package](#including-the-package)
- [Configuring a SDK object](#configuring-a-sdk-object)
- [`request()`](./request.md)
- [Users](./users.md)

## Including the package

Expand Down
66 changes: 66 additions & 0 deletions docs/users.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Users

This is a collection of method that are intended to be helpful wrappers around the [Users API endpoints](user-api-docs).

View the [User response object](user-object-reference) for details on the properties you'll get back with each response.

## Table on Contents

- [`sdk.users.me()`](#me)
- [`sdk.users.get(id)`](#getById)

<a id="me"></a>

## `sdk.users.me()`
Gets details about the current logged in user.

**Read [`/users/me` documentation](user-get-me) for more details.**

### API
```js
sdk.users.me(): Promise<User>
```

### Example

```js
const eventbrite = require('eventbrite');

// Create configured Eventbrite SDK
const sdk = eventbrite({token: 'OATH_TOKEN_HERE'});

sdk.users.me().then((user) => {
console.log(`Hi ${user.name}!`);
});
```

<a id="getById"></a>

## `sdk.users.get(id)`
Gets the details for a specific user by their user id.

**Read [`/users/:id` documentation](user-get-me) for more details.**

### API
```js
sdk.users.get(id: string): Promise<User>
```

### Example

```js
const eventbrite = require('eventbrite');

// Create configured Eventbrite SDK
const sdk = eventbrite({token: 'OATH_TOKEN_HERE'});

sdk.users.get('1234567890').then((user) => {
console.log(`Hi ${user.name}!`);
});
```

<!-- link reference section -->
[user-api-docs]: https://www.eventbrite.com/platform/api#/reference/user
[user-object-reference]: https://www.eventbrite.com/platform/api#/reference/user/retrieve-a-user
[user-by-id]: https://www.eventbrite.com/platform/api#/reference/user/retrieve-a-user
[user-get-me]: https://www.eventbrite.com/platform/api#/reference/user/retrieve/retrieve-your-user
16 changes: 16 additions & 0 deletions src/__tests__/__fixtures__/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,22 @@ export const MOCK_USERS_ME_RESPONSE_DATA = {
image_id: null as string,
};

export const MOCK_TRANSFORMED_USERS_ME_RESPONSE_DATA = {
emails: [
{
email: '[email protected]',
verified: true,
primary: true,
},
],
id: '142429416488',
name: 'Eventbrite Engineer',
firstName: 'Eventbrite',
lastName: 'Engineer',
isPublic: false,
imageId: null as string,
};

export const MOCK_INTERNAL_ERROR_RESPONSE_DATA = {
status_code: 500,
error: 'INTERNAL_ERROR',
Expand Down
42 changes: 41 additions & 1 deletion src/__tests__/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import {
restoreMockFetch,
getMockResponse,
} from './utils';
import {MOCK_USERS_ME_RESPONSE_DATA} from './__fixtures__';
import {
MOCK_USERS_ME_RESPONSE_DATA,
MOCK_TRANSFORMED_USERS_ME_RESPONSE_DATA,
} from './__fixtures__';

describe('configurations', () => {
it('does not error when creating sdk object w/o configuration', () => {
Expand Down Expand Up @@ -90,4 +93,41 @@ describe('request', () => {
})
);
});

describe('users collection', () => {
it('should return an object of functions', () => {
const {users} = eventbrite({
token: MOCK_TOKEN,
baseUrl: MOCK_BASE_URL,
});

expect(users).toBeDefined();
Object.keys(users).forEach((key) => {
const value = (users as any)[key];

expect(value).toBeInstanceOf(Function);
});
});

it('makes request to API base url override w/ specified token', async() => {
const {users} = eventbrite({
token: MOCK_TOKEN,
baseUrl: MOCK_BASE_URL,
});

await expect(users.me()).resolves.toEqual(
MOCK_TRANSFORMED_USERS_ME_RESPONSE_DATA
);

expect(getMockFetch()).toHaveBeenCalledTimes(1);
expect(getMockFetch()).toHaveBeenCalledWith(
`${MOCK_BASE_URL}/users/me/`,
expect.objectContaining({
headers: expect.objectContaining({
Authorization: `Bearer ${MOCK_TOKEN}`,
}),
})
);
});
});
});
112 changes: 112 additions & 0 deletions src/__tests__/users.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import {
mockFetch,
getMockFetch,
getMockResponse,
restoreMockFetch,
} from './utils';
import {
MOCK_USERS_ME_RESPONSE_DATA,
MOCK_TRANSFORMED_USERS_ME_RESPONSE_DATA,
} from './__fixtures__';

import request from '../request';
import {UserApi} from '../users';

describe('me()', () => {
it('calls fetch and calls fetch with appropriate defaults', async() => {
const users = new UserApi(request);

mockFetch(getMockResponse(MOCK_USERS_ME_RESPONSE_DATA));

await expect(users.me()).resolves.toEqual(
MOCK_TRANSFORMED_USERS_ME_RESPONSE_DATA
);

expect(getMockFetch()).toHaveBeenCalledTimes(1);
expect(getMockFetch()).toHaveBeenCalledWith(
'/users/me/',
expect.objectContaining({})
);

restoreMockFetch();
});

it('handle token missing requests', async() => {
const users = new UserApi(request);

mockFetch(
getMockResponse(
{
status_code: 401,
error_description:
'An OAuth token is required for all requests',
error: 'NO_AUTH',
},
{status: 401}
)
);

await expect(users.me()).rejects.toMatchObject({
response: expect.objectContaining({
status: 401,
statusText: 'Unauthorized',
ok: false,
}),
parsedError: {
description: 'An OAuth token is required for all requests',
error: 'NO_AUTH',
},
});

restoreMockFetch();
});
});

describe('get(id)', () => {
it('calls fetch and calls fetch with appropriate defaults', async() => {
const users = new UserApi(request);

mockFetch(getMockResponse(MOCK_USERS_ME_RESPONSE_DATA));

await expect(users.get('142429416488')).resolves.toEqual(
MOCK_TRANSFORMED_USERS_ME_RESPONSE_DATA
);

expect(getMockFetch()).toHaveBeenCalledTimes(1);
expect(getMockFetch()).toHaveBeenCalledWith(
'/users/142429416488/',
expect.objectContaining({})
);

restoreMockFetch();
});

it('should handle not found users', async() => {
const users = new UserApi(request);

mockFetch(
getMockResponse(
{
status_code: 404,
error_description: 'The user you requested does not exist.',
error: 'NOT_FOUND',
},
{status: 404}
)
);

await expect(users.get('123')).rejects.toMatchObject({
response: expect.objectContaining({
status: 404,
statusText: 'Not Found',
ok: false,
}),
parsedError: {
description: 'The user you requested does not exist.',
error: 'NOT_FOUND',
},
});

restoreMockFetch();
});
});
54 changes: 54 additions & 0 deletions src/baseApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import {JSONRequest} from './types';

const SNAKE_CASE_MATCH = /_\w/g;
const snakeToCamel = (str: string) =>
str.replace(SNAKE_CASE_MATCH, (chars: string) => chars[1].toUpperCase());

const transformKeysSnakeToCamel = <T extends { [key: string]: any } = {}>(
obj: T
) =>
Object.keys(obj).reduce((memo, key) => {
let newValue = obj[key];
const camelKey = snakeToCamel(key);

if (
newValue &&
typeof newValue === 'object' &&
!Array.isArray(newValue)
) {
newValue = transformKeysSnakeToCamel(newValue);
}

return {
...memo,
[camelKey]: newValue,
};
}, {}) as T;

/**
* Returns a function that sends a request, and transforms its results
*/
const makeJsonRequest = <T>(
request: JSONRequest,
transformers: Array<(obj: T) => T>
) => (url: string, options?: RequestInit) =>
request(url, options).then((response: T) =>
transformers.reduce<T>((acc, transformer) => {
let memo = acc;

memo = transformer(response);
return memo;
}, response)
);

/**
* Base API class for creating new API Classes.
* Also encapsulates default transformers such as snake to camel.
*/
export abstract class BaseApi<T> {
request: JSONRequest<T>;

constructor(req: JSONRequest<T>) {
this.request = makeJsonRequest(req, [transformKeysSnakeToCamel]);
}
}
51 changes: 32 additions & 19 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,43 @@
import {Sdk, SdkConfig} from './types';
import {Sdk, SdkConfig, JSONRequest} from './types';
import request from './request';
import {UserApi} from './users';

export * from './constants';

const DEFAULT_API_URL = 'https://www.eventbriteapi.com/v3';

type MakeRequestFunction = (baseUrl: string, token: string) => JSONRequest;

const makeRequest: MakeRequestFunction = (baseUrl: string, token: string) => (
endpoint,
options = {}
) => {
const url = `${baseUrl}${endpoint}`;
let requestOptions = options;

if (token) {
requestOptions = {
...requestOptions,
headers: {
...(requestOptions.headers || {}),
Authorization: `Bearer ${token}`,
},
};
}

return request(url, requestOptions);
};

const eventbrite = ({
baseUrl = DEFAULT_API_URL,
token,
}: SdkConfig = {}): Sdk => ({
request: (endpoint, options = {}) => {
const url = `${baseUrl}${endpoint}`;
let requestOptions = options;

if (token) {
requestOptions = {
...requestOptions,
headers: {
...(requestOptions.headers || {}),
Authorization: `Bearer ${token}`,
},
};
}

return request(url, requestOptions);
},
});
}: SdkConfig = {}): Sdk => {
const jsonRequest = makeRequest(baseUrl, token);

return {
request: jsonRequest,
users: new UserApi(jsonRequest),
};
};

export default eventbrite;
Loading

0 comments on commit 4ac55ac

Please sign in to comment.