Created
December 10, 2018 12:04
-
-
Save antony/c41e09a85ff3024af9784d11446d51d3 to your computer and use it in GitHub Desktop.
Universal Api Client for Sapper
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import querystring from 'querystring' | |
import fetch from 'node-fetch' | |
const base = `${process.env.apiUrl}/api/v1` | |
class HttpError extends Error { | |
} | |
class AccessDeniedError extends HttpError { | |
} | |
class NotFoundError extends HttpError { | |
} | |
class ConflictError extends HttpError { | |
} | |
const errors = { | |
401: AccessDeniedError, | |
404: NotFoundError, | |
409: ConflictError | |
} | |
function formatBody (body) { | |
if (!body) { return {} } | |
if (body instanceof FormData) { | |
return { | |
headers: { 'Content-Type': 'multipart/form-data' }, | |
body | |
} | |
} else { | |
return { | |
headers: { 'Content-Type': 'application/json' }, | |
body: JSON.stringify(body) | |
} | |
} | |
} | |
class Api { | |
constructor (client) { | |
if (client) { | |
this.client = client | |
} else if (typeof window !== 'undefined') { | |
this.client = window.fetch.bind(window) | |
} else { | |
this.client = fetch | |
} | |
} | |
async send (method, url, data, overrides = {}) { | |
const endpoint = url.includes('://') ? url : `${base}/${url}` | |
const body = formatBody(data) | |
const options = { | |
method, | |
cors: true, | |
credentials: 'include', | |
headers: { | |
'Accept': 'application/json' | |
}, | |
...overrides, | |
...body | |
} | |
let r | |
try { | |
r = await this.client(endpoint, options) | |
} catch (e) { | |
throw new Error(e.message) | |
} | |
try { | |
if (r.ok) { | |
return await r.json() | |
} | |
} catch (e) { | |
return undefined | |
} | |
if (Object.keys(errors).includes(`${r.status}`)) { | |
throw new errors[r.status](r.statusText) | |
} | |
throw new HttpError(`${r.status}: ${r.statusText} - ${await r.text()}`) | |
} | |
async callWithQuery (method, endpoint, query = {}) { | |
const qs = querystring.stringify(query) | |
return this.send(method, `${endpoint}${qs ? `?${qs}` : ''}`) | |
} | |
async get (endpoint, query = {}) { | |
return this.callWithQuery('get', endpoint, query) | |
} | |
async post (endpoint, payload) { | |
return this.send('post', endpoint, payload) | |
} | |
async put (endpoint, payload) { | |
return this.send('put', endpoint, payload) | |
} | |
async del (endpoint, query) { | |
return this.callWithQuery('delete', endpoint, query) | |
} | |
} | |
export { | |
Api, | |
AccessDeniedError, | |
NotFoundError, | |
HttpError, | |
ConflictError | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { stub } from 'sinon' | |
import { expect } from 'code' | |
import { Api, HttpError, AccessDeniedError } from '.' | |
describe('util/api', () => { | |
describe('#get()', () => { | |
let api | |
let clientStub | |
beforeEach(async () => { | |
global.FormData = Map | |
clientStub = stub() | |
api = new Api(clientStub) | |
}) | |
it('fetches data from url', async () => { | |
clientStub.resolves({ | |
ok: true, | |
json: stub().resolves({ foo: 'bar' }) | |
}) | |
expect( | |
await api.get('some/url') | |
).to.equal({ | |
foo: 'bar' | |
}) | |
}) | |
it('appends baseUrl if endpoint is relative', async () => { | |
clientStub.resolves({ | |
ok: true, | |
json: stub() | |
}) | |
await api.get('some/url') | |
expect(clientStub.firstCall.args[0]).to.equal(`undefined/api/v1/some/url`) | |
}) | |
it('fetches data from url with query params', async () => { | |
clientStub.resolves({ | |
ok: true, | |
json: stub() | |
}) | |
await api.get('some/url', { foo: 'bar', baz: 'qux' }) | |
expect(clientStub.firstCall.args[0]).to.endWith('/some/url?foo=bar&baz=qux') | |
}) | |
it('fetches data from url with query params', async () => { | |
clientStub.resolves({ | |
ok: true, | |
json: stub() | |
}) | |
await api.get('some/url', { foo: [ 'bar', 'qux' ] }) | |
expect(clientStub.firstCall.args[0]).to.endWith('/some/url?foo=bar&foo=qux') | |
}) | |
it('with unknown status code', async () => { | |
clientStub.resolves({ | |
ok: false, | |
statusText: 'No', | |
text: stub().resolves('No'), | |
status: 419 | |
}) | |
await expect( | |
api.get('some/url') | |
).to.reject( | |
HttpError, | |
'419: No - No' | |
) | |
}) | |
it('with known status code', async () => { | |
clientStub.resolves({ | |
ok: false, | |
text: stub().resolves('foo'), | |
statusText: 'no', | |
status: 401 | |
}) | |
await expect( | |
api.get('some/url') | |
).to.reject( | |
AccessDeniedError, | |
'no' | |
) | |
}) | |
}) | |
describe('#post()', () => { | |
let api | |
let clientStub | |
beforeEach(async () => { | |
clientStub = stub() | |
api = new Api(clientStub) | |
}) | |
it('fetches data from url', async () => { | |
clientStub.resolves({ | |
ok: true, | |
json: stub() | |
}) | |
const content = { foo: 'bar' } | |
await api.post('some/url', content) | |
expect(clientStub.firstCall.args[1]).to.include({ | |
method: 'post', | |
headers: { | |
'Content-Type': 'application/json' | |
}, | |
body: JSON.stringify(content) | |
}) | |
}) | |
}) | |
describe('#put()', () => { | |
let api | |
let clientStub | |
beforeEach(async () => { | |
clientStub = stub() | |
api = new Api(clientStub) | |
}) | |
it('fetches data from url', async () => { | |
clientStub.resolves({ | |
ok: true, | |
json: stub() | |
}) | |
const content = { foo: 'bar' } | |
await api.put('some/url', content) | |
expect(clientStub.firstCall.args[1]).to.include({ | |
method: 'put', | |
headers: { | |
'Content-Type': 'application/json' | |
}, | |
body: JSON.stringify(content) | |
}) | |
}) | |
}) | |
describe('#del()', () => { | |
let api | |
let clientStub | |
beforeEach(async () => { | |
clientStub = stub() | |
api = new Api(clientStub) | |
}) | |
it('calls url with query params', async () => { | |
clientStub.resolves({ | |
ok: true | |
}) | |
await api.del('some/url', { foo: 'bar', baz: 'qux' }) | |
expect(clientStub.firstCall.args[0]).to.endWith('/some/url?foo=bar&baz=qux') | |
}) | |
it('calls url with delete method', async () => { | |
clientStub.resolves({ | |
ok: true | |
}) | |
await api.del('some/url') | |
expect(clientStub.firstCall.args[1].method).equals('delete') | |
}) | |
}) | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment