Skip to content

Instantly share code, notes, and snippets.

@antony
Created Dec 10, 2018
Embed
What would you like to do?
Universal Api Client for Sapper
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
}
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