diff --git a/README.md b/README.md
index 42b0708..1ecd891 100644
--- a/README.md
+++ b/README.md
@@ -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)
diff --git a/docs/README.md b/docs/README.md
index 29bda20..5544a6c 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -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
diff --git a/docs/users.md b/docs/users.md
new file mode 100644
index 0000000..0f4532e
--- /dev/null
+++ b/docs/users.md
@@ -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)
+
+
+
+## `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
+```
+
+### 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}!`);
+});
+```
+
+
+
+## `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
+```
+
+### 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}!`);
+});
+```
+
+
+[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
\ No newline at end of file
diff --git a/src/__tests__/__fixtures__/index.ts b/src/__tests__/__fixtures__/index.ts
index 25bc4ef..4d446ff 100644
--- a/src/__tests__/__fixtures__/index.ts
+++ b/src/__tests__/__fixtures__/index.ts
@@ -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: 'engineer@eventbrite.com',
+ 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',
diff --git a/src/__tests__/index.spec.ts b/src/__tests__/index.spec.ts
index ea5f3b5..ddb799f 100644
--- a/src/__tests__/index.spec.ts
+++ b/src/__tests__/index.spec.ts
@@ -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', () => {
@@ -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}`,
+ }),
+ })
+ );
+ });
+ });
});
diff --git a/src/__tests__/users.spec.ts b/src/__tests__/users.spec.ts
new file mode 100644
index 0000000..1f42dd2
--- /dev/null
+++ b/src/__tests__/users.spec.ts
@@ -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();
+ });
+});
diff --git a/src/baseApi.ts b/src/baseApi.ts
new file mode 100644
index 0000000..222212f
--- /dev/null
+++ b/src/baseApi.ts
@@ -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 = (
+ 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 = (
+ request: JSONRequest,
+ transformers: Array<(obj: T) => T>
+) => (url: string, options?: RequestInit) =>
+ request(url, options).then((response: T) =>
+ transformers.reduce((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 {
+ request: JSONRequest;
+
+ constructor(req: JSONRequest) {
+ this.request = makeJsonRequest(req, [transformKeysSnakeToCamel]);
+ }
+}
diff --git a/src/index.ts b/src/index.ts
index 0a5a4f4..2ae9ac3 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -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;
diff --git a/src/request.ts b/src/request.ts
index 4b908be..18ab058 100644
--- a/src/request.ts
+++ b/src/request.ts
@@ -14,7 +14,9 @@ const _checkStatus = (res: Response): Promise => {
return Promise.resolve(res);
};
-const _tryParseJSON = (res: Response): Promise => {
+const _tryParseJSON = (
+ res: Response
+): Promise => {
try {
return (
res
@@ -35,10 +37,10 @@ const _tryParseJSON = (res: Response): Promise => {
* with our JSON API. Parses the JSON, provides appropriate headers, and asserts
* a valid status from the server.
*/
-export const _fetchJSON = (
+const _fetchJSON = (
url: string,
{headers = {}, method = 'GET', mode, ...options}: RequestInit = {}
-): Promise<{}> => {
+): Promise => {
let fetchHeaders = headers as HeadersInit;
if (method !== 'GET') {
@@ -58,7 +60,7 @@ export const _fetchJSON = (
return fetch(url, fetchOptions)
.then(_checkStatus)
- .then(_tryParseJSON);
+ .then(_tryParseJSON);
};
const _hasArgumentsError = (responseData: JSONResponseData): boolean =>
@@ -141,9 +143,18 @@ const _catchStatusError = (res: Response): Promise =>
);
});
+export interface DefaultApiResponse {
+ [key: string]: any;
+}
+
/**
* Low-level method that makes fetch requests, returning the response formatted as JSON.
* It parses errors from API v3 and throws exceptions with those errors
*/
-export default (url: string, options?: RequestInit): Promise<{}> =>
- _fetchJSON(url, options).catch(_catchStatusError);
+const jsonRequest = (
+ url: string,
+ options?: RequestInit
+): Promise =>
+ _fetchJSON(url, options).catch(_catchStatusError);
+
+export default jsonRequest;
diff --git a/src/types.ts b/src/types.ts
index bc95fdd..159a2b3 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,9 +1,18 @@
+import {UserApi} from './users';
+
export interface SdkConfig {
token?: string;
baseUrl?: string;
}
+
+export type JSONRequest = (
+ apiPath: string,
+ options?: RequestInit
+) => Promise;
+
export interface Sdk {
- request: (apiPath: string, options?: RequestInit) => Promise<{}>;
+ request: JSONRequest;
+ users: UserApi;
}
export interface ArgumentErrors {
diff --git a/src/users.ts b/src/users.ts
new file mode 100644
index 0000000..c508390
--- /dev/null
+++ b/src/users.ts
@@ -0,0 +1,27 @@
+import {BaseApi} from './baseApi';
+
+export interface Email {
+ email?: string;
+ primary?: boolean;
+}
+
+export interface User {
+ id?: string;
+ firstName?: string;
+ lastName?: string;
+ imageId?: string;
+ email?: Email[];
+}
+
+/**
+ * API for working with Users
+ */
+export class UserApi extends BaseApi {
+ me() {
+ return this.request('/users/me/');
+ }
+
+ get(id: string) {
+ return this.request(`/users/${id}/`);
+ }
+}
diff --git a/tsconfig.json b/tsconfig.json
index 56608ac..7f623a1 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,6 +1,6 @@
{
"compilerOptions": {
- "lib": ["es2015", "dom"],
+ "lib": ["es2017", "dom"],
"noImplicitAny": true,
"noEmit": true
},