UNPKG

js-facade

Version:
611 lines (521 loc) 17.9 kB
(function() { 'use strict' window.Facade = {}; var backendIsInitialized = false; var definitionCallback; Facade.resources = {}; Facade.db = {}; var facadeRoutes = {}; var originalResources = {} var originalDb = {} var originalRoutes = {}; var customRouteOpts = []; // PUBLIC FUNCTIONS // Facade.resource = function(opts) { opts = opts || {}; checkForResourceErrors(opts); // Create 'table' in the 'database' this.db[opts.name] = buildTable(opts.name); // Create a slot for the resources routes in the master route list; facadeRoutes[opts.name] = {}; // Add resource to master list return this.resources[opts.name] = buildResource(opts); }; Facade.initialize = function(opts) { checkIfAlreadyInitialized(); Facade.clear(); opts = opts || {}; Facade.backend = opts.backend; checkForHttpBackend(opts); backendIsInitialized = true; _.isFunction(definitionCallback) && definitionCallback(); _.each(this.resources, function(resource) { createRestRoutes(resource); }); createCopiesOfMasterLists(); }; Facade.reset = function() { Facade.resources = _.clone(originalResources, true); Facade.db = _.clone(originalDb, true); facadeRoutes = _.clone(originalRoutes, true); backendIsInitialized = false; }; Facade.define = function(callback) { definitionCallback = callback }; Facade.undefine = function() { definitionCallback = undefined; } Facade.clear = function() { this.resources = {}; this.db = {}; facadeRoutes = {}; customRouteOpts = []; this.backend = undefined; backendIsInitialized = false; }; Facade.findRoute = function(method, url) { var routeObj; var fullRoute = [method, url].join(' '); var exists = _.some(facadeRoutes, function(resourceRoutes) { routeObj = resourceRoutes[fullRoute]; if (routeObj) { return routeObj; } return routeObj = _.chain(resourceRoutes) .filter('regExp') .filter({method: method}) .find(function(route) { return route.regExp.test(url); }).value(); }); if (!exists) { throw new Error("The route " + fullRoute + " does not exist"); } return routeObj; } // PRIVATE FUNCTIONS // // ** Routes ** // function createCopiesOfMasterLists() { originalResources = _.clone(Facade.resources, true); originalDb = _.clone(Facade.db, true); originalRoutes = _.clone(facadeRoutes, true); } function createRestRoutes(resource) { createCollectionRoutesFor(resource); createAllItemRoutes(resource); } function createCollectionRoutesFor(resource) { _.each(collectionRouteCreators(), function(routeCreator) { var opts = {resource: resource, method: routeCreator.method} routeCreator.createWith(opts); storeRoute(opts); }) } function createAllItemRoutes(resource) { var allItems = getAllItems(resource); _.each(allItems, function(item) { createItemRoutesFor(resource, item); }); } function createItemRoutesFor(resource, item) { _.each(itemRouteCreators(), function(routeCreator) { var opts = {resource: resource, item: item, method: routeCreator.method} routeCreator.createWith(opts); storeRoute(opts); }); _.each(customRouteOpts, function(opts) { if (opts.onItem) { opts.item = item; createCustomRouteForItem(opts); storeRoute(opts); } }); } function createItemIdRoute(opts) { var headers = {}; Facade.backend.whenGET(opts.resource.url + '/' + opts.item.id) .respond(function(method, url, data, headers) { var route = Facade.findRoute(method, url); var response = route.getSpecialResponseOr(function() { var item = getOneItem(opts.resource, opts.item.id); return [200, JSON.stringify(item), {}, 'OK']; }) // TODO: Add check that the response is an array with 4 items; return response; }); } function createPutRoute(opts) { var headers = {}; Facade.backend.whenPUT(opts.resource.url + '/' + opts.item.id) .respond(function(method, url, data, headers) { data = data || {}; var route = Facade.findRoute(method, url); var response = route.getSpecialResponseOr(function() { var item = getOneItem(opts.resource, opts.item.id); // Perform the patch on the db object _.assign(item, JSON.parse(data)); return [200, JSON.stringify(item), headers, 'OK'] }) return response; }); } function createPatchRoute(opts) { var headers = {}; Facade.backend.whenPATCH(opts.resource.url + '/' + opts.item.id) .respond(function(method, url, data, headers) { data = data || {}; var route = Facade.findRoute(method, url); var response = route.getSpecialResponseOr(function() { var item = getOneItem(opts.resource, opts.item.id); // Perform the patch on the db object _.assign(item, JSON.parse(data)); return [200, JSON.stringify(item), headers, 'OK'] }) return response; }); } function createDeleteRoute(opts) { var headers = {}; Facade.backend.whenDELETE(opts.resource.url + '/' + opts.item.id) .respond(function(method, url, data, headers) { data = data || {}; var route = Facade.findRoute(method, url); // Perform the delete on the db var response = route.getSpecialResponseOr(function() { var item = getTable(opts.resource).delete(opts.item.id); return [200, JSON.stringify(item), {}, 'OK']; }) return response; }); } function createCreateRoute(opts) { var headers = {}; Facade.backend.whenPOST(opts.resource.url) .respond(function(method, url, data, headers) { data = data || {}; data = JSON.parse(data); var route = Facade.findRoute(method, url); // Perform the POST on the db var response = route.getSpecialResponseOr(function() { var item = _.isFunction(opts.resource.createDefault) && opts.resource.createDefault(data); item = item || data; opts.resource.addItem(item); return [200, JSON.stringify(item), {}, 'OK']; }) return response; }); } function createIndexRoute(opts) { var headers = {}; Facade.backend.whenGET(opts.resource.url) .respond(function(method, url, data, headers) { var route = Facade.findRoute(method, url); var response = route.getSpecialResponseOr(function() { return [200, getAllItems(opts.resource), {}, 'OK'] }) return response; }); } function itemRouteCreators() { return [ {createWith: createItemIdRoute, method: 'GET'}, {createWith: createPutRoute, method: 'PUT'}, {createWith: createPatchRoute, method: 'PATCH'}, {createWith: createDeleteRoute, method: 'DELETE'} ]; } function collectionRouteCreators() { return [ {createWith: createCreateRoute, method: 'POST'}, {createWith: createIndexRoute, method: 'GET'} ]; } function createCustomRouteForItem(opts) { throwIfRegex(opts.route); var fullUrl = opts.resource.url + '/' + opts.item.id + opts.route; Facade.backend.when(opts.method, fullUrl).respond(function(method, url, requestData, headers) { requestData = JSON.parse(requestData || "{}"); var route = Facade.findRoute(method, url); var item = getTable(opts.resource).find(opts.item.id); var response = route.getSpecialResponseOr(function() { return opts.callback(requestData, item, headers); }); checkForValidResponse(response); return response; }); } function createCustomRouteForCollection(opts) { var fullUrl = _.isRegExp(opts.route) ? opts.route : opts.resource.url + opts.route Facade.backend.when(opts.method, fullUrl).respond(function(method, url, requestData, headers) { requestData = JSON.parse(requestData || "{}"); var collection = getTable(opts.resource).getAll(); var route = Facade.findRoute(method, url); var response = route.getSpecialResponseOr(function() { return opts.callback(requestData, collection); }); checkForValidResponse(response); return response; }); } function createExpectationFor(opts) { var fullUrl = opts.resource.url + opts.route; Facade.backend.expect(opts.method, fullUrl, withJSON(opts.expected)) .respond(function(method, url, requestData, headers) { requestData = JSON.parse(requestData || "{}"); var collection = getTable(opts.resource).getAll(); var route = Facade.findRoute(method, url); if (!route.hasSpecialResponse()) { _.isFunction(opts.callback) && opts.callback(requestData, collection); } var response = route.getSpecialResponseOr(function() { return [200, JSON.stringify(collection), {}, 'OK']; }); return response; }); } function storeRoute(opts) { opts.route = opts.route || ''; if (_.isRegExp(opts.route)) { opts.regExp = opts.route; }else { var prefix = opts.resource.url; var fullUrl = opts.item ? prefix + '/' + opts.item.id + opts.route : prefix + opts.route; var fullRoute = [opts.method, fullUrl].join(' ') opts.fullRoute = fullRoute; } facadeRoutes[opts.resource.name][(opts.regExp || opts.fullRoute)] = buildRoute(opts); } function storeRouteOpts(opts) { customRouteOpts.push(opts); }; function withJSON(expectedParams) { return function (postData) { var jsonData = JSON.parse(postData); if (!jsonData) { console.log("Unable to parse to JSON:", postData); return false; } return _.every(expectedParams, function (expectedValue, expectedKey) { return findParam(jsonData, expectedValue, expectedKey); }); }; function findParam(json, expectedVal, expectedKey) { if (json[expectedKey]) { if (json[expectedKey] === expectedVal) { return true } console.log('Expected', expectedKey, "to equal", expectedVal, "in", json, "but it was", json[expectedKey]); return false; } var nested = _.filter(json, function(val) { return _.isObject(val); }) var foundParam = json[expectedKey] === expectedVal; if (!nested && !foundParam) { console.log('Missing expectedKey', expectedKey, "in", nestedData, "should include", expectedParams); return false; } return _.some(nested, function(json) { return findParam(json, expectedVal, expectedKey); }); } } // ** Class Factories ** // function buildResource(opts) { // For nesting child urls if called from a parent. opts = opts || {}; return { url: opts.url, name: opts.name, createDefault: opts.createDefault, addItem: function(item) { checkForResourceId(item); getTable(this).create(item); if (backendIsInitialized) { createItemRoutesFor(this, item); } return item; }, resource: function(opts) { opts.url = this.url + opts.url; return Facade.resource(opts); }, addRoute: function(opts) { opts = opts || {}; checkForRequiredRouteArgs(opts); opts.resource = this; storeRouteOpts(opts); if (opts.onItem) { var allItems = getTable(this).getAll() _.each(allItems, function(item) { opts.item = item; createCustomRouteForItem(opts); storeRoute(opts); }); }else { createCustomRouteForCollection(opts); storeRoute(opts); } }, expect: function(method, route) { checkForValidMethod(method); route = route || ''; var self = this; return { with: function(params) { opts = {method: method, route: route, expected: params, resource: self}; createExpectationFor(opts); } } } }; } function buildRoute(opts) { var specialResponses = []; return { fullRoute: opts.fullRoute, regExp: opts.regExp, method: opts.method, nextResponse: function(status, data) { specialResponses.push({status: status, data: data}); }, getSpecialResponse: function() { return specialResponses.shift(); }, hasSpecialResponse: function() { return Boolean(specialResponses.length); }, getSpecialResponseOr: function(callback) { if (this.hasSpecialResponse()) { var response = this.getSpecialResponse(); return [response.status, JSON.stringify(response.data), {}, 'OK']; }else { return _.isFunction(callback) && callback() } } } } function buildTable(name) { var storage = {}; return { getAll: function() { return _.map(storage); }, create: function(item) { checkForResourceId(item); var id = item.id; storage[JSON.stringify(id)] = item; }, find: function(id, opts) { checkForIdToFindOn(id); opts = opts || {}; var item = storage[JSON.stringify(id)]; if (!item) { throw new Error("No item found in " + name + " table with id of " + id) } return opts.wrap ? itemWrapper(item, getResource(name)) : item; }, delete: function(id) { checkForIdToFindOn(id); var id = JSON.stringify(id); var item = storage[id]; if (!item) { throw new Error("No item found in " + name + " table with id of " + id + ". So can't delete it."); } storage[id] = null; return item; } } } // ** Db Helpers ** // function getTable(resource) { var table = Facade.db[resource.name]; if (!table) { throw new Error("There doesnt appear to be a table called " + resource.name); } return table; } function getAllItems(resource) { return getTable(resource).getAll(); } function getOneItem(resource, id, opts) { opts = opts || {}; return getTable(resource).find(id, opts); } function itemWrapper(item, resource) { item = _.extend({}, item); item.showUrl = function() { return resource.url + '/' + item.id; } return item; } // ** Resource Helpers ** // function getResource(name) { var resource = Facade.resources[name]; if (!resource) { throw new Error("There doesnt appear to be a resource called " + name); } return resource; } // ** Error Handling ** // function checkForResourceErrors(opts) { if (!_.isString(opts.name) ) { throw new Error("You must provide a name for the resource"); } if (Facade.resources[opts.name]) { throw new Error( "A resource named " + opts.name + " already exists. Please choose a different name." ); } if (!_.isString(opts.url) ) { throw new Error("You must provide a url for the " + opts.name + " resource"); } var urls = _.map(Facade.resources, 'url'); if ( _.find(urls, function(url) { return url === opts.url; }) ) { throw new Error("The url " + opts.url + " is already taken. Please change one"); } }; function checkForResourceId(resourceInstance) { if (!resourceInstance.id) { throw new Error("The resource must have an id property."); } } function checkForIdToFindOn(id) { if (!id) { throw new Error("You must pass in an id to find a record"); } } function checkForArray(items) { if (!_.isArray(items)) { throw new Error("addItems must take an array") } } function checkForValidMethod(method) { if (!_.isString(method)) { throw new Error("No HTTP method was provided"); } var httpVerbs = ['GET', 'POST', 'PATCH', 'PUT', 'HEAD', 'DELETE']; if (!_.includes(httpVerbs, method)) { throw new Error(method + " is not a valid HTTP method.") } }; function checkForHttpBackend(opts) { opts = opts || {}; var backend = opts.backend || {}; if (!_.isFunction(backend.whenGET)) { throw new Error( "$httpBackend not detected. You must add it when initializing, like Facade.initialize({backend: $httpBackend})" ); } } function checkIfAlreadyInitialized() { if (backendIsInitialized) { console.warn("Facade is already initialized. Generally this shouldn't happen. So everything will be cleared, and initialized again." + " Thus, you shouldn't rely on any data added before the first initialization." ) } } function throwIfRegex(route) { if (_.isRegExp(route)) { throw new Error("Regex routes can't be used for item routes. Either make 'onItem' false" + " or make the route a string") } } function checkForRequiredRouteArgs(opts) { checkForValidMethod(opts.method); if (!_.isString(opts.route) && !_.isRegExp(opts.route)) { throw new Error("You must supply a route (eg: '/my_route') as either a string or regex"); } if (!opts.callback) { throw new Error("You must supply a response callback for custom routes."); } } function checkForValidResponse(response) { if (!_.isArray(response)) { throw new Error("Response must be an array");} if (response.length !== 4) { throw new Error("Response does not appear to be in the form of [status, data, headers, status_text]" ) } } function checkForUrlExistence(method, url) { var inquiredRoute = [method, url].join(' '); } }).call(this);