Skip to content

Instantly share code, notes, and snippets.

@tokenvolt
Created January 31, 2016 15:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tokenvolt/21f8bd755ce7b0c18a61 to your computer and use it in GitHub Desktop.
Save tokenvolt/21f8bd755ce7b0c18a61 to your computer and use it in GitHub Desktop.
REST endpoint generators
import fetch from 'isomorphic-fetch'
import { camelizeKeys } from 'humps'
import R from 'ramda'
import { normalize, arrayOf } from 'normalizr'
import Path from 'path-parser'
import { encode } from 'querystring'
import pluralize from 'pluralize'
import { playlistSchema } from 'schemas'
import { assocUid, mapValues } from 'utils/helpers'
import cuid from 'cuid'
// 1) Standardizing response (pagination, normalization) and Error handling
const API = {
namespace: "http://localhost:4000/api"
}
const IS_COLLECTION = '__collection__'
const commonMethods = {
// :: String -> [String] -> (k: v) -> (k: v) -> (a -> b)
"GET": R.curry((url, idAttributes, params, schema, headers = {}) => {
const idAttributeObject = R.pick(idAttributes, params);
const missingIdAttibutes = R.reject(R.flip(R.contains)(R.keys( idAttributeObject )), R.reject(R.equals(IS_COLLECTION), idAttributes));
const lastIdAttribute = R.last(idAttributes);
let queryParams;
let buildedUrl;
if ( !R.isEmpty(missingIdAttibutes) ) {
throw new Error(`You must provide "${missingIdAttibutes}" in params`)
}
if ( idAttributes.length === 1 && lastIdAttribute === IS_COLLECTION ) {
queryParams = params;
buildedUrl = url;
}
if ( idAttributes.length > 1 || lastIdAttribute !== IS_COLLECTION ) {
queryParams = R.omit(idAttributes, params);
buildedUrl = new Path(url).build(idAttributeObject);
}
return fetch(`${API.namespace}${buildedUrl}?${encode(queryParams)}`, {
headers: R.merge({
"Content-Type": "application/json"
}, headers)
})
.then(response => response.json())
.then(json => {
return normalize( camelizeKeys(json.data), arrayOf(schema) )
})
.then(
response => response,
error => ({error: error.message || 'Something bad happened'})
)
}),
// :: String -> [String] -> (k: v) -> (a -> b)
"POST": R.curry((url, idAttributes, params, schema, headers = {}) => {
const idAttributeObject = R.pick(idAttributes, params);
const missingIdAttibutes = R.reject(R.flip(R.contains)(R.keys( idAttributeObject )), R.reject(R.equals(IS_COLLECTION), idAttributes));
const lastIdAttribute = R.last(idAttributes);
let bodyParams;
let buildedUrl;
if ( !R.isEmpty(missingIdAttibutes) ) {
throw new Error(`You must provide "${missingIdAttibutes}" in params`)
}
if ( idAttributes.length === 1 && lastIdAttribute === IS_COLLECTION ) {
bodyParams = params;
buildedUrl = url;
}
if ( idAttributes.length > 1 || lastIdAttribute !== IS_COLLECTION ) {
bodyParams = R.omit(idAttributes, params);
buildedUrl = new Path(url).build(idAttributeObject);
}
return fetch(`${API.namespace}${buildedUrl}`, {
method: "POST",
body: JSON.stringify(bodyParams),
headers: R.merge({
"Content-Type": "application/json"
}, headers)
})
.then(response => response.json())
.then(json => {
return normalize( camelizeKeys(json), schema)
})
.then(
response => response,
error => ({error: error.message || 'Something bad happened'})
)
}),
// :: String -> [String] -> (k: v) -> (a -> b)
"PATCH": R.curry((url, idAttributes, params, schema, headers = {}) => {
const idAttributeObject = R.pick(idAttributes, params);
const missingIdAttibutes = R.reject(R.flip(R.contains)(R.keys( idAttributeObject )), R.reject(R.equals(IS_COLLECTION), idAttributes));
const lastIdAttribute = R.last(idAttributes);
let bodyParams;
let buildedUrl;
if ( !R.isEmpty(missingIdAttibutes) ) {
throw new Error(`You must provide "${missingIdAttibutes}" in params`)
}
if ( idAttributes.length === 1 && lastIdAttribute === IS_COLLECTION ) {
bodyParams = params;
buildedUrl = url;
}
if ( idAttributes.length > 1 || lastIdAttribute !== IS_COLLECTION ) {
bodyParams = R.omit(idAttributes, params);
buildedUrl = new Path(url).build(idAttributeObject);
}
return fetch(`${API.namespace}${buildedUrl}`, {
method: "PATCH",
body: JSON.stringify(bodyParams),
headers: R.merge({
"Content-Type": "application/json"
}, headers)
})
.then(response => response.json())
.then(json => {
return normalize( camelizeKeys(json), schema)
})
.then(
response => response,
error => ({error: error.message || 'Something bad happened'})
)
}),
// :: String -> [String] -> (k: v) -> (a -> b)
"DELETE": R.curry((url, idAttributes, params, headers = {}) => {
const idAttributeObject = R.pick(idAttributes, params);
const missingIdAttibutes = R.reject(R.flip(R.contains)(R.keys( idAttributeObject )), R.reject(R.equals(IS_COLLECTION), idAttributes));
const lastIdAttribute = R.last(idAttributes);
let bodyParams;
let buildedUrl;
if ( !R.isEmpty(missingIdAttibutes) ) {
throw new Error(`You must provide "${missingIdAttibutes}" in params`)
}
if ( idAttributes.length === 1 && lastIdAttribute === IS_COLLECTION ) {
buildedUrl = url;
}
if ( idAttributes.length > 1 || lastIdAttribute !== IS_COLLECTION ) {
buildedUrl = new Path(url).build(idAttributeObject);
}
return fetch(`${API.namespace}${buildedUrl}`, {
method: "DELETE",
headers: R.merge({
"Content-Type": "application/json"
}, headers)
})
.then(response => response.json())
.then(json => {
return camelizeKeys(json)
})
.then(
response => response,
error => ({error: error.message || 'Something bad happened'})
)
})
}
function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
// endpoint :: String -> (k: v) -> [(k: v)] -> (k: v)
export function generateEndpoint(name, options = {}, nonRestfulRoutes = []) {
const idAttribute = options.idAttribute || 'id';
const nestedEndpoint = options.nestUnder;
const nestedIdAttributes = nestedEndpoint ? nestedEndpoint.idAttributes : [];
const nestedNamespacedIdAttributes = nestedEndpoint ? nestedEndpoint.namespacedIdAttributes : [];
let urls;
if ( nestedEndpoint !== undefined && nestedEndpoint !== null ) {
urls = {
collection: `${nestedEndpoint.collectionUrl}/:${R.last(nestedNamespacedIdAttributes)}/${name}`,
member: `${nestedEndpoint.collectionUrl}/:${R.last(nestedNamespacedIdAttributes)}/${name}/:${idAttribute}`
}
} else {
urls = {
collection: `/${name}`,
member: `/${name}/:${idAttribute}`
}
}
const collectionUrl = urls['collection'];
const memberUrl = urls['member'];
const namespacedIdAttribute = R.concat( pluralize(name, 1) )( capitalize(idAttribute) );
const nonRestful = nonRestfulRoutes.reduce( (result, routeConfig) => {
let url = R.concat( urls[routeConfig.on] )(`/${routeConfig.name}`);
result[routeConfig.name] = commonMethods[routeConfig.method](url)(R.flatten([...nestedNamespacedIdAttributes, (routeConfig.on == 'collection' ? IS_COLLECTION : idAttribute) ]));
return result;
}, {})
return R.merge({
name,
collectionUrl,
memberUrl,
idAttributes : R.append(idAttribute, nestedIdAttributes),
namespacedIdAttributes : R.append(namespacedIdAttribute, nestedNamespacedIdAttributes),
getCollection : commonMethods["GET"](collectionUrl)( [...nestedNamespacedIdAttributes, IS_COLLECTION] ),
get : commonMethods["GET"](memberUrl)( [...nestedNamespacedIdAttributes, idAttribute] ),
create : commonMethods["POST"](collectionUrl)( [...nestedNamespacedIdAttributes, IS_COLLECTION] ),
update : commonMethods["PATCH"](memberUrl)( [...nestedNamespacedIdAttributes, idAttribute] ),
destroy : commonMethods["DELETE"](memberUrl)( [...nestedNamespacedIdAttributes, idAttribute] )
})(nonRestful);
}
export const tracksAPI = generateEndpoint('tracks')
tracksAPI.()
// playlists = generateEndpoint('playlists')
// '/playlists'
// '/playlists/:id'
// playlists = generateEndpoint('playlists', {idAttribute: 'slug'})
// '/playlists'
// '/playlists/:slug'
// tracks = generateEndpoint('tracks', { nestUnder: generateEndpoint('playlists', {idAttribute: 'slug'}) })
// '/playlists/:playlistSlug/tracks'
// '/playlists/:playlistSlug/tracks/:id'
// users = generateEndpoint('users', { nestUnder: tracks })
// '/playlists/:playlistSlug/tracks/:trackId/users'
// '/playlists/:playlistSlug/tracks/:trackId/users/:id'
// tracks = generateEndpoint('tracks', {}, [{ name: "listen", method: "GET", on: 'member' }]
// '/tracks'
// '/tracks/:id'
// '/tracks/:id/listen'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment