Repository: twitch-apis/twitch-js
Pull request #168 (2018-10-05 12:03:04)

Feature: Helix API support
## Proposed changes

Proposal for #92 and open for alternatives.

## Types of changes

* [ ] Bugfix (non-breaking change which fixes an issue)
* [x] New feature (non-breaking change which adds functionality)
* [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
* [ ] Chore (change that does not fix an issue or add functionality, but improves TwitchJS in some other way)

## Checklist

* [x] I have read the [CONTRIBUTING](https://github.com/twitch-apis/twitch-js/blob/master/CONTRIBUTING.md) doc
* [x] My PR is named according to [CONTRIBUTING](https://github.com/twitch-apis/twitch-js/blob/master/CONTRIBUTING.md) doc
* [x] Lint and unit tests pass locally with my changes
* [x] I have added tests that prove my fix is effective or that my feature works
* [x] I have added necessary documentation (if appropriate)
* [x] Any dependent changes have been merged and published in downstream modules

codecov[bot]: # [Codecov](https://codecov.io/gh/twitch-apis/twitch-js/pull/168?src=pr&el=h1) Report
> Merging [#168](https://codecov.io/gh/twitch-apis/twitch-js/pull/168?src=pr&el=desc) into [next](https://codecov.io/gh/twitch-apis/twitch-js/commit/38a45d632f9071774eadde8636bb39796223907d?src=pr&el=desc) will **increase** coverage by `0.22%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/twitch-apis/twitch-js/pull/168/graphs/tree.svg?width=650&token=qKety0Jenl&height=150&src=pr)](https://codecov.io/gh/twitch-apis/twitch-js/pull/168?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##             next     #168      +/-   ##
==========================================
+ Coverage   96.99%   97.21%   +0.22%     
==========================================
  Files          21       21              
  Lines         532      539       +7     
  Branches       74       75       +1     
==========================================
+ Hits          516      524       +8     
  Misses         15       15              
+ Partials        1        0       -1
```


| [Impacted Files](https://codecov.io/gh/twitch-apis/twitch-js/pull/168?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/Api/utils/validators.js](https://codecov.io/gh/twitch-apis/twitch-js/pull/168/diff?src=pr&el=tree#diff-c3JjL0FwaS91dGlscy92YWxpZGF0b3JzLmpz) | `87.5% <ø> (ø)` | :arrow_up: |
| [src/Api/constants.js](https://codecov.io/gh/twitch-apis/twitch-js/pull/168/diff?src=pr&el=tree#diff-c3JjL0FwaS9jb25zdGFudHMuanM=) | `100% <100%> (ø)` | :arrow_up: |
| [src/Api/index.js](https://codecov.io/gh/twitch-apis/twitch-js/pull/168/diff?src=pr&el=tree#diff-c3JjL0FwaS9pbmRleC5qcw==) | `100% <100%> (+2.7%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/twitch-apis/twitch-js/pull/168?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/twitch-apis/twitch-js/pull/168?src=pr&el=footer). Last update [38a45d6...02a41be](https://codecov.io/gh/twitch-apis/twitch-js/pull/168?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/docs/reference/typedef.md b/docs/reference/typedef.md
index 60f7906..4575431 100644
--- a/docs/reference/typedef.md
+++ b/docs/reference/typedef.md
@@ -119,8 +119,6 @@ API options
     </tr><tr>
     <td>[token]</td><td><code>string</code></td><td></td><td><p>Optional if clientId is defined.</p>
 </td>
-    </tr><tr>
-    <td>[urlRoot]</td><td><code>string</code></td><td></td><td></td>
     </tr><tr>
     <td>[debug]</td><td><code>boolean</code></td><td><code>false</code></td><td></td>
     </tr><tr>
diff --git a/src/Api/constants.js b/src/Api/constants.js
index 6bf397e..f2b7078 100644
--- a/src/Api/constants.js
+++ b/src/Api/constants.js
@@ -1,3 +1,4 @@
+export const HELIX_URL_ROOT = 'https://api.twitch.tv/helix'
 export const KRAKEN_URL_ROOT = 'https://api.twitch.tv/kraken'
 
 /** @typedef {string} ApiReadyState */
diff --git a/src/Api/index.js b/src/Api/index.js
index ac97b89..a7b4b54 100644
--- a/src/Api/index.js
+++ b/src/Api/index.js
@@ -1,5 +1,5 @@
 import camelcaseKeys from 'camelcase-keys'
-import { get, includes, replace } from 'lodash'
+import { get, includes, toUpper } from 'lodash'
 
 import fetchUtil from '../utils/fetch'
 import * as Errors from '../utils/fetch/Errors'
@@ -63,10 +63,6 @@ class Api {
     return this._options
   }
 
-  set readyState(readyState) {
-    this._readyState = constants.READY_STATES[readyState] ? readyState : 0
-  }
-
   get readyState() {
     return this._readyState
   }
@@ -84,12 +80,30 @@ class Api {
     this.options = { ...options, clientId, token }
   }
 
-  getHeaders() {
+  getAuthorizationType(version) {
+    if (toUpper(version) === 'HELIX') {
+      return 'Bearer'
+    }
+
+    return 'OAuth'
+  }
+
+  getBaseUrl(version) {
+    if (toUpper(version) === 'HELIX') {
+      return constants.HELIX_URL_ROOT
+    }
+
+    return constants.KRAKEN_URL_ROOT
+  }
+
+  getHeaders(version) {
     const { clientId, token } = this.options
+    const authType = this.getAuthorizationType(version)
+
     return {
       Accept: 'application/vnd.twitchtv.v5+json',
       'Client-ID': clientId ? clientId : undefined,
-      Authorization: token ? `OAuth ${token}` : undefined,
+      Authorization: token ? `${authType} ${token}` : undefined,
     }
   }
 
@@ -169,14 +183,14 @@ class Api {
 }
 
 function handleFetch(maybeUrl = '', options = {}) {
-  const url = replace(maybeUrl, /^\//g, '')
+  const [, version, url] = /^(?:([a-z]+):)?\/?(.*)$/i.exec(maybeUrl)
 
   const request = () =>
-    fetchUtil(`${this.options.urlRoot}/${url}`, {
+    fetchUtil(`${this.getBaseUrl(version)}/${url}`, {
       ...options,
       headers: {
         ...options.headers,
-        ...this.getHeaders(),
+        ...this.getHeaders(version),
       },
     }).then(res => camelcaseKeys(res, { deep: true }))
 
diff --git a/src/Api/utils/validators.js b/src/Api/utils/validators.js
index 3604f85..2f0348e 100644
--- a/src/Api/utils/validators.js
+++ b/src/Api/utils/validators.js
@@ -7,15 +7,12 @@ import {
   isString,
 } from 'lodash'
 
-import * as constants from '../constants'
-
 const apiOptions = maybeOptions => {
   /**
    * API options
    * @typedef {Object} ApiOptions
    * @property {string} [clientId] Optional if token is defined.
    * @property {string} [token] Optional if clientId is defined.
-   * @property {string} [urlRoot]
    * @property {boolean} [debug=false]
    * @property {function} [onAuthenticationFailure]
    *
@@ -39,7 +36,6 @@ const apiOptions = maybeOptions => {
     {},
     { ...maybeOptions },
     {
-      urlRoot: constants.KRAKEN_URL_ROOT,
       debug: false,
       onAuthenticationFailure: () => Promise.reject(),
     },


Test patch:
diff --git a/src/Api/__tests__/index.test.js b/src/Api/__tests__/index.test.js
index 4ccced8..dfab2eb 100644
--- a/src/Api/__tests__/index.test.js
+++ b/src/Api/__tests__/index.test.js
@@ -1,4 +1,5 @@
 import Api from '../index'
+import * as constants from '../constants'
 import fetchUtil from '../../utils/fetch'
 import * as Errors from '../../utils/fetch/Errors'
 
@@ -133,6 +134,50 @@ describe('Api', () => {
     })
   })
 
+  describe('versions', () => {
+    test('should fallback to the Kraken endpoint', () => {
+      const api = new Api(options)
+
+      const endpoint = 'ENDPOINT'
+      const opts = { a: { b: 'c ' } }
+
+      return api.get(endpoint, opts).then(() => {
+        const [actualEndpoint, actualOpts] = fetchUtil.mock.calls[0]
+
+        expect(actualEndpoint).toBe(`${constants.KRAKEN_URL_ROOT}/ENDPOINT`)
+        expect(actualOpts).toMatchObject(opts)
+      })
+    })
+
+    test('should call the Kraken endpoint', () => {
+      const api = new Api(options)
+
+      const endpoint = 'kraken:ENDPOINT'
+      const opts = { a: { b: 'c ' } }
+
+      return api.get(endpoint, opts).then(() => {
+        const [actualEndpoint, actualOpts] = fetchUtil.mock.calls[0]
+
+        expect(actualEndpoint).toBe(`${constants.KRAKEN_URL_ROOT}/ENDPOINT`)
+        expect(actualOpts).toMatchObject(opts)
+      })
+    })
+
+    test('should call the Helix endpoint', () => {
+      const api = new Api(options)
+
+      const endpoint = 'helix:ENDPOINT'
+      const opts = { a: { b: 'c ' } }
+
+      return api.get(endpoint, opts).then(() => {
+        const [actualEndpoint, actualOpts] = fetchUtil.mock.calls[0]
+
+        expect(actualEndpoint).toBe(`${constants.HELIX_URL_ROOT}/ENDPOINT`)
+        expect(actualOpts).toMatchObject(opts)
+      })
+    })
+  })
+
   describe('onAuthenticationFailure', () => {
     test('should call onAuthenticationFailure', done => {
       const onAuthenticationFailure = jest.fn(() => Promise.reject())