Skip to content

Instantly share code, notes, and snippets.

@abiodun0
Last active December 7, 2017 16:06
Show Gist options
  • Save abiodun0/670dc97841d7c1e97e66ca6c58e6ac75 to your computer and use it in GitHub Desktop.
Save abiodun0/670dc97841d7c1e97e66ca6c58e6ac75 to your computer and use it in GitHub Desktop.
some interesting api abstraction
import Path from 'path-parser'
import { isTokenExpired } from './auth0Utils'
import { HttpForbiddenError, UnauthorizedError } from './errors'
const GET = 'GET'
const PUT = 'PUT'
const POST = 'POST'
const PATCH = 'PATCH'
const DELETE = 'DELETE'
const FANCY_METHODS = new Set([ PATCH, PUT, DELETE ])
const endpoint = (method = GET, authenticated = true) => route => {
return {
method,
authenticated,
route
}
}
// @formatter:off
/* eslint key-spacing: 0, func-call-spacing: 0, no-multi-spaces: 0 */
const API_DEFNS = {
/* Authenticated Routes */
account: {
get: endpoint(GET) ('/account'),
goals: endpoint(GET) ('/account/goals'),
fields: endpoint(GET) ('/fields'),
update: endpoint(PATCH) ('/account')
},
audience: {
list: endpoint(GET) ('/audiences/aperture'),
get: endpoint(GET) ('/audiences/:id/aperture_fast'),
getDetails: endpoint(GET) ('/audiences/:id/aperture'),
create: endpoint(POST) ('/audiences'),
update: endpoint(PATCH) ('/audiences/:id/aperture')
},
campaigns: {
deliver: endpoint(POST) ('/campaigns/:id/deliveries'),
list: endpoint(GET) ('/campaigns/aperture'),
get: endpoint(GET) ('/campaigns/:id'),
update: endpoint(PATCH) ('/campaigns/:id')
},
contexts: {
get: endpoint(GET) ('/mars/contexts/:id')
},
deliveries: {
get: endpoint(GET) ('/deliveries/:id'),
url: endpoint(GET) ('/deliveries/:id/url'),
preapprove: endpoint(POST) ('/deliveries/:id/preapprove')
},
ensembles: {
list: endpoint(GET) ('/ensembles')
},
files: {
list: endpoint(GET) ('/customer_files'),
get: endpoint(GET) ('/customer_files/:id'),
url: endpoint(GET) ('/customer_files/:id/url'),
create: endpoint(POST) ('/customer_files'),
report: endpoint(GET) ('/customer_files/:id/customer_file_report')
},
places: {
search: endpoint(GET) ('/places?q')
},
pointsOfInterest: {
list: endpoint(GET) ('/points_of_interest')
},
subscription: {
get: endpoint(GET) ('/subscription')
},
user: {
get: endpoint(GET) ('/user'),
update: endpoint(PATCH) ('/user')
},
users: {
list: endpoint(GET) ('/users'),
create: endpoint(POST) ('/users'),
kick: endpoint(POST) ('/users/:id/kick')
},
vendors: {
list: endpoint(GET) ('/vendors'),
create: endpoint(POST) ('/vendors/:id/setting'),
update: endpoint(PATCH) ('/vendors/:id/setting'),
delete: endpoint(DELETE)('/vendors/:id/setting')
},
/* Unauthenticated Routes */
password: {
requestReset: endpoint(POST, false)('/password_resets'),
reset: endpoint(PUT, false)('/password_resets/:token')
},
session: {
create: endpoint(POST, false)('/sessions'),
social: endpoint(POST, true) ('/social_sessions') // note, requires auth0 token to complete
},
signup: {
complete: endpoint(POST, false)('/user/registration_completion?token')
}
}
// @formatter:on
const PREFIX = '/api'
const STANDARD_HEADERS = {
'Content-Type': 'application/json',
Accept: 'application/json'
}
export class SimpleAuthService {
async getToken () {
const token = localStorage.token
if (this.loggingOut) {
return new Promise(() => {}) // never resolve token
} else if (!token || isTokenExpired(token)) {
// todo: refresh token if possible
this.kick()
throw new UnauthorizedError()
} else {
return token
}
}
kick () {
if (!this.loggingOut) {
delete localStorage.token
delete localStorage.profile
window.location = '/login/' // kick user
this.loggingOut = true
}
}
}
export class ApiService {
// Split incoming args into URL args and body args:
static splitArgs (method, args, path) {
const url = path.build(args)
if (method === GET || !args) return { url, body: null }
else {
const pathArgs = path.test(url)
const body = Object.entries(args).reduce((args, [argKey, argVal]) =>
(argKey in pathArgs)
? args
: { ...args, [argKey]: argVal },
{})
return { url, body }
}
}
static async getBody (response) {
const responseBody = await response.text()
if (responseBody.trim()) { // sometimes we get empty responses back from Flora
return JSON.parse(responseBody)
} else {
return null
}
}
constructor (authService) {
this.authService = authService
Object.entries(API_DEFNS).forEach(([subject, value]) => {
if (!(subject in this)) this[subject] = {}
Object.entries(value).forEach(([endpoint, {method, authenticated, route}]) => {
const path = new Path(`${PREFIX}${route}`)
this[subject][endpoint] = (args) => {
const { url, body } = ApiService.splitArgs(method, args, path)
return this.request({
method,
url,
body,
authenticated
})
}
})
})
}
async request ({ method, url, body, authenticated }) {
const finalHeaders = {...STANDARD_HEADERS}
if (FANCY_METHODS.has(method)) {
finalHeaders['X-HTTP-Method-Override'] = method
}
if (authenticated) {
finalHeaders['Authorization'] = `Bearer ${await this.authService.getToken()}`
}
const response = await fetch(url, {
method,
headers: finalHeaders,
body: body ? JSON.stringify(body) : undefined
})
if (response.ok) {
return ApiService.getBody(response)
} else if (response.status === 503) {
window.location.reload()
throw new Error('Flora gone into maintenence mode')
} else if (authenticated && (response.status === 401 || (url === '/api/account' && response.status === 404))) {
this.authService.kick()
return new Promise(() => {})
} else if (response.status === 403) {
throw new HttpForbiddenError(response)
} else {
const responseBody = await ApiService.getBody(response)
const e = new Error(response.statusText)
e.details = responseBody
throw e
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment