├── .babelrc ├── .eslintrc ├── .gitignore ├── .npmignore ├── README.md ├── dist ├── fetch-factory.js └── fetch-factory.min.js ├── example └── index.html ├── package.json ├── spec ├── fetch-factory.spec.js ├── helpers.js ├── request-interceptors.spec.js ├── response-interceptors.spec.js ├── shortcuts.spec.js └── url-parsing.spec.js └── src ├── browserify-entry.js ├── index.js └── url-parsing.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true 5 | }, 6 | "rules": { 7 | "curly": 2, 8 | "max-len": [ 9 | 2, 10 | 100, 11 | 4 12 | ], 13 | "indent": [2, 2, {"SwitchCase": 1}], 14 | "no-underscore-dangle": 2, 15 | "comma-dangle": [2, "always-multiline"], 16 | "no-unused-expressions": 2, 17 | "no-use-before-define": 2, 18 | "keyword-spacing": 2, 19 | "quotes": [2, "single"], 20 | "strict": [2, "never"] 21 | }, 22 | "ecmaFeatures": { 23 | "destructuring": true, 24 | "arrowFunctions": true, 25 | "blockBindings": true, 26 | "classes": true, 27 | "jsx": true, 28 | "modules": true 29 | }, 30 | "parser": "babel-eslint" 31 | } 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | .idea 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | example/ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fetch-factory 2 | 3 | A wrapper around the new `fetch` API to make creating services to talk to APIs easier. 4 | 5 | ## Example 6 | 7 | ```js 8 | var fetchFactory = require('fetch-factory'); 9 | 10 | var Users = fetchFactory.create({ 11 | url: 'http://api.mysite.com/users/:id', 12 | }, { 13 | find: { method: 'GET' }, 14 | create: { method: 'POST' }, 15 | }); 16 | 17 | Users.find(); // GET /users 18 | 19 | Users.find({ 20 | params: { id: 123 }, 21 | }); // GET /users/123 22 | 23 | Users.create({ 24 | data: { 25 | name: 'Jack', 26 | }, 27 | }); // POST /users with JSON stringified obj { name: 'jack' } 28 | ``` 29 | 30 | You can run another example by cloning this repo and running `npm i && npm run example`. 31 | 32 | ## Install 33 | 34 | ``` 35 | npm install fetch-factory 36 | ``` 37 | 38 | Consumable in the client through jspm, Webpack or Browserify. 39 | 40 | You can also grab `dist/fetch-factory.js` or `dist/fetch-factory.min.js` which is a browser build. It exposes `global.fetchFactory`. `example/index.html` shows how you would use this. 41 | 42 | Note that this library assumes a global `fetch` and `Promise` object. If you need to polyfill these, the following are recommended: 43 | 44 | - [github/fetch](https://github.com/github/fetch) `window.fetch` polyfill 45 | - [jakearchibald/es6-promise](https://github.com/jakearchibald/es6-promise) `Promise` polyfill. 46 | 47 | ## Configuration 48 | 49 | Configuration for a particular request can be set in one of three places: 50 | 51 | - in the config object that's the first argument to `fetchFactory.create` 52 | - in an object that you pass when telling fetch-factory what methods to create 53 | - in the call to the method that fetch factory created 54 | 55 | Configuration set further down the chain will override configuration set previously. For example: 56 | 57 | ```js 58 | var UserFactory = fetchFactory.create({ 59 | url: 'http://api.mysite.com/users/:id', 60 | method: 'GET', 61 | }, { 62 | find: {}, 63 | create: { method: 'POST' }, 64 | }); 65 | ``` 66 | 67 | When `UserFactory.find` is called, it will make a `GET` request, because the default configuration for `UserFactory` was given `method: 'GET'`. However, when `UserFactory.create` is called, it will make a `POST` request, because configuration was passed that is specific to that method. Although in reality you never need to, you could call `UserFactory.find({ method: 'POST' })`, which would cause the `find` method to make a `POST` request that time, because configuration passed in when a method is invoked overrides any set before it. 68 | 69 | ## POST Requests 70 | 71 | When a method defined by fetch-factory makes a `POST` request, it assumes that you'd like to POST JSON and sets some extra configuration: 72 | - the `Accept` header of the request is set to `application/json` 73 | - the `Content-Type` header of the request is set to `application/json` 74 | - if you pass in a `data` parameter, that is converted into JSON and sent as the body of the request 75 | 76 | ## Shortcut Methods 77 | 78 | There's a few methods that we've come to use often with our factories: `find`, `create` and `update`. fetch-factory comes with these definitions by default, so you can just tell it which ones you'd like to create: 79 | 80 | ```js 81 | var UserFactory = fetchFactory.create({ 82 | url: '/users/:id', 83 | methods: ['find', 'create'], 84 | }); 85 | ``` 86 | 87 | ## Interceptors 88 | 89 | fetch-factory also supports the concept of interceptors that can take a request and manipulate it before passing it on. 90 | 91 | ### Request Interceptors 92 | 93 | If you need to apply a transformation to every request before it is made (for example, adding an authorisation header), you can use a request interceptor. These can be sync or async. You can define a single request interceptor, or an array of multiple. An interceptor is expected to return the modified request object, or a new object with three properties: 94 | 95 | - `headers`: an object of key value pairs mapping headers to values 96 | - `body`: the string representing the request body, or `null`. 97 | - `method`: the method of the request 98 | 99 | ```js 100 | var UserFactory = fetchFactory.create({ 101 | url: 'http://api.mysite.com/users/:id', 102 | method: 'GET', 103 | interceptors: { 104 | request: function(request) { 105 | request.headers['Authorisation']: 'Bearer ACCESS_TOKEN123'; 106 | return request; 107 | }, 108 | }, 109 | }, { 110 | find: {}, 111 | }); 112 | 113 | UserFactory.find().then(function(data) { 114 | console.log(data.name) // 'bob' 115 | }); 116 | ``` 117 | 118 | By using an interceptor in this way you can avoid repeating the authorisation logic accross your frontend code base. 119 | 120 | ### Response Interceptors 121 | 122 | By default, fetch-factory will call its default response interceptor: 123 | - It simply takes the stream returned by `fetch` and consumes it as JSON, returning a JavaScript object. 124 | 125 | You can override this interceptor by passing an `interceptors` object with a `response` key: 126 | 127 | ```js 128 | var UserFactory = fetchFactory.create({ 129 | url: 'http://api.mysite.com/users/:id', 130 | method: 'GET', 131 | interceptors: { 132 | response: function(data) { 133 | return { name: 'bob' }; 134 | }, 135 | }, 136 | }, { 137 | find: {}, 138 | }); 139 | 140 | UserFactory.find().then(function(data) { 141 | console.log(data.name) // 'bob' 142 | }); 143 | ``` 144 | 145 | By default, fetch-factory will call its default error handler. 146 | - It simply checks the status and rejects on any non-2xx status 147 | 148 | You can disable the error handler by setting the `rejectOnBadResponse` flag to false. You can implement your own error handling logic. It is important that you handle response errors within the first passed `ìnterceptor`: 149 | 150 | ```js 151 | var UserFactory = fetchFactory.create({ 152 | url: 'http://api.mysite.com/users/:id', 153 | method: 'GET', 154 | rejectOnBadResponse: false, 155 | interceptors: { 156 | response: [ 157 | function(response) { 158 | if (response.status < 200 || response.status >= 300) { 159 | const error = new Error(response.statusText); 160 | error.response = response; 161 | throw error; 162 | } 163 | }, 164 | function(data) { 165 | return { name: 'bob' }; 166 | }, 167 | ], 168 | }, 169 | }, { 170 | find: {}, 171 | }); 172 | 173 | UserFactory.find().catch(function(error) { 174 | console.log(error.message) 175 | }); 176 | ``` 177 | 178 | A time when you might want to override the default response interceptor is if you need access to extra information on the response, such as headers. In this case fetch-factory's default interceptor will be insufficient, and you should override it to simply pass the full request through: 179 | 180 | ```js 181 | var UserFactory = fetchFactory.create({ 182 | url: 'http://api.mysite.com/users/:id', 183 | method: 'GET', 184 | interceptors: { 185 | response: function(response) { return response; }, 186 | }, 187 | }, { 188 | find: {}, 189 | }); 190 | 191 | UserFactory.find().then(function(response) { 192 | console.log(response.headers.get('Content-Type')); 193 | }); 194 | ``` 195 | 196 | 197 | 198 | ## Changelog 199 | 200 | ##### V0.2.1 - 8/12/2015 201 | - fix issue that lead to port numbers in URLs not working - thanks @copyhold 202 | 203 | ##### V0.2.0 - 8/12/2015 204 | - fix isssue that lead to being unable to create more than one factory 205 | 206 | ##### V0.1.0 - 11/11/2015 207 | - first release 208 | -------------------------------------------------------------------------------- /dist/fetch-factory.min.js: -------------------------------------------------------------------------------- 1 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.fetchFactory=f()}})(function(){var define,module,exports;return function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o=200&&response.status<300){return response}else{var error=new Error(response.statusText);error.response=response;throw error}};var fetchFactoryTemplates={find:{method:"GET"},create:{method:"POST"},update:{method:"PUT"}};var FetchFactory=function(){function FetchFactory(options){var _this=this;var methods=arguments.length>1&&arguments[1]!==undefined?arguments[1]:{};_classCallCheck(this,FetchFactory);this.factory={};this.defaultOptions=options;this.defaultOptions.rejectOnBadResponse=(0,_lodash4.default)(options,"rejectOnBadResponse",true);var templateMethods=(options.methods||[]).map(function(methodName){if(fetchFactoryTemplates[methodName]){return _defineProperty({},methodName,fetchFactoryTemplates[methodName])}else{throw new Error("Unknown method "+methodName)}}).reduce(Object.assign,{});Object.keys(templateMethods).forEach(function(method){_this.defineMethod(method,templateMethods[method])});Object.keys(methods).forEach(function(method){_this.defineMethod(method,methods[method])})}_createClass(FetchFactory,[{key:"removeNullFetchOptions",value:function removeNullFetchOptions(options){return(0,_lodash2.default)(options,function(v,k){return v!=null&&!(0,_lodash6.default)(v)})}},{key:"getResponseInterceptors",value:function getResponseInterceptors(){var responseInterceptors=(0,_lodash4.default)(this.defaultOptions,"interceptors.response",[function(response){return response.json()}]);if(!Array.isArray(responseInterceptors)){responseInterceptors=[responseInterceptors]}var result=responseInterceptors;if(this.defaultOptions.rejectOnBadResponse){result=[throwOnResponseError].concat(responseInterceptors)}return result}},{key:"getRequestInterceptors",value:function getRequestInterceptors(){var requestInterceptors=(0,_lodash4.default)(this.defaultOptions,"interceptors.request",[]);if(!Array.isArray(requestInterceptors)){requestInterceptors=[requestInterceptors]}return requestInterceptors}},{key:"applyRequestInterceptors",value:function applyRequestInterceptors(interceptors,fetchOptions){return interceptors.reduce(function(options,interceptor){return options.then(interceptor)},Promise.resolve(fetchOptions))}},{key:"applyResponseInterceptors",value:function applyResponseInterceptors(interceptors,fetchResult){return interceptors.reduce(function(result,interceptor){return result.then(interceptor)},fetchResult)}},{key:"defineMethod",value:function defineMethod(methodName,methodConfig){var _this2=this;this.factory[methodName]=function(){var runtimeConfig=arguments.length>0&&arguments[0]!==undefined?arguments[0]:{};var requestMethod=methodConfig.method||_this2.defaultOptions.method;var fetchOptions={method:[_this2.defaultOptions,methodConfig,runtimeConfig].reduce(function(method,config){return config.method||method},DEFAULT_REQUEST_METHOD),headers:runtimeConfig.headers||methodConfig.headers||{},body:null};if(requestMethod==="POST"||requestMethod==="PUT"){fetchOptions.headers["Accept"]="application/json";fetchOptions.headers["Content-Type"]="application/json";fetchOptions.body=JSON.stringify(runtimeConfig.data)}var baseUrl=runtimeConfig.url||methodConfig.url||_this2.defaultOptions.url;var requestInterceptors=_this2.getRequestInterceptors();var requestOptionsPromise=_this2.applyRequestInterceptors(requestInterceptors,fetchOptions);return requestOptionsPromise.then(function(requestOptions){return _this2.removeNullFetchOptions(requestOptions)}).then(function(requestOptions){var requestUrl=(0,_urlParsing.constructUrl)(baseUrl,runtimeConfig.params);var fetchResult=fetch(requestUrl,requestOptions);var responseInterceptors=_this2.getResponseInterceptors();return _this2.applyResponseInterceptors(responseInterceptors,fetchResult)})}}}]);return FetchFactory}();var fetchFactory={create:function create(options){var methods=arguments.length>1&&arguments[1]!==undefined?arguments[1]:{};return new FetchFactory(options,methods).factory}};exports.default=fetchFactory},{"./url-parsing":3,"lodash.get":4,"lodash.isEmpty":5,"lodash.pickBy":7}],3:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.placeholdersInUrl=placeholdersInUrl;exports.constructUrl=constructUrl;var _urlPattern=require("url-pattern");var _urlPattern2=_interopRequireDefault(_urlPattern);var _lodash=require("lodash.pickBy");var _lodash2=_interopRequireDefault(_lodash);var _lodash3=require("lodash.merge");var _lodash4=_interopRequireDefault(_lodash3);var _queryString=require("query-string");var _queryString2=_interopRequireDefault(_queryString);function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){if(key in obj){Object.defineProperty(obj,key,{value:value,enumerable:true,configurable:true,writable:true})}else{obj[key]=value}return obj}function placeholdersInUrl(url){var placeholderRegex=/(:\w+)/g;return(url.match(placeholderRegex)||[]).map(function(key){return key.substring(1)})}function constructUrl(urlBase){var params=arguments.length>1&&arguments[1]!==undefined?arguments[1]:{};var portRegex=/:(\d+)/;var portMatch=urlBase.match(portRegex);var protocolRegex=/^(http|https):\/\//i;var protocolMatch=urlBase.match(protocolRegex)&&urlBase.match(protocolRegex)[0];if(protocolMatch){urlBase=urlBase.replace(protocolRegex,"")}var urlPattern=new _urlPattern2.default(urlBase);var placeholders=placeholdersInUrl(urlBase);var placeholderParams=placeholders.reduce(function(obj,key){return(0,_lodash4.default)(obj,_defineProperty({},key,params[key]||""))},{});if(portMatch){placeholderParams[portMatch[1]]=portMatch[0]}var urlWithPlaceholdersFilled=urlPattern.stringify(placeholderParams);var queryParams=(0,_lodash2.default)(params,function(val,paramKey){return placeholders.indexOf(paramKey)===-1});var stringifiedParams=_queryString2.default.stringify(queryParams);var fullUrl=urlWithPlaceholdersFilled+(stringifiedParams?"?"+stringifiedParams:"");if(protocolMatch){return protocolMatch+fullUrl.replace(/\/$/,"")}else{return fullUrl.replace(/\/$/,"")}}},{"lodash.merge":6,"lodash.pickBy":7,"query-string":9,"url-pattern":11}],4:[function(require,module,exports){(function(global){var FUNC_ERROR_TEXT="Expected a function";var HASH_UNDEFINED="__lodash_hash_undefined__";var INFINITY=1/0;var funcTag="[object Function]",genTag="[object GeneratorFunction]",symbolTag="[object Symbol]";var reIsDeepProp=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,reIsPlainProp=/^\w*$/,reLeadingDot=/^\./,rePropName=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g;var reRegExpChar=/[\\^$.*+?()[\]{}|]/g;var reEscapeChar=/\\(\\)?/g;var reIsHostCtor=/^\[object .+?Constructor\]$/;var freeGlobal=typeof global=="object"&&global&&global.Object===Object&&global;var freeSelf=typeof self=="object"&&self&&self.Object===Object&&self;var root=freeGlobal||freeSelf||Function("return this")();function getValue(object,key){return object==null?undefined:object[key]}function isHostObject(value){var result=false;if(value!=null&&typeof value.toString!="function"){try{result=!!(value+"")}catch(e){}}return result}var arrayProto=Array.prototype,funcProto=Function.prototype,objectProto=Object.prototype;var coreJsData=root["__core-js_shared__"];var maskSrcKey=function(){var uid=/[^.]+$/.exec(coreJsData&&coreJsData.keys&&coreJsData.keys.IE_PROTO||"");return uid?"Symbol(src)_1."+uid:""}();var funcToString=funcProto.toString;var hasOwnProperty=objectProto.hasOwnProperty;var objectToString=objectProto.toString;var reIsNative=RegExp("^"+funcToString.call(hasOwnProperty).replace(reRegExpChar,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$");var Symbol=root.Symbol,splice=arrayProto.splice;var Map=getNative(root,"Map"),nativeCreate=getNative(Object,"create");var symbolProto=Symbol?Symbol.prototype:undefined,symbolToString=symbolProto?symbolProto.toString:undefined;function Hash(entries){var index=-1,length=entries?entries.length:0;this.clear();while(++index-1}function listCacheSet(key,value){var data=this.__data__,index=assocIndexOf(data,key);if(index<0){data.push([key,value])}else{data[index][1]=value}return this}ListCache.prototype.clear=listCacheClear;ListCache.prototype["delete"]=listCacheDelete;ListCache.prototype.get=listCacheGet;ListCache.prototype.has=listCacheHas;ListCache.prototype.set=listCacheSet;function MapCache(entries){var index=-1,length=entries?entries.length:0;this.clear();while(++index-1&&value%1==0&&value<=MAX_SAFE_INTEGER}function isObject(value){var type=typeof value;return!!value&&(type=="object"||type=="function")}function isObjectLike(value){return!!value&&typeof value=="object"}function stubFalse(){return false}module.exports=isEmpty}).call(this,typeof global!=="undefined"?global:typeof self!=="undefined"?self:typeof window!=="undefined"?window:{})},{}],6:[function(require,module,exports){(function(global){var LARGE_ARRAY_SIZE=200;var HASH_UNDEFINED="__lodash_hash_undefined__";var MAX_SAFE_INTEGER=9007199254740991;var argsTag="[object Arguments]",arrayTag="[object Array]",boolTag="[object Boolean]",dateTag="[object Date]",errorTag="[object Error]",funcTag="[object Function]",genTag="[object GeneratorFunction]",mapTag="[object Map]",numberTag="[object Number]",objectTag="[object Object]",promiseTag="[object Promise]",regexpTag="[object RegExp]",setTag="[object Set]",stringTag="[object String]",symbolTag="[object Symbol]",weakMapTag="[object WeakMap]";var arrayBufferTag="[object ArrayBuffer]",dataViewTag="[object DataView]",float32Tag="[object Float32Array]",float64Tag="[object Float64Array]",int8Tag="[object Int8Array]",int16Tag="[object Int16Array]",int32Tag="[object Int32Array]",uint8Tag="[object Uint8Array]",uint8ClampedTag="[object Uint8ClampedArray]",uint16Tag="[object Uint16Array]",uint32Tag="[object Uint32Array]";var reRegExpChar=/[\\^$.*+?()[\]{}|]/g;var reFlags=/\w*$/;var reIsHostCtor=/^\[object .+?Constructor\]$/;var reIsUint=/^(?:0|[1-9]\d*)$/;var typedArrayTags={};typedArrayTags[float32Tag]=typedArrayTags[float64Tag]=typedArrayTags[int8Tag]=typedArrayTags[int16Tag]=typedArrayTags[int32Tag]=typedArrayTags[uint8Tag]=typedArrayTags[uint8ClampedTag]=typedArrayTags[uint16Tag]=typedArrayTags[uint32Tag]=true;typedArrayTags[argsTag]=typedArrayTags[arrayTag]=typedArrayTags[arrayBufferTag]=typedArrayTags[boolTag]=typedArrayTags[dataViewTag]=typedArrayTags[dateTag]=typedArrayTags[errorTag]=typedArrayTags[funcTag]=typedArrayTags[mapTag]=typedArrayTags[numberTag]=typedArrayTags[objectTag]=typedArrayTags[regexpTag]=typedArrayTags[setTag]=typedArrayTags[stringTag]=typedArrayTags[weakMapTag]=false;var cloneableTags={};cloneableTags[argsTag]=cloneableTags[arrayTag]=cloneableTags[arrayBufferTag]=cloneableTags[dataViewTag]=cloneableTags[boolTag]=cloneableTags[dateTag]=cloneableTags[float32Tag]=cloneableTags[float64Tag]=cloneableTags[int8Tag]=cloneableTags[int16Tag]=cloneableTags[int32Tag]=cloneableTags[mapTag]=cloneableTags[numberTag]=cloneableTags[objectTag]=cloneableTags[regexpTag]=cloneableTags[setTag]=cloneableTags[stringTag]=cloneableTags[symbolTag]=cloneableTags[uint8Tag]=cloneableTags[uint8ClampedTag]=cloneableTags[uint16Tag]=cloneableTags[uint32Tag]=true;cloneableTags[errorTag]=cloneableTags[funcTag]=cloneableTags[weakMapTag]=false;var freeGlobal=typeof global=="object"&&global&&global.Object===Object&&global;var freeSelf=typeof self=="object"&&self&&self.Object===Object&&self;var root=freeGlobal||freeSelf||Function("return this")();var freeExports=typeof exports=="object"&&exports&&!exports.nodeType&&exports;var freeModule=freeExports&&typeof module=="object"&&module&&!module.nodeType&&module;var moduleExports=freeModule&&freeModule.exports===freeExports;var freeProcess=moduleExports&&freeGlobal.process;var nodeUtil=function(){try{return freeProcess&&freeProcess.binding("util")}catch(e){}}();var nodeIsTypedArray=nodeUtil&&nodeUtil.isTypedArray;function addMapEntry(map,pair){map.set(pair[0],pair[1]);return map}function addSetEntry(set,value){set.add(value);return set}function apply(func,thisArg,args){switch(args.length){case 0:return func.call(thisArg);case 1:return func.call(thisArg,args[0]);case 2:return func.call(thisArg,args[0],args[1]);case 3:return func.call(thisArg,args[0],args[1],args[2])}return func.apply(thisArg,args)}function arrayEach(array,iteratee){var index=-1,length=array?array.length:0;while(++index-1}function listCacheSet(key,value){var data=this.__data__,index=assocIndexOf(data,key);if(index<0){data.push([key,value])}else{data[index][1]=value}return this}ListCache.prototype.clear=listCacheClear;ListCache.prototype["delete"]=listCacheDelete;ListCache.prototype.get=listCacheGet;ListCache.prototype.has=listCacheHas;ListCache.prototype.set=listCacheSet;function MapCache(entries){var index=-1,length=entries?entries.length:0;this.clear();while(++index1?sources[length-1]:undefined,guard=length>2?sources[2]:undefined;customizer=assigner.length>3&&typeof customizer=="function"?(length--,customizer):undefined;if(guard&&isIterateeCall(sources[0],sources[1],guard)){customizer=length<3?undefined:customizer;length=1}object=Object(object);while(++index-1&&value%1==0&&value-1&&value%1==0&&value<=MAX_SAFE_INTEGER}function isObject(value){var type=typeof value;return!!value&&(type=="object"||type=="function")}function isObjectLike(value){return!!value&&typeof value=="object"}function isPlainObject(value){if(!isObjectLike(value)||objectToString.call(value)!=objectTag||isHostObject(value)){return false}var proto=getPrototype(value);if(proto===null){return true}var Ctor=hasOwnProperty.call(proto,"constructor")&&proto.constructor;return typeof Ctor=="function"&&Ctor instanceof Ctor&&funcToString.call(Ctor)==objectCtorString}var isTypedArray=nodeIsTypedArray?baseUnary(nodeIsTypedArray):baseIsTypedArray;function toPlainObject(value){return copyObject(value,keysIn(value))}function keys(object){return isArrayLike(object)?arrayLikeKeys(object):baseKeys(object)}function keysIn(object){return isArrayLike(object)?arrayLikeKeys(object,true):baseKeysIn(object)}var merge=createAssigner(function(object,source,srcIndex){baseMerge(object,source,srcIndex)});function stubArray(){return[]}function stubFalse(){return false}module.exports=merge}).call(this,typeof global!=="undefined"?global:typeof self!=="undefined"?self:typeof window!=="undefined"?window:{})},{}],7:[function(require,module,exports){(function(global){var LARGE_ARRAY_SIZE=200;var FUNC_ERROR_TEXT="Expected a function";var HASH_UNDEFINED="__lodash_hash_undefined__";var UNORDERED_COMPARE_FLAG=1,PARTIAL_COMPARE_FLAG=2;var INFINITY=1/0,MAX_SAFE_INTEGER=9007199254740991;var argsTag="[object Arguments]",arrayTag="[object Array]",boolTag="[object Boolean]",dateTag="[object Date]",errorTag="[object Error]",funcTag="[object Function]",genTag="[object GeneratorFunction]",mapTag="[object Map]",numberTag="[object Number]",objectTag="[object Object]",promiseTag="[object Promise]",regexpTag="[object RegExp]",setTag="[object Set]",stringTag="[object String]",symbolTag="[object Symbol]",weakMapTag="[object WeakMap]";var arrayBufferTag="[object ArrayBuffer]",dataViewTag="[object DataView]",float32Tag="[object Float32Array]",float64Tag="[object Float64Array]",int8Tag="[object Int8Array]",int16Tag="[object Int16Array]",int32Tag="[object Int32Array]",uint8Tag="[object Uint8Array]",uint8ClampedTag="[object Uint8ClampedArray]",uint16Tag="[object Uint16Array]",uint32Tag="[object Uint32Array]";var reIsDeepProp=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,reIsPlainProp=/^\w*$/,reLeadingDot=/^\./,rePropName=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g;var reRegExpChar=/[\\^$.*+?()[\]{}|]/g;var reEscapeChar=/\\(\\)?/g;var reIsHostCtor=/^\[object .+?Constructor\]$/;var reIsUint=/^(?:0|[1-9]\d*)$/;var typedArrayTags={};typedArrayTags[float32Tag]=typedArrayTags[float64Tag]=typedArrayTags[int8Tag]=typedArrayTags[int16Tag]=typedArrayTags[int32Tag]=typedArrayTags[uint8Tag]=typedArrayTags[uint8ClampedTag]=typedArrayTags[uint16Tag]=typedArrayTags[uint32Tag]=true;typedArrayTags[argsTag]=typedArrayTags[arrayTag]=typedArrayTags[arrayBufferTag]=typedArrayTags[boolTag]=typedArrayTags[dataViewTag]=typedArrayTags[dateTag]=typedArrayTags[errorTag]=typedArrayTags[funcTag]=typedArrayTags[mapTag]=typedArrayTags[numberTag]=typedArrayTags[objectTag]=typedArrayTags[regexpTag]=typedArrayTags[setTag]=typedArrayTags[stringTag]=typedArrayTags[weakMapTag]=false;var freeGlobal=typeof global=="object"&&global&&global.Object===Object&&global;var freeSelf=typeof self=="object"&&self&&self.Object===Object&&self;var root=freeGlobal||freeSelf||Function("return this")();var freeExports=typeof exports=="object"&&exports&&!exports.nodeType&&exports;var freeModule=freeExports&&typeof module=="object"&&module&&!module.nodeType&&module;var moduleExports=freeModule&&freeModule.exports===freeExports;var freeProcess=moduleExports&&freeGlobal.process;var nodeUtil=function(){try{return freeProcess&&freeProcess.binding("util")}catch(e){}}();var nodeIsTypedArray=nodeUtil&&nodeUtil.isTypedArray;function arrayPush(array,values){var index=-1,length=values.length,offset=array.length;while(++index-1}function listCacheSet(key,value){var data=this.__data__,index=assocIndexOf(data,key);if(index<0){data.push([key,value])}else{data[index][1]=value}return this}ListCache.prototype.clear=listCacheClear;ListCache.prototype["delete"]=listCacheDelete;ListCache.prototype.get=listCacheGet;ListCache.prototype.has=listCacheHas;ListCache.prototype.set=listCacheSet;function MapCache(entries){var index=-1,length=entries?entries.length:0;this.clear();while(++indexarrLength)){return false}var stacked=stack.get(array);if(stacked&&stack.get(other)){return stacked==other}var index=-1,result=true,seen=bitmask&UNORDERED_COMPARE_FLAG?new SetCache:undefined;stack.set(array,other);stack.set(other,array);while(++index-1&&value%1==0&&value-1&&value%1==0&&value<=MAX_SAFE_INTEGER}function isObject(value){var type=typeof value;return!!value&&(type=="object"||type=="function")}function isObjectLike(value){return!!value&&typeof value=="object"}function isSymbol(value){return typeof value=="symbol"||isObjectLike(value)&&objectToString.call(value)==symbolTag}var isTypedArray=nodeIsTypedArray?baseUnary(nodeIsTypedArray):baseIsTypedArray;function toString(value){return value==null?"":baseToString(value)}function get(object,path,defaultValue){var result=object==null?undefined:baseGet(object,path);return result===undefined?defaultValue:result}function hasIn(object,path){return object!=null&&hasPath(object,path,baseHasIn)}function keys(object){return isArrayLike(object)?arrayLikeKeys(object):baseKeys(object)}function keysIn(object){return isArrayLike(object)?arrayLikeKeys(object,true):baseKeysIn(object)}function pickBy(object,predicate){return object==null?{}:basePickBy(object,getAllKeysIn(object),baseIteratee(predicate))}function identity(value){return value}function property(path){return isKey(path)?baseProperty(toKey(path)):basePropertyDeep(path)}function stubArray(){return[]}module.exports=pickBy}).call(this,typeof global!=="undefined"?global:typeof self!=="undefined"?self:typeof window!=="undefined"?window:{})},{}],8:[function(require,module,exports){"use strict";var hasOwnProperty=Object.prototype.hasOwnProperty;var propIsEnumerable=Object.prototype.propertyIsEnumerable;function toObject(val){if(val===null||val===undefined){throw new TypeError("Object.assign cannot be called with null or undefined")}return Object(val)}function shouldUseNative(){try{if(!Object.assign){return false}var test1=new String("abc");test1[5]="de";if(Object.getOwnPropertyNames(test1)[0]==="5"){return false}var test2={};for(var i=0;i<10;i++){test2["_"+String.fromCharCode(i)]=i}var order2=Object.getOwnPropertyNames(test2).map(function(n){return test2[n]});if(order2.join("")!=="0123456789"){return false}var test3={};"abcdefghijklmnopqrst".split("").forEach(function(letter){test3[letter]=letter});if(Object.keys(Object.assign({},test3)).join("")!=="abcdefghijklmnopqrst"){return false}return true}catch(e){return false}}module.exports=shouldUseNative()?Object.assign:function(target,source){var from;var to=toObject(target);var symbols;for(var s=1;smaxIndex){if(sideEffects){throw new Error("too few values provided for key `"+key+"`")}else{return}}result=Array.isArray(value)?value[index]:value;if(sideEffects){nextIndexes[key]=index+1}return result};astNodeContainsSegmentsForProvidedParams=function(astNode,params,nextIndexes){var i,length;if(Array.isArray(astNode)){i=-1;length=astNode.length;while(++i 2 | 3 | 4 | Fetch Factory Example 5 |

 6 |     
 7 |     
23 |   
24 |   
25 |   
26 | 
27 | 


--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "fetch-factory",
 3 |   "version": "0.2.1",
 4 |   "description": "Easy JS objects for talking to your APIs",
 5 |   "main": "lib/index.js",
 6 |   "scripts": {
 7 |     "example": "open http://localhost:8000/example && python -m SimpleHTTPServer",
 8 |     "browserify": "npm run mkdir && browserify lib/browserify-entry.js -o dist/fetch-factory.js -s fetchFactory",
 9 |     "mkdir": "fsys --task=mkdir --dir=dist",
10 |     "minify": "uglifyjs dist/fetch-factory.js > dist/fetch-factory.min.js",
11 |     "build": "npm run babel && npm run browserify && npm run minify",
12 |     "babel": "babel -d lib src",
13 |     "prepublish": "npm run build",
14 |     "pretest": "eslint src spec",
15 |     "test": "babel-tape-runner spec/*.spec.js | tap-summary"
16 |   },
17 |   "repository": {
18 |     "type": "git",
19 |     "url": "https://github.com/jackfranklin/fetch-factory.git"
20 |   },
21 |   "author": "",
22 |   "contributors": [
23 |     "Vincent Schlatter  (https://github.com/vischlatter)"
24 |   ],
25 |   "license": "ISC",
26 |   "dependencies": {
27 |     "es6-promise": "^4.0.5",
28 |     "isomorphic-fetch": "^2.2.1",
29 |     "lodash.assign": "^4.2.0",
30 |     "lodash.get": "^4.4.2",
31 |     "lodash.isempty": "^4.4.0",
32 |     "lodash.merge": "^4.6.0",
33 |     "lodash.pickby": "^4.6.0",
34 |     "query-string": "^4.2.3",
35 |     "url-pattern": "^1.0.2"
36 |   },
37 |   "devDependencies": {
38 |     "babel-cli": "^6.18.0",
39 |     "babel-core": "^6.18.2",
40 |     "babel-eslint": "^7.1.0",
41 |     "babel-preset-es2015": "^6.18.0",
42 |     "babel-tape-runner": "^2.0.1",
43 |     "browserify": "^13.1.1",
44 |     "eslint": "^3.10.2",
45 |     "fs-extra-cli": "0.0.4",
46 |     "nock": "^9.0.2",
47 |     "sinon": "^1.17.6",
48 |     "tap-summary": "^3.0.1",
49 |     "tape": "^4.6.2",
50 |     "uglify-js": "^2.7.4"
51 |   }
52 | }
53 | 


--------------------------------------------------------------------------------
/spec/fetch-factory.spec.js:
--------------------------------------------------------------------------------
  1 | import 'isomorphic-fetch';
  2 | import test from 'tape';
  3 | 
  4 | import {
  5 |   mockPostJson,
  6 |   stubCalledBy,
  7 |   defaultMock,
  8 |   BASE_URL,
  9 |   mockPostJsonBody,
 10 | } from './helpers';
 11 | 
 12 | import fetchFactory from '../src/index';
 13 | 
 14 | test('fetchFactory findAll makes the right request', (t) => {
 15 |   t.plan(1);
 16 | 
 17 |   const stub = defaultMock();
 18 | 
 19 |   const UserFactory = fetchFactory.create({
 20 |     url: `${BASE_URL}/users`,
 21 |     method: 'GET',
 22 |   }, {
 23 |     findAll: {},
 24 |   });
 25 | 
 26 |   stubCalledBy(t, UserFactory.findAll(), stub);
 27 | });
 28 | 
 29 | 
 30 | test('configuration can be overriden in the call of the method', (t) => {
 31 |   t.plan(1);
 32 |   const stub = defaultMock('/foo');
 33 | 
 34 |   const UserFactory = fetchFactory.create({
 35 |     url: `${BASE_URL}/users`,
 36 |     method: 'GET',
 37 |   }, {
 38 |     findAll: {},
 39 |   });
 40 | 
 41 |   stubCalledBy(t, UserFactory.findAll({ url: `${BASE_URL}/foo` }), stub);
 42 | });
 43 | 
 44 | test('configuration can be overriden when defining a method', (t) => {
 45 |   t.plan(1);
 46 | 
 47 |   const stub = mockPostJson();
 48 | 
 49 |   const UserFactory = fetchFactory.create({
 50 |     url: `${BASE_URL}/users`,
 51 |     method: 'GET',
 52 |   }, {
 53 |     create: { method: 'POST' },
 54 |   });
 55 | 
 56 |   stubCalledBy(t, UserFactory.create(), stub);
 57 | });
 58 | 
 59 | test('data for a POST request is serialized to JSON', (t) => {
 60 |   t.plan(1);
 61 | 
 62 |   const stub = mockPostJsonBody();
 63 | 
 64 |   const UserFactory = fetchFactory.create({
 65 |     url: `${BASE_URL}/users`,
 66 |     method: 'GET',
 67 |   }, {
 68 |     create: { method: 'POST' },
 69 |   });
 70 | 
 71 |   stubCalledBy(t, UserFactory.create({ data: { name: 'jack' } }), stub);
 72 | });
 73 | 
 74 | test('it can take query params', (t) => {
 75 |   t.plan(1);
 76 | 
 77 |   const stub = defaultMock('/users?id=123');
 78 | 
 79 |   const UserFactory = fetchFactory.create({
 80 |     url: `${BASE_URL}/users`,
 81 |     method: 'GET',
 82 |   }, {
 83 |     findAll: {},
 84 |   });
 85 | 
 86 |   stubCalledBy(t, UserFactory.findAll({ params: { id: 123 } }), stub);
 87 | });
 88 | 
 89 | test('it can take a URL with placeholders for params', (t) => {
 90 |   t.plan(1);
 91 | 
 92 |   const stub = defaultMock('/users/123');
 93 |   const UserFactory = fetchFactory.create({
 94 |     url: `${BASE_URL}/users/:id`,
 95 |     method: 'GET',
 96 |   }, {
 97 |     findOne: {},
 98 |   });
 99 | 
100 |   stubCalledBy(t, UserFactory.findOne({
101 |     params: { id: 123 },
102 |   }), stub);
103 | });
104 | 
105 | test('it can take a URL with placeholders and query strings', (t) => {
106 |   t.plan(1);
107 | 
108 |   const stub = defaultMock('/users/123?name=jack');
109 | 
110 |   const UserFactory = fetchFactory.create({
111 |     url: `${BASE_URL}/users/:id`,
112 |     method: 'GET',
113 |   }, {
114 |     findOne: {},
115 |   });
116 | 
117 |   stubCalledBy(t, UserFactory.findOne({
118 |     params: { id: 123, name: 'jack' },
119 |   }), stub);
120 | });
121 | 


--------------------------------------------------------------------------------
/spec/helpers.js:
--------------------------------------------------------------------------------
 1 | import nock from 'nock';
 2 | 
 3 | export const BASE_URL = 'http://www.api.com';
 4 | 
 5 | export function defaultMock(slash = '/users') {
 6 |   return nock(BASE_URL).get(slash).reply(200, {
 7 |     name: 'jack',
 8 |   });
 9 | }
10 | 
11 | export function stubCalledBy(t, promise, stub) {
12 |   promise.then(() => {
13 |     t.ok(stub.isDone(), 'the stub was called');
14 |   }).catch((err) => {
15 |     console.log('TEST ERR', err.message, err.stack);
16 |   });
17 | }
18 | 
19 | export const JSON_HEADERS = {
20 |   'Accept': 'application/json',
21 |   'Content-Type': 'application/json',
22 | };
23 | 
24 | export function mockPostWithHeaders(slash = '/users', headers = {}, body) {
25 |   let stub = nock(BASE_URL, { reqheaders: headers });
26 |   stub = ( body ? stub.post(slash, body) : stub.post(slash) );
27 |   stub = stub.reply(200, { name: 'jack' });
28 |   return stub;
29 | }
30 | 
31 | export function mockPostJson(slash = '/users', body = null) {
32 |   return mockPostWithHeaders(slash, JSON_HEADERS, body);
33 | }
34 | 
35 | export function mockPostJsonBody(slash = '/users') {
36 |   return mockPostJson(slash, { name: 'jack' });
37 | }
38 | 
39 | export function mockPutJsonBody(slash = '/users') {
40 |   return nock(BASE_URL, { reqheaders: JSON_HEADERS })
41 |     .put(slash, { name: 'jack' })
42 |     .reply(201, { name: 'jack' });
43 | }
44 | 


--------------------------------------------------------------------------------
/spec/request-interceptors.spec.js:
--------------------------------------------------------------------------------
  1 | // got to use Nock for these tests so rather than stubbing some complex
  2 | // chain of fetch promises, we can easily test the interceptors by letting them work on the data
  3 | 
  4 | import test from 'tape';
  5 | import 'isomorphic-fetch';
  6 | import nock from 'nock';
  7 | import fetchFactory from '../src/index';
  8 | 
  9 | test('you can define a request interceptor', (t) => {
 10 |   t.plan(2);
 11 |   const UserFactory = fetchFactory.create({
 12 |     url: 'http://www.api.com/users',
 13 |     method: 'GET',
 14 |     interceptors: {
 15 |       request: (request) => {
 16 |         request.headers['Foo'] = 'Test';
 17 |         return request;
 18 |       },
 19 |     },
 20 |   }, {
 21 |     findAll: {},
 22 |   });
 23 | 
 24 |   const stub = nock('http://www.api.com', {
 25 |     reqheaders: {
 26 |       'Foo': 'Test',
 27 |     },
 28 |   })
 29 |   .get('/users')
 30 |   .reply(200, { name: 'jack' });
 31 | 
 32 |   UserFactory.findAll().then((data) => {
 33 |     t.ok(stub.isDone(), 'the stub was called');
 34 |     t.deepEqual(data, { name: 'jack' });
 35 |   }).catch((err) => {
 36 |     console.log('err', err.message, err.stack);
 37 |   });
 38 | });
 39 | 
 40 | test('you can define multiple request interceptors', (t) => {
 41 |   t.plan(2);
 42 |   const UserFactory = fetchFactory.create({
 43 |     url: 'http://www.api.com/users',
 44 |     method: 'GET',
 45 |     interceptors: {
 46 |       request: [(request) => {
 47 |         request.headers['Foo'] = 'Test';
 48 |         return request;
 49 |       }, (request) => {
 50 |         request.headers['Foo'] = 'Test2';
 51 |         return request;
 52 |       }],
 53 |     },
 54 |   }, {
 55 |     findAll: {},
 56 |   });
 57 | 
 58 |   const stub = nock('http://www.api.com', {
 59 |     reqheaders: {
 60 |       'Foo': 'Test2',
 61 |     }})
 62 |     .get('/users')
 63 |     .reply(200, { name: 'jack' });
 64 | 
 65 |   UserFactory.findAll().then((data) => {
 66 |     t.ok(stub.isDone(), 'the stub was called');
 67 |     t.deepEqual(data, { name: 'jack' });
 68 |   });
 69 | });
 70 | 
 71 | test('multiple interceptors when some are async works', (t) => {
 72 |   t.plan(2);
 73 |   const UserFactory = fetchFactory.create({
 74 |     url: 'http://www.api.com/users',
 75 |     method: 'GET',
 76 |     interceptors: {
 77 |       request: [(request) => {
 78 |         request.headers['Foo'] = 'Test';
 79 |         return request;
 80 |       }, (request) => {
 81 |         request.headers['Foo'] = 'Test2';
 82 |         return Promise.resolve(request);
 83 |       }],
 84 |     },
 85 |   }, {
 86 |     findAll: {},
 87 |   });
 88 | 
 89 |   const stub = nock('http://www.api.com', {
 90 |     reqheaders: {
 91 |       'Foo': 'Test2',
 92 |     }})
 93 |     .get('/users')
 94 |     .reply(200, { name: 'jack' });
 95 | 
 96 |   UserFactory.findAll().then((data) => {
 97 |     t.ok(stub.isDone(), 'the stub was called');
 98 |     t.deepEqual(data, { name: 'jack' });
 99 |   });
100 | });
101 | 


--------------------------------------------------------------------------------
/spec/response-interceptors.spec.js:
--------------------------------------------------------------------------------
  1 | // got to use Nock for these tests so rather than stubbing some complex
  2 | // chain of fetch promises, we can easily test the interceptors by letting them work on the data
  3 | 
  4 | import test from 'tape';
  5 | import 'isomorphic-fetch';
  6 | import nock from 'nock';
  7 | import fetchFactory from '../src/index';
  8 | 
  9 | test('the default response interceptor consumes the data as JSON', (t) => {
 10 |   t.plan(2);
 11 |   const UserFactory = fetchFactory.create({
 12 |     url: 'http://www.api.com/users',
 13 |     method: 'GET',
 14 |   }, {
 15 |     findAll: {},
 16 |   });
 17 | 
 18 |   const stub = nock('http://www.api.com')
 19 |                  .get('/users')
 20 |                  .reply(200, { name: 'jack' });
 21 | 
 22 |   UserFactory.findAll().then((data) => {
 23 |     t.ok(stub.isDone(), 'the stub was called');
 24 |     t.deepEqual(data, { name: 'jack' });
 25 |   });
 26 | });
 27 | 
 28 | test('the default response interceptor throws on response error', (t) => {
 29 |   t.plan(2);
 30 |   const UserFactory = fetchFactory.create({
 31 |     url: 'http://www.api.com/users',
 32 |     method: 'GET',
 33 |   }, {
 34 |     findAll: {},
 35 |   });
 36 | 
 37 |   const stub = nock('http://www.api.com')
 38 |                  .get('/users')
 39 |                  .reply(404);
 40 | 
 41 |   UserFactory.findAll().catch((error) => {
 42 |     t.ok(stub.isDone(), 'the stub was called');
 43 |     t.equal(error.response.status, 404);
 44 |   });
 45 | });
 46 | 
 47 | test('you can pass in a custom interceptor', (t) => {
 48 |   t.plan(2);
 49 | 
 50 |   const UserFactory = fetchFactory.create({
 51 |     url: 'http://www.api.com/users',
 52 |     method: 'GET',
 53 |     interceptors: {
 54 |       response: (data) => ({ name: 'bob' }),
 55 |     },
 56 |   }, {
 57 |     findAll: {},
 58 |   });
 59 | 
 60 |   const stub = nock('http://www.api.com')
 61 |     .get('/users')
 62 |     .reply(200, { name: 'jack' });
 63 | 
 64 |   UserFactory.findAll().then((data) => {
 65 |     t.ok(stub.isDone(), 'the stub was called');
 66 |     t.deepEqual(data, { name: 'bob' });
 67 |   });
 68 | });
 69 | 
 70 | test('the default error handler throws on respsonse error with a custom interceptor', (t) => {
 71 |   t.plan(2);
 72 | 
 73 |   const UserFactory = fetchFactory.create({
 74 |     url: 'http://www.api.com/users',
 75 |     method: 'GET',
 76 |     interceptors: {
 77 |       response: (data) => ({ name: 'bob' }),
 78 |     },
 79 |   }, {
 80 |     findAll: {},
 81 |   });
 82 | 
 83 |   const stub = nock('http://www.api.com')
 84 |     .get('/users')
 85 |     .reply(404);
 86 | 
 87 |   UserFactory.findAll().catch((error) => {
 88 |     t.ok(stub.isDone(), 'the stub was called');
 89 |     t.equal(error.response.status, 404);
 90 |   });
 91 | });
 92 | 
 93 | test('the default error handler does not throw with rejectOnBadResponse set to false', (t) => {
 94 |   t.plan(2);
 95 | 
 96 |   const UserFactory = fetchFactory.create({
 97 |     url: 'http://www.api.com/users',
 98 |     method: 'GET',
 99 |     rejectOnBadResponse: false,
100 |     interceptors: {
101 |       response: [
102 |         (data) => ({ name: 'bob' }),
103 |       ],
104 |     },
105 |   }, {
106 |     findAll: {},
107 |   });
108 | 
109 |   const stub = nock('http://www.api.com')
110 |     .get('/users')
111 |     .reply(404);
112 | 
113 |   UserFactory.findAll().then((data) => {
114 |     t.ok(stub.isDone(), 'the stub was called');
115 |     t.deepEqual(data, { name: 'bob' });
116 |   });
117 | });
118 | 
119 | test('you can have multiple interceptors', (t) => {
120 |   t.plan(2);
121 | 
122 |   const UserFactory = fetchFactory.create({
123 |     url: 'http://www.api.com/users',
124 |     method: 'GET',
125 |     interceptors: {
126 |       response: [
127 |         (data) => ({ name: 'bob' }),
128 |         (data) => ({ name: 'pete' }),
129 |       ],
130 |     },
131 |   }, {
132 |     findAll: {},
133 |   });
134 | 
135 |   const stub = nock('http://www.api.com')
136 |                  .get('/users')
137 |                  .reply(200, { name: 'jack' });
138 | 
139 |   UserFactory.findAll().then((data) => {
140 |     t.ok(stub.isDone(), 'the stub was called');
141 |     t.deepEqual(data, { name: 'pete' });
142 |   });
143 | });
144 | 
145 | test('you can have async interceptors', (t) => {
146 |   t.plan(2);
147 | 
148 |   const UserFactory = fetchFactory.create({
149 |     url: 'http://www.api.com/users',
150 |     method: 'GET',
151 |     interceptors: {
152 |       response: [
153 |         (data) => ({ name: 'bob' }),
154 |         (data) => {
155 |           return new Promise((resolve, reject) => {
156 |             resolve({ name: 'pete' });
157 |           });
158 |         },
159 |       ],
160 |     },
161 |   }, {
162 |     findAll: {},
163 |   });
164 | 
165 |   const stub = nock('http://www.api.com')
166 |                  .get('/users')
167 |                  .reply(200, { name: 'jack' });
168 | 
169 |   UserFactory.findAll().then((data) => {
170 |     t.ok(stub.isDone(), 'the stub was called');
171 |     t.deepEqual(data, { name: 'pete' });
172 |   });
173 | });
174 | 


--------------------------------------------------------------------------------
/spec/shortcuts.spec.js:
--------------------------------------------------------------------------------
 1 | import test from 'tape';
 2 | import sinon from 'sinon';
 3 | import fetchFactory from '../src/index';
 4 | 
 5 | import {
 6 |   defaultMock,
 7 |   mockPostJson,
 8 |   stubCalledBy,
 9 |   BASE_URL,
10 |   mockPutJsonBody,
11 | } from './helpers';
12 | 
13 | import 'isomorphic-fetch';
14 | 
15 | test('fetch factory find shortcut', (t) => {
16 |   t.plan(1);
17 | 
18 |   let stub = defaultMock('/users');
19 | 
20 |   const UserFactory = fetchFactory.create({
21 |     url: `${BASE_URL}/users/:id`,
22 |     methods: ['find', 'create', 'update'],
23 |   });
24 | 
25 |   stubCalledBy(t, UserFactory.find(), stub);
26 | });
27 | 
28 | test('fetch factory create shortcut', (t) => {
29 |   t.plan(1);
30 | 
31 |   let stub = mockPostJson('/users');
32 | 
33 |   const UserFactory = fetchFactory.create({
34 |     url: `${BASE_URL}/users/:id`,
35 |     methods: ['find', 'create', 'update'],
36 |   });
37 | 
38 |   stubCalledBy(t, UserFactory.create({
39 |     data: { name: 'jack' },
40 |   }), stub);
41 | });
42 | 
43 | test('fetch factory update shortcut', (t) => {
44 |   t.plan(1);
45 | 
46 |   let stub = mockPutJsonBody('/users/123');
47 | 
48 |   const UserFactory = fetchFactory.create({
49 |     url: `${BASE_URL}/users/:id`,
50 |     methods: ['find', 'create', 'update'],
51 |   });
52 | 
53 |   stubCalledBy(t, UserFactory.update({
54 |     params: { id: 123 },
55 |     data: { name: 'jack' },
56 |   }), stub);
57 | });
58 | 
59 | 
60 | test('when you give fetch factory an unkown shortcut', (t) => {
61 |   t.plan(1);
62 | 
63 |   t.throws(() => {
64 |     fetchFactory.create({
65 |       url: '/users/:id',
66 |       methods: ['foo'],
67 |     });
68 |   }, /unknown method foo/i);
69 | });
70 | 


--------------------------------------------------------------------------------
/spec/url-parsing.spec.js:
--------------------------------------------------------------------------------
 1 | import test from 'tape';
 2 | 
 3 | import { placeholdersInUrl, constructUrl } from '../src/url-parsing';
 4 | 
 5 | test('#placeholdersInUrl', (t) => {
 6 |   t.plan(3);
 7 | 
 8 |   t.deepEqual(placeholdersInUrl('/users/:id'), ['id']);
 9 |   t.deepEqual(placeholdersInUrl('/users/:id/:name'), [
10 |     'id', 'name',
11 |   ]);
12 |   t.deepEqual(placeholdersInUrl('/users/:id/foo'), ['id']);
13 | });
14 | 
15 | test('#constructUrl', (t) => {
16 |   t.test('given a query param', (t) => {
17 |     t.plan(1);
18 | 
19 |     const result = constructUrl('/users', {
20 |       id: 123,
21 |     });
22 | 
23 |     t.equal(result, '/users?id=123');
24 |   });
25 | 
26 |   t.test('given multiple query params', (t) => {
27 |     t.plan(1);
28 | 
29 |     const result = constructUrl('/users', {
30 |       id: 123,
31 |       name: 'jack',
32 |     });
33 | 
34 |     t.equal(result, '/users?id=123&name=jack');
35 |   });
36 | 
37 |   t.test('given a URL with a placeholder but no param', (t) => {
38 |     t.plan(1);
39 | 
40 |     const result = constructUrl('/users/:id', {});
41 | 
42 |     t.equal(result, '/users');
43 |   });
44 | 
45 |   t.test('given a URL with a placeholder', (t) => {
46 |     t.plan(1);
47 | 
48 |     const result = constructUrl('/users/:id', {
49 |       id: 123,
50 |     });
51 | 
52 |     t.equal(result, '/users/123');
53 |   });
54 | 
55 |   t.test('given a URL with a placeholder and query params', (t) => {
56 |     t.plan(1);
57 | 
58 |     const result = constructUrl('/users/:id', {
59 |       id: 123,
60 |       name: 'jack',
61 |     });
62 | 
63 |     t.equal(result, '/users/123?name=jack');
64 |   });
65 | 
66 |   t.test('Given a port number in the URL', (t) => {
67 |     t.plan(1);
68 | 
69 |     const result = constructUrl('http://foo.com:8000/users/:id', {
70 |       id: 123,
71 |     });
72 | 
73 |     t.equal(result, 'http://foo.com:8000/users/123');
74 |   });
75 | });
76 | 


--------------------------------------------------------------------------------
/src/browserify-entry.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./index.js').default;
2 | 


--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
  1 | import assign from 'lodash.assign';
  2 | import pickBy from 'lodash.pickBy';
  3 | import get from 'lodash.get';
  4 | import isEmpty from 'lodash.isEmpty';
  5 | 
  6 | import { constructUrl } from './url-parsing';
  7 | 
  8 | const DEFAULT_REQUEST_METHOD = 'GET';
  9 | 
 10 | const throwOnResponseError = (response) => {
 11 |   if (response.status >= 200 && response.status < 300) {
 12 |     return response;
 13 |   } else {
 14 |     const error = new Error(response.statusText);
 15 |     error.response = response;
 16 |     throw error;
 17 |   }
 18 | }
 19 | 
 20 | const fetchFactoryTemplates = {
 21 |   find: {
 22 |     method: 'GET',
 23 |   },
 24 |   create: {
 25 |     method: 'POST',
 26 |   },
 27 |   update: {
 28 |     method: 'PUT',
 29 |   },
 30 | };
 31 | 
 32 | class FetchFactory {
 33 |   constructor(options, methods = {}) {
 34 |     this.factory = {};
 35 | 
 36 |     this.defaultOptions = options;
 37 | 
 38 | 
 39 |     this.defaultOptions.rejectOnBadResponse = get(
 40 |       options,
 41 |       'rejectOnBadResponse',
 42 |       true
 43 |     );
 44 | 
 45 |     const templateMethods = (options.methods || []).map((methodName) => {
 46 |       if (fetchFactoryTemplates[methodName]) {
 47 |         return { [methodName]: fetchFactoryTemplates[methodName] };
 48 |       } else {
 49 |         throw new Error(`Unknown method ${methodName}`);
 50 |       }
 51 |     }).reduce(assign, {});
 52 | 
 53 |     Object.keys(templateMethods).forEach((method) => {
 54 |       this.defineMethod(method, templateMethods[method]);
 55 |     });
 56 | 
 57 |     Object.keys(methods).forEach((method) => {
 58 |       this.defineMethod(method, methods[method]);
 59 |     });
 60 |   }
 61 | 
 62 |   removeNullFetchOptions(options) {
 63 |     return pickBy(options, (v, k) => v != null && !isEmpty(v));
 64 |   }
 65 | 
 66 |   getResponseInterceptors() {
 67 |     let responseInterceptors = get(
 68 |       this.defaultOptions,
 69 |       'interceptors.response',
 70 |       [(response) => response.json()]
 71 |     );
 72 | 
 73 |     if (!Array.isArray(responseInterceptors)) {
 74 |       responseInterceptors = [responseInterceptors];
 75 |     }
 76 | 
 77 |     var result = responseInterceptors;
 78 | 
 79 |     if (this.defaultOptions.rejectOnBadResponse) {
 80 |       result = [throwOnResponseError].concat(responseInterceptors);
 81 |     }
 82 | 
 83 |     return result;
 84 |   }
 85 | 
 86 |   getRequestInterceptors() {
 87 |     let requestInterceptors = get(this.defaultOptions, 'interceptors.request', []);
 88 | 
 89 |     if (!Array.isArray(requestInterceptors)) {
 90 |       requestInterceptors = [requestInterceptors];
 91 |     }
 92 | 
 93 |     return requestInterceptors;
 94 |   }
 95 | 
 96 |   applyRequestInterceptors(interceptors, fetchOptions) {
 97 |     return interceptors.reduce((options, interceptor) => {
 98 |       return options.then(interceptor);
 99 |     }, Promise.resolve(fetchOptions));
100 |   }
101 | 
102 |   applyResponseInterceptors(interceptors, fetchResult) {
103 |     return interceptors.reduce((result, interceptor) => {
104 |       return result.then(interceptor);
105 |     }, fetchResult);
106 |   }
107 | 
108 |   defineMethod(methodName, methodConfig) {
109 |     this.factory[methodName] = (runtimeConfig = {}) => {
110 |       const requestMethod = methodConfig.method || this.defaultOptions.method;
111 | 
112 |       const fetchOptions = {
113 |         method: [
114 |           this.defaultOptions,
115 |           methodConfig,
116 |           runtimeConfig,
117 |         ].reduce((method, config) => config.method || method, DEFAULT_REQUEST_METHOD),
118 |         headers: runtimeConfig.headers || methodConfig.headers || {},
119 |         body: null,
120 |       }
121 | 
122 |       if (requestMethod === 'POST' || requestMethod === 'PUT') {
123 |         fetchOptions.headers['Accept'] = 'application/json';
124 |         fetchOptions.headers['Content-Type'] = 'application/json';
125 | 
126 |         fetchOptions.body = JSON.stringify(runtimeConfig.data);
127 |       }
128 | 
129 |       const baseUrl = runtimeConfig.url || methodConfig.url || this.defaultOptions.url;
130 | 
131 |       const requestInterceptors = this.getRequestInterceptors();
132 |       const requestOptionsPromise = this.applyRequestInterceptors(
133 |         requestInterceptors,
134 |         fetchOptions
135 |       );
136 | 
137 |       return requestOptionsPromise.then((requestOptions) => {
138 |         return this.removeNullFetchOptions(requestOptions);
139 |       }).then((requestOptions) => {
140 |         const requestUrl = constructUrl(baseUrl, runtimeConfig.params);
141 |         const fetchResult = fetch(requestUrl, requestOptions);
142 |         const responseInterceptors = this.getResponseInterceptors();
143 | 
144 |         return this.applyResponseInterceptors(responseInterceptors, fetchResult);
145 |       });
146 |     };
147 |   }
148 | }
149 | 
150 | const fetchFactory = {
151 |   create(options, methods = {}) {
152 |     return new FetchFactory(options, methods).factory;
153 |   },
154 | };
155 | 
156 | export default fetchFactory;
157 | 


--------------------------------------------------------------------------------
/src/url-parsing.js:
--------------------------------------------------------------------------------
 1 | import UrlPattern from 'url-pattern';
 2 | import pickBy from 'lodash.pickBy';
 3 | import merge from 'lodash.merge';
 4 | import queryString from 'query-string';
 5 | 
 6 | export function placeholdersInUrl(url) {
 7 |   const placeholderRegex = /(:\w+)/g;
 8 |   return (url.match(placeholderRegex) || []).map((key) => key.substring(1));
 9 | }
10 | 
11 | export function constructUrl(urlBase, params = {}) {
12 |   const portRegex = /:(\d+)/;
13 | 
14 |   const portMatch = urlBase.match(portRegex);
15 | 
16 |   const protocolRegex = /^(http|https):\/\//i;
17 | 
18 |   const protocolMatch = urlBase.match(protocolRegex) && urlBase.match(protocolRegex)[0];
19 | 
20 |   // TODO: bit funky - UrlPattern can't deal with the protocol ?
21 |   if (protocolMatch) {
22 |     urlBase = urlBase.replace(protocolRegex, '');
23 |   }
24 | 
25 |   const urlPattern = new UrlPattern(urlBase);
26 |   const placeholders = placeholdersInUrl(urlBase);
27 | 
28 |   const placeholderParams = placeholders.reduce((obj, key) => {
29 |     return merge(obj, { [key]: params[key] || '' });
30 |   }, {});
31 | 
32 |   if (portMatch) {
33 |     placeholderParams[portMatch[1]] = portMatch[0];
34 |   }
35 | 
36 |   const urlWithPlaceholdersFilled = urlPattern.stringify(placeholderParams);
37 | 
38 |   const queryParams = pickBy(params, (val, paramKey) => {
39 |     return placeholders.indexOf(paramKey) === -1;
40 |   });
41 | 
42 |   const stringifiedParams = queryString.stringify(queryParams);
43 | 
44 |   const fullUrl = urlWithPlaceholdersFilled + (stringifiedParams ? `?${stringifiedParams}` : '');
45 | 
46 |   if (protocolMatch) {
47 |     return protocolMatch + fullUrl.replace(/\/$/, '');
48 |   } else {
49 |     return fullUrl.replace(/\/$/, '');
50 |   }
51 | }
52 | 


--------------------------------------------------------------------------------