├── .gitignore ├── LICENSE.txt ├── apey-eye ├── Auth.js ├── BaseClass.js ├── BaseRouter.js ├── Decorators.js ├── DefaultProperties.js ├── Exceptions.js ├── FormatNegotiator.js ├── Formatters.js ├── GenericResource.js ├── HTTPCodes.js ├── Input.js ├── Model.js ├── ModelRegister.js ├── Resource.js ├── RethinkDBAdapter.js ├── RethinkDBModel.js ├── bluebird-extended.js ├── config │ ├── database.js │ ├── router.js │ └── server.js ├── index.js ├── models │ ├── RoleModel.js │ └── UserModel.js └── routers │ ├── HapiGenericRouter.js │ ├── HapiRouter.js │ ├── KoaGenericRouter.js │ └── KoaRouter.js ├── example ├── index-hapi.js ├── index-koa.js ├── models │ ├── CategoryModel.js │ ├── ClientModel.js │ ├── CourierModel.js │ ├── OrderModel.js │ ├── OrderProductModel.js │ ├── ProductModel.js │ ├── RestaurantModel.js │ └── ScheduleModel.js └── resources │ ├── CategoryResource.js │ ├── ClientResource.js │ ├── CourierResource.js │ ├── OrderProductResource.js │ ├── OrderResource.js │ ├── ProductResource.js │ ├── RestaurantResource.js │ └── ScheduleResource.js ├── gulpfile.js ├── package.json ├── readme.md ├── runbabel.cmd └── test ├── Input.js ├── models.js ├── requests-hapi.js ├── resources.js └── router.js /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | coverage 3 | node_modules 4 | .idea -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 glazedSolutions 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /apey-eye/Auth.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by GlazedSolutions on 17/04/2015. 3 | */ 4 | import passportLocal from 'passport-local'; 5 | import passportHTTP from 'passport-http'; 6 | import co from 'co'; 7 | import ModelRegister from './ModelRegister.js'; 8 | import UserModel from './models/UserModel'; 9 | 10 | let LocalStrategy = passportLocal.Strategy; 11 | let BasicStrategy = passportHTTP.BasicStrategy; 12 | 13 | class Auth{ 14 | constructor(passport){ 15 | 16 | passport.serializeUser(function (user, done) { 17 | done(null, user.id); 18 | }); 19 | 20 | passport.deserializeUser(function (id, done) { 21 | let UserModel = ModelRegister.model('user'); 22 | UserModel.fetchOne({id: id}) 23 | .then(user => { 24 | done(null, user); 25 | }) 26 | .catch(err=> { 27 | done(err, null); 28 | }); 29 | }); 30 | 31 | passport.use('local', new LocalStrategy({ 32 | passReqToCallback: true // allows us to pass back the entire request to the callback 33 | }, function (req, username, password, done) { 34 | UserModel.fetch({resourceProperties: {_filter: {username: username}}}).then(usersList => { 35 | if (usersList.length == 0) { 36 | done("User not found") 37 | } 38 | else if (usersList.length > 1) { 39 | done(`Error finding user with username equals to '${username}'`) 40 | } 41 | else { 42 | let user = usersList[0]; 43 | if (user.obj.password === password) { 44 | done(null, user) 45 | } 46 | else { 47 | done("Invalid password"); 48 | } 49 | } 50 | }); 51 | } 52 | )); 53 | 54 | passport.use('basic', new BasicStrategy({ 55 | passReqToCallback: true // allows us to pass back the entire request to the callback 56 | }, function (req, username, password, done) { 57 | UserModel.fetch({resourceProperties: {_filter: {username: username}}}).then(usersList => { 58 | if (usersList.length == 0) { 59 | done("User not found") 60 | } 61 | else if (usersList.length > 1) { 62 | done(`Error finding user with username equals to '${username}'`) 63 | } 64 | else { 65 | let user = usersList[0]; 66 | if (user.obj.password === password) { 67 | done(null, user) 68 | } 69 | else { 70 | done("Invalid password"); 71 | } 72 | } 73 | }) 74 | .catch( error => { 75 | console.error(error.stack) 76 | done("Error finding user"); 77 | }); 78 | } 79 | )); 80 | this.passport = passport; 81 | 82 | } 83 | authenticate(){ 84 | return this.passport.authenticate.apply(this.passport, arguments); 85 | } 86 | } 87 | 88 | export default Auth; 89 | -------------------------------------------------------------------------------- /apey-eye/BaseClass.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by GlazedSolutions on 26/03/2015. 3 | */ 4 | import * as Decorators from './Decorators'; 5 | import BluebirdExtended from './bluebird-extended'; 6 | import _ from 'underscore'; 7 | import ModelRegister from './ModelRegister.js'; 8 | import bluebird from 'bluebird'; 9 | import async from 'async'; 10 | 11 | let asyncEach = bluebird.promisify(async.each); 12 | 13 | 14 | class BaseClass extends BluebirdExtended { 15 | constructor(executor) { 16 | super(); 17 | let Class = this; 18 | super(function () { 19 | try { 20 | var method = executor.bind(Class); 21 | return method().then(function (obj) { 22 | return Promise.resolve(obj); 23 | }).catch(function (error) { 24 | return Promise.reject(error); 25 | }) 26 | } 27 | catch (err) { 28 | return Promise.reject(err); 29 | } 30 | }) 31 | } 32 | static getProperties(method = undefined){ 33 | var outputProperties = this.getOutput(method) || {}; 34 | var queryProperties = this.getQuery(method) || {}; 35 | 36 | return _.clone(_.extend(outputProperties, queryProperties)); 37 | } 38 | static joinProperties(resourceProperties = {}, method = undefined) { 39 | var outputProperties = this.getOutput(method) || {}; 40 | var queryProperties = this.getQuery(method) || {}; 41 | 42 | var resultProperties = _.clone(_.extend(outputProperties, queryProperties)); 43 | 44 | if (resourceProperties._sort) { 45 | resultProperties._sort = resourceProperties._sort; 46 | } 47 | 48 | if (resultProperties._filter != undefined && resourceProperties._filter != undefined) { 49 | resultProperties._filter = _.extend(resourceProperties._filter, resultProperties._filter); 50 | } 51 | else if (resourceProperties._filter != undefined) { 52 | resultProperties._filter = resourceProperties._filter; 53 | } 54 | 55 | if (resultProperties._page_size != undefined && resourceProperties._pagination && resourceProperties._pagination._page_size != undefined) { 56 | resultProperties._pagination = {_page_size: _.min([resultProperties._page_size, resourceProperties._pagination.page_size])}; 57 | } 58 | else if (resourceProperties._pagination != undefined && resourceProperties._pagination._page_size != undefined) { 59 | resultProperties._pagination = {_page_size: resourceProperties._pagination._page_size}; 60 | } 61 | else { 62 | resultProperties._pagination = {_page_size: resultProperties._page_size}; 63 | } 64 | 65 | 66 | if (resourceProperties._pagination && resourceProperties._pagination._page) { 67 | resultProperties._pagination._page = resourceProperties._pagination._page; 68 | } 69 | else { 70 | resultProperties._pagination._page = 1; 71 | } 72 | 73 | if (resultProperties._fields != undefined && resourceProperties._fields != undefined) { 74 | resultProperties._fields = _.intersection(resultProperties._fields, resourceProperties._fields); 75 | } 76 | else if (resourceProperties._fields != undefined) { 77 | resultProperties._fields = resourceProperties._fields; 78 | } 79 | 80 | if (resultProperties._embedded != undefined && resourceProperties._embedded != undefined) { 81 | resultProperties._embedded = _.intersection(resultProperties._embedded, resourceProperties._embedded); 82 | } 83 | else if (resourceProperties._embedded != undefined) { 84 | resultProperties._embedded = resourceProperties._embedded; 85 | } 86 | 87 | return resultProperties; 88 | 89 | } 90 | 91 | static getName(method) { 92 | return Decorators.getProperty(this, "name", method) 93 | } 94 | 95 | static getInput(method) { 96 | return Decorators.getProperty(this, "input", method) 97 | } 98 | 99 | static getOutput(method) { 100 | return Decorators.getProperty(this, "output", method) 101 | } 102 | 103 | static getQuery(method) { 104 | return Decorators.getProperty(this, "query", method) 105 | } 106 | 107 | static async valid(data, method) { 108 | let input = this.getInput(method); 109 | if (input) { 110 | return await input.valid(data); 111 | } 112 | else { 113 | return true; 114 | } 115 | } 116 | 117 | static async processOutput(obj, properties) { 118 | obj = this.selectFields(obj, properties._fields); 119 | obj = await this.processRelations(obj, properties._embedded, properties._fields); 120 | 121 | return obj; 122 | } 123 | 124 | static selectFields(serializedObj, fields) { 125 | if (fields) { 126 | serializedObj.obj = _.pick(serializedObj.obj, fields) 127 | } 128 | return serializedObj; 129 | } 130 | 131 | static async processRelations(serializedObj, embeddedFields, showFields) { 132 | let self = this, 133 | input = self.getInput(); 134 | 135 | if (!input) { 136 | return serializedObj; 137 | } 138 | let keys = Object.keys(input.properties); 139 | if (showFields && showFields.length > 0) { 140 | keys = _.intersection(keys, showFields); 141 | } 142 | 143 | await asyncEach(keys, function (field, callback) { 144 | self.processRelationsAux(input, field, serializedObj, embeddedFields) 145 | .then(item => { 146 | callback(); 147 | }) 148 | .catch(error => { 149 | console.error(error.stack); 150 | callback(); 151 | }); 152 | }); 153 | 154 | return serializedObj; 155 | } 156 | 157 | static async processRelationsAux(input, field, serializedObj, embeddedFields) { 158 | let property = input.properties[field]; 159 | if (property.type === 'reference') { 160 | if (serializedObj.obj[field]) { 161 | 162 | if (embeddedFields && embeddedFields.indexOf(field) > -1) { 163 | let RelatedModelClass = ModelRegister.model(property.model), 164 | embeddedObj = await RelatedModelClass.fetchOne({id: serializedObj.obj[field]}); 165 | 166 | serializedObj.obj[field] = embeddedObj.obj; 167 | } 168 | } 169 | } 170 | else if (property.type === "collection") { 171 | let RelatedModelClass = ModelRegister.model(property.model), 172 | _filter = {}; 173 | _filter[property.inverse] = serializedObj.id; 174 | let embeddedObj = await RelatedModelClass.fetch({ 175 | resourceProperties: { 176 | _filter: _filter 177 | } 178 | }); 179 | 180 | if (embeddedFields && embeddedFields.indexOf(field) > -1) { 181 | serializedObj.obj[field] = embeddedObj.obj; 182 | } 183 | else { 184 | serializedObj.obj[field] = _.reduce(embeddedObj, function (memo, elem) { 185 | return memo.concat(elem.id) 186 | }, []); 187 | } 188 | } 189 | else if (property.type === "manyToMany") { 190 | let ThroughModelClass = ModelRegister.model(property.through); 191 | 192 | if (!ThroughModelClass) { 193 | throw new Error(`BaseClass: through model '${property.through}' class for field '${field}' not exists.`); 194 | } 195 | else { 196 | let throughInput = ThroughModelClass.getInput(); 197 | 198 | let sourceModelName = this.getName(); 199 | let sourceField = _.findKey(throughInput.properties, function (obj) { 200 | return obj.model === sourceModelName; 201 | }); 202 | 203 | let targetField = _.findKey(throughInput.properties, function (obj) { 204 | return obj.model === property.model; 205 | }); 206 | 207 | let _filter = {}, 208 | _embedded = [targetField]; 209 | _filter[sourceField] = serializedObj.id; 210 | 211 | let embeddedObjList; 212 | if (embeddedFields && embeddedFields.indexOf(field) > -1) { 213 | embeddedObjList = await ThroughModelClass.fetch({ 214 | resourceProperties: { 215 | _filter: _filter, 216 | _embedded: _embedded 217 | } 218 | }); 219 | } 220 | else { 221 | embeddedObjList = await ThroughModelClass.fetch({ 222 | resourceProperties: { 223 | _filter: _filter 224 | } 225 | }); 226 | } 227 | if (embeddedObjList) { 228 | serializedObj.obj[field] = _.uniq(_.reduce(embeddedObjList, function (memo, elem) { 229 | return memo.concat(elem.obj[targetField]) 230 | }, [])); 231 | } 232 | else { 233 | serializedObj.obj[field] = undefined; 234 | } 235 | 236 | } 237 | } 238 | } 239 | } 240 | 241 | export default BaseClass; -------------------------------------------------------------------------------- /apey-eye/BaseRouter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by GlazedSolutions on 16/04/2015. 3 | */ 4 | import * as Exceptions from './Exceptions.js'; 5 | import Resource from './Resource.js'; 6 | import GenericResource from './GenericResource.js'; 7 | import * as Decorators from './Decorators.js'; 8 | import RoleModel from './models/RoleModel.js'; 9 | import RouterConfig from './config/router.js'; 10 | 11 | class BaseRouter { 12 | 13 | register(entries) { 14 | if (entries) { 15 | entries.forEach(entry => { 16 | if (entry.resource && entry.resource.prototype instanceof Resource) { 17 | 18 | let obj = { 19 | path: entry.path || entry.resource.getName(), 20 | resource: entry.resource 21 | }; 22 | let resourceName = BaseRouter.resourceName(obj.path); 23 | if (resourceName) { 24 | if (!this.entries[resourceName]) { 25 | this.entries[resourceName] = obj.resource; 26 | this.appendBaseMethods(obj); 27 | } 28 | else { 29 | throw new Error(`BaseRouter: path '${resourceName}' already in use.`); 30 | } 31 | } 32 | else { 33 | throw new Error(`BaseRouter: invalid resource path "${obj.path}"`); 34 | } 35 | } 36 | else { 37 | throw new Error('BaseRouter: received invalid entries, must receive objects with one path and one resource class.'); 38 | } 39 | }); 40 | } 41 | else { 42 | throw new Error("BaseRouter: register must receive an array of routing properties."); 43 | } 44 | } 45 | rootOptions(baseUri){ 46 | let options = {}, 47 | resources = {}, 48 | basePath = RouterConfig.basePath||'/'; 49 | 50 | Object.keys(this.entries).forEach( entry => { 51 | resources[entry] = `${baseUri}${RouterConfig.basePath}/${entry}` ; 52 | }); 53 | options.resources = resources; 54 | return options; 55 | } 56 | appendBaseMethods() { 57 | throw new Error("BaseRouter: Method not implemented. Must be overridden by subclass"); 58 | } 59 | 60 | static async checkUserRole(user, allowedRoles) { 61 | if (!allowedRoles) { 62 | return true; 63 | } 64 | else { 65 | if (allowedRoles.length === 0) { 66 | throw new Exceptions.Forbidden(); 67 | } 68 | else { 69 | 70 | if (!user || !user.obj.role) { 71 | throw new Exceptions.Forbidden(); 72 | } 73 | if (allowedRoles.indexOf(user.obj.role) > -1) { 74 | return true; 75 | } 76 | else { 77 | let childRoles = await RoleModel.fetch({ 78 | resourceProperties: { 79 | _filter: { 80 | parentRole: user.obj.role 81 | }, 82 | _fields: ["id"] 83 | } 84 | }); 85 | while (childRoles.length > 0) { 86 | let role = childRoles.shift(); 87 | if (allowedRoles.indexOf(role.id) > -1) { 88 | return true; 89 | } 90 | else { 91 | let newChilds = await RoleModel.fetch({ 92 | resourceProperties: { 93 | _filter: { 94 | parentRole: role.id 95 | }, 96 | _fields: ["id"] 97 | } 98 | }); 99 | childRoles = childRoles.concat(newChilds); 100 | } 101 | } 102 | throw new Exceptions.Forbidden(); 103 | } 104 | } 105 | } 106 | } 107 | 108 | static getResourceMethod(request, resourceClass) { 109 | let pathType; 110 | if (request.params.action && request.params.id) { 111 | pathType = 'instance_action'; 112 | } 113 | else if(request.params.id) { 114 | pathType = 'instance'; 115 | } 116 | else { 117 | pathType = 'collection'; 118 | } 119 | return resourceClass.getResourceMethod({ 120 | method : request.method, 121 | pathType : pathType, 122 | id : request.params.id, 123 | action : request.params.action 124 | }); 125 | } 126 | 127 | static parseRequest(request) { 128 | var requestProperties = {}; 129 | requestProperties._filter = BaseRouter.parseFilters(request.query._filter); 130 | requestProperties._sort = BaseRouter.parseSort(request.query._sort); 131 | requestProperties._pagination = BaseRouter.parsePagination(request.query._page, request.query._page_size); 132 | requestProperties._fields = BaseRouter.parseFields(request.query._fields); 133 | requestProperties._embedded = BaseRouter.parseEmbedded(request.query._embedded); 134 | requestProperties._format = BaseRouter.parseFormat(request.query._format); 135 | requestProperties._mediaType = request.headers.accept; 136 | 137 | return requestProperties; 138 | } 139 | 140 | static parseFilters(_filter) { 141 | if (_filter) { 142 | try { 143 | var filters = JSON.parse(_filter); 144 | } catch (e) { 145 | //console.error(`BaseRouter: Cannot parse filters JSON.`); 146 | return; 147 | } 148 | if (filters) { 149 | return filters; 150 | } 151 | } 152 | } 153 | 154 | static parseSort(_sort) { 155 | 156 | if (_sort) { 157 | try { 158 | var sortParsed = JSON.parse(_sort); 159 | } catch (e) { 160 | //console.error("BaseRouter: Cannot parse _sort JSON."); 161 | return; 162 | } 163 | if (Array.isArray(sortParsed)) { 164 | var sortArray = []; 165 | try { 166 | sortParsed.some(s => { 167 | let order, 168 | field, 169 | sortObj = {}; 170 | 171 | if (typeof s != 'string') { 172 | sortArray = undefined; 173 | throw new Error(); 174 | } 175 | 176 | if (s.charAt(0) === '-') { 177 | order = -1; 178 | field = s.substr(1); 179 | 180 | } 181 | else { 182 | order = 1; 183 | field = s; 184 | } 185 | 186 | sortObj[field] = order; 187 | sortArray.push(sortObj); 188 | }); 189 | } 190 | catch (e) { 191 | //console.error("BaseRouter: _sort properties must be an array of strings.") 192 | } 193 | 194 | if (sortArray && sortArray.length > 0) { 195 | return sortArray; 196 | } 197 | } 198 | else { 199 | //console.error("BaseRouter: _sort properties must be an array."); 200 | } 201 | 202 | } 203 | 204 | } 205 | 206 | static parsePagination(_page, _page_size) { 207 | if (_page) { 208 | var page = parseInt(_page); 209 | 210 | if (isNaN(page)) { 211 | //console.error("BaseRouter: Cannot parse '_page' value. Must receive an integer."); 212 | } 213 | } 214 | if (_page_size) { 215 | var pageSize = parseInt(_page_size); 216 | 217 | if (isNaN(pageSize)) { 218 | //console.error("BaseRouter: Cannot parse '_page_size' value. Must receive an integer."); 219 | } 220 | } 221 | 222 | if (page || pageSize) { 223 | return { 224 | _page: page, 225 | _page_size: pageSize 226 | }; 227 | } 228 | } 229 | 230 | static parseFields(_fields) { 231 | 232 | if (_fields) { 233 | try { 234 | var fieldsParsed = JSON.parse(_fields); 235 | } catch (e) { 236 | //console.error("BaseRouter: Cannot parse _fields JSON."); 237 | return; 238 | } 239 | if (Array.isArray(fieldsParsed)) { 240 | var fieldsArray = []; 241 | try { 242 | fieldsParsed.forEach(s => { 243 | if (typeof s !== 'string') { 244 | fieldsArray = undefined; 245 | throw new Error(); 246 | } else { 247 | fieldsArray.push(s); 248 | } 249 | }); 250 | } catch (e) { 251 | //console.error("BaseRouter: _fields properties must be an array of strings.") 252 | } 253 | 254 | if (fieldsArray && fieldsArray.length > 0) { 255 | return fieldsArray; 256 | } 257 | } else { 258 | //console.error("BaseRouter: _fields properties must be an array."); 259 | } 260 | } 261 | } 262 | 263 | static parseEmbedded(_embedded) { 264 | 265 | if (_embedded) { 266 | try { 267 | var embeddedParsed = JSON.parse(_embedded); 268 | } catch (e) { 269 | //console.error("BaseRouter: Cannot parse _embedded JSON."); 270 | return; 271 | } 272 | if (Array.isArray(embeddedParsed)) { 273 | var embeddedArray = []; 274 | try { 275 | embeddedParsed.forEach(s => { 276 | if (typeof s !== 'string') { 277 | embeddedArray = false; 278 | } 279 | else { 280 | embeddedArray.push(s); 281 | } 282 | }); 283 | } catch (e) { 284 | //console.error("BaseRouter: _embedded properties must be an array of strings.") 285 | } 286 | 287 | if (embeddedArray && embeddedArray.length > 0) { 288 | return embeddedArray; 289 | } 290 | } 291 | else { 292 | //console.error("BaseRouter: _embedded properties must be an array."); 293 | } 294 | } 295 | } 296 | 297 | static parseFormat(_format) { 298 | if (typeof _format === 'string') { 299 | return _format; 300 | } 301 | } 302 | 303 | static resourceName(resourcePath) { 304 | var pathRegEx = /^\/?[^\/][a-z0-9\-_]+/i; 305 | 306 | var match = resourcePath.match(pathRegEx); 307 | if (match) { 308 | var resourceName; 309 | if (match[0].charAt(0) === '/') { 310 | resourceName = match[0].substring(1); 311 | } 312 | else { 313 | resourceName = match[0]; 314 | } 315 | return resourceName; 316 | } 317 | else { 318 | return false; 319 | } 320 | } 321 | 322 | static createGenericResourceClass(resourceName) { 323 | @Decorators.Name(resourceName) 324 | class NoBackendResource extends GenericResource { 325 | } 326 | NoBackendResource.noBackend = true; 327 | 328 | return NoBackendResource; 329 | } 330 | } 331 | export default BaseRouter; -------------------------------------------------------------------------------- /apey-eye/Decorators.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by GlazedSolutions on 03/03/2015. 3 | */ 4 | import BaseClass from './BaseClass.js'; 5 | import _ from 'underscore'; 6 | 7 | function Annotation(type, properties) { 8 | return function decorator(target, key, descriptor) { 9 | if (descriptor) { 10 | descriptor.value.annotations = descriptor.value.annotations || {}; 11 | descriptor.value.annotations[type] = properties; 12 | } 13 | else { 14 | target.annotations = target.annotations || {}; 15 | target.annotations[type] = properties; 16 | } 17 | return descriptor; 18 | }; 19 | } 20 | 21 | function InputAnnotation(properties) { 22 | let Input = require('./Input'); 23 | 24 | if (!(properties instanceof Input)) { 25 | throw new Error("Decorators: @Input constructor must receive an Input instance."); 26 | } 27 | return Annotation('input', properties); 28 | } 29 | export {InputAnnotation as Input} 30 | 31 | function NameAnnotation(properties) { 32 | if (typeof properties != 'string') { 33 | throw new Error("Decorators: @Name property value must be a string.") 34 | } 35 | return function decorator(target, key, descriptor) { 36 | if (descriptor) { 37 | descriptor.value.annotations = descriptor.value.annotations || {}; 38 | descriptor.value.annotations[type] = properties; 39 | } 40 | else { 41 | target.annotations = target.annotations || {}; 42 | target.annotations['name'] = properties; 43 | 44 | var Model = require('./Model'); 45 | if(target.prototype instanceof Model){ 46 | var ModelRegister = require('./ModelRegister'); 47 | ModelRegister.register(properties, target); 48 | } 49 | } 50 | return descriptor; 51 | }; 52 | } 53 | export {NameAnnotation as Name} 54 | 55 | function QueryAnnotation(properties) { 56 | 57 | if (properties != undefined && typeof properties === "object" && !Array.isArray(properties)) { 58 | Object.keys(properties).forEach(property => { 59 | if (QueryAnnotation.AllowedProperties.indexOf(property) === -1) { 60 | throw new Error(`Annotations: @Query property '${property}' aren't allowed. Use one of ${QueryAnnotation.AllowedProperties}`); 61 | } 62 | }); 63 | 64 | if (properties._sort != undefined) { 65 | properties._sort = parseSort(properties._sort); 66 | } 67 | if (properties._page_size != undefined) { 68 | parsePageSize(properties._page_size); 69 | } 70 | if (properties._filter != undefined) { 71 | parseFilters(properties._filter); 72 | } 73 | } 74 | else{ 75 | throw new Error("Decorators: @Query must receive an object."); 76 | } 77 | 78 | return Annotation('query', properties); 79 | } 80 | QueryAnnotation.AllowedProperties = ['_sort', '_filter', '_page_size']; 81 | export {QueryAnnotation as Query} 82 | 83 | function parseSort(sortProperties) { 84 | var sortingArray = []; 85 | 86 | if (Array.isArray(sortProperties)) { 87 | sortProperties.forEach(s => { 88 | if (typeof s !== 'string') { 89 | throw new Error("Decorators: All elements in @Query._sort array must be strings."); 90 | } 91 | let order, field, sortObj = {}; 92 | 93 | if (s.charAt(0) === '-') { 94 | order = -1; 95 | field = s.substr(1); 96 | } 97 | else { 98 | order = 1; 99 | field = s; 100 | } 101 | sortObj[field] = order; 102 | sortingArray.push(sortObj); 103 | }); 104 | sortProperties = sortingArray; 105 | return sortProperties; 106 | } 107 | else { 108 | throw new Error("Decorators: Argument received in @Query._sort must be an array."); 109 | } 110 | } 111 | function parsePageSize(pageSizeProperties) { 112 | 113 | if (typeof pageSizeProperties != 'number') { 114 | throw new Error("Decorators: @Query._page_size property value must be a number."); 115 | } 116 | else if (pageSizeProperties % 1 !== 0 || pageSizeProperties <= 0) { 117 | throw new Error("Decorators: @Query._page_size property value must be a positive integer.") 118 | } 119 | } 120 | function parseFilters(filterProperties) { 121 | 122 | if (typeof filterProperties != "object" || Array.isArray(filterProperties)) { 123 | throw new Error("Decorators: @Query._filter must receive an object with filter properties."); 124 | } 125 | } 126 | 127 | function OutputAnnotation(properties) { 128 | if (properties != undefined && typeof properties === "object" && !Array.isArray(properties)) { 129 | Object.keys(properties).forEach(property => { 130 | if (OutputAnnotation.AllowedProperties.indexOf(property) === -1) { 131 | throw new Error(`Annotations: @Output property '${property}' aren't allowed. Use one of ${OutputAnnotation.AllowedProperties}`); 132 | } 133 | }); 134 | 135 | if (properties._embedded != undefined) { 136 | parseEmbedded(properties._embedded); 137 | } 138 | if (properties._fields != undefined) { 139 | parseFields(properties._fields); 140 | } 141 | return Annotation('output', properties); 142 | } 143 | else if (properties != undefined) { 144 | throw new Error("Decorators: @Query must receive an object."); 145 | } 146 | } 147 | OutputAnnotation.AllowedProperties = ['_fields', '_embedded']; 148 | export {OutputAnnotation as Output} 149 | 150 | function parseEmbedded(embeddedProperties) { 151 | if (Array.isArray(embeddedProperties)) { 152 | embeddedProperties.forEach(s => { 153 | if (typeof s !== 'string') { 154 | throw new Error("Decorators: All elements in @Output._embedded array must be strings."); 155 | } 156 | }); 157 | } 158 | else { 159 | throw new Error("Decorators: Argument received in @Output._embedded must be an array."); 160 | } 161 | } 162 | function parseFields(fieldsProperties) { 163 | if (Array.isArray(fieldsProperties)) { 164 | fieldsProperties.forEach(s => { 165 | if (typeof s !== 'string') { 166 | throw new Error("Decorators: All elements in @Output._fields array must be strings."); 167 | } 168 | }); 169 | } 170 | else { 171 | throw new Error("Decorators: Argument received in @Output._fields must be an array."); 172 | } 173 | } 174 | 175 | function ModelAnnotation(modelClass) { 176 | var Model = require('./Model'); 177 | 178 | if (!(modelClass.prototype instanceof Model )) { 179 | throw new Error("Decorators: @Model must receive an instance of Model class as argument"); 180 | } 181 | return Annotation('model', modelClass); 182 | } 183 | export {ModelAnnotation as Model} 184 | 185 | function MethodsAnnotation(properties) { 186 | 187 | if (properties === undefined) { 188 | this.properties = undefined; 189 | } 190 | else if (properties && !Array.isArray(properties)) { 191 | throw new Error("Decorators: Argument received in @Methods must be an array."); 192 | } 193 | else{ 194 | properties.forEach(s => { 195 | if (typeof s !== 'string') { 196 | throw new Error("Decorators: All elements in @Roles array must be strings."); 197 | } 198 | }); 199 | } 200 | return Annotation('methods', properties); 201 | 202 | } 203 | export {MethodsAnnotation as Methods} 204 | 205 | function FormatAnnotation(formatClass) { 206 | var BaseFormatter = require('./Formatters').BaseFormatter; 207 | 208 | if (!(formatClass.prototype instanceof BaseFormatter )) { 209 | throw new Error("Decorators: @Format must receive a class with base class BaseFormatter."); 210 | } 211 | return Annotation('format', formatClass); 212 | } 213 | export {FormatAnnotation as Format} 214 | 215 | function MediaTypeAnnotation(mediaType) { 216 | if (typeof mediaType != 'string') { 217 | throw new Error("Decorators: @MediaType property value must be a string.") 218 | } 219 | return Annotation('mediaType', mediaType); 220 | 221 | 222 | } 223 | export {MediaTypeAnnotation as MediaType} 224 | 225 | function AuthenticationAnnotation(authenticationType) { 226 | if (authenticationType != undefined && typeof authenticationType != 'string') { 227 | throw new Error("Decorators: @Authentication property value must be a string.") 228 | } 229 | return Annotation('authentication', authenticationType); 230 | } 231 | export {AuthenticationAnnotation as Authentication} 232 | 233 | function RolesAnnotation(properties) { 234 | if (properties === undefined) { 235 | this.properties = undefined; 236 | } 237 | else if (properties && !Array.isArray(properties)) { 238 | throw new Error("Decorators: Argument received in @Roles must be an array."); 239 | } 240 | else{ 241 | properties.forEach(s => { 242 | if (typeof s !== 'string') { 243 | throw new Error("Decorators: All elements in @Roles array must be strings."); 244 | } 245 | }); 246 | } 247 | 248 | return Annotation('roles', properties); 249 | 250 | } 251 | export {RolesAnnotation as Roles} 252 | 253 | function ActionAnnotation() { 254 | return function decorator(target, key, descriptor) { 255 | if (descriptor) { 256 | let actions, 257 | ResourceClass; 258 | 259 | if(target.prototype instanceof BaseClass){ 260 | ResourceClass = target; 261 | if(!ResourceClass.actions){ 262 | target.actions = {instance:{},collection:{}}; 263 | } 264 | actions = ResourceClass.actions.collection; 265 | } 266 | else if(target.constructor.prototype instanceof BaseClass){ 267 | ResourceClass = target.constructor; 268 | if(!ResourceClass.actions){ 269 | ResourceClass.actions = {instance:{},collection:{}}; 270 | } 271 | actions = ResourceClass.actions.instance; 272 | } 273 | if (!actions[key]) { 274 | actions[key] = descriptor.value; 275 | return wrapActionDescriptor(ResourceClass, descriptor); 276 | } else { 277 | throw new Error('Action \'' + key + ' already exists.'); 278 | } 279 | } 280 | } 281 | } 282 | export {ActionAnnotation as Action} 283 | 284 | function wrapActionDescriptor(ResourceClass, descriptor){ 285 | let oldDescritor = _.clone(descriptor); 286 | 287 | descriptor.value = async function() { 288 | let result = await oldDescritor.value.call(this, arguments) 289 | return ResourceClass._serialize(undefined, result); 290 | }; 291 | return descriptor; 292 | } 293 | 294 | function DocumentationAnnotation(documentation) { 295 | if (documentation != undefined && typeof documentation === "object" && !Array.isArray(documentation)) { 296 | return Annotation('documentation', documentation); 297 | } 298 | else{ 299 | throw new Error("Decorators: @Documentation must receive an object."); 300 | } 301 | } 302 | export {DocumentationAnnotation as Documentation} 303 | 304 | function getProperty(source, type, method) { 305 | let property, 306 | annotation; 307 | 308 | let getAnnotation = function (annotations, annotationType) { 309 | return annotations[annotationType]; 310 | }; 311 | if (method) { 312 | let annotations = method.annotations; 313 | 314 | if (annotations) { 315 | annotation = getAnnotation(annotations, type); 316 | 317 | if (annotation) { 318 | property = annotation; 319 | if (property) { 320 | return property; 321 | } 322 | } 323 | } 324 | } 325 | if (source && source.annotations) { 326 | annotation = getAnnotation(source.annotations, type); 327 | if (annotation) { 328 | return annotation; 329 | } 330 | else { 331 | return 332 | } 333 | } 334 | }; 335 | export {getProperty} -------------------------------------------------------------------------------- /apey-eye/DefaultProperties.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by GlazedSolutions on 04/03/2015. 3 | */ 4 | 5 | import * as Formatters from './Formatters'; 6 | 7 | export const PageSize = 10; 8 | export const HTTPMethods = ["GET", "POST", "PUT", "PATCH", "DELETE","OPTIONS"] 9 | export const Formatter = Formatters.JSONFormat; -------------------------------------------------------------------------------- /apey-eye/Exceptions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by GlazedSolutions on 19/03/2015. 3 | */ 4 | 5 | class NotFound extends Error{ 6 | constructor(id){ 7 | super(); 8 | if(id){ 9 | this.message = `${id} not found`; 10 | } 11 | else{ 12 | this.message = "Not Found"; 13 | } 14 | } 15 | } 16 | class ModelNotFound extends Error{ 17 | constructor(resourceName){ 18 | super(); 19 | if(resourceName){ 20 | this.message = `Model not found for resource '${resourceName}'.`; 21 | } 22 | else{ 23 | this.message = `Model not found for this resource.'`; 24 | } 25 | } 26 | } 27 | class MethodNotAllowed extends Error{ 28 | constructor(){ 29 | super(); 30 | this.message = `Method not allowed`; 31 | } 32 | } 33 | class NotImplemented extends Error{ 34 | constructor(){ 35 | super(); 36 | this.message = `Method not implemented`; 37 | } 38 | } 39 | class BadRequest extends Error{ 40 | constructor(message){ 41 | super(); 42 | this.message = `Bad Request: ${message}`; 43 | } 44 | } 45 | class Unauthorized extends Error{ 46 | constructor(){ 47 | super(); 48 | } 49 | } 50 | 51 | class Forbidden extends Error{ 52 | constructor(){ 53 | super(); 54 | } 55 | } 56 | 57 | export {NotFound, ModelNotFound,MethodNotAllowed, NotImplemented, BadRequest, Unauthorized,Forbidden} -------------------------------------------------------------------------------- /apey-eye/FormatNegotiator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by GlazedSolutions on 17/03/2015. 3 | */ 4 | import * as Formatters from './Formatters'; 5 | import * as DefaultProperties from './DefaultProperties'; 6 | import Negotiator from 'negotiator'; 7 | 8 | class FormatNegotiator { 9 | static selectFormatter(requestProperties) { 10 | let formatters = Formatters.FormattersList, 11 | mediaTypeFormatters = {}; 12 | 13 | if (formatters && formatters.length > 0) { 14 | formatters.forEach(formatter => { 15 | let mediaType = formatter.getMediaType(); 16 | mediaTypeFormatters[mediaType] = formatter; 17 | }); 18 | 19 | let negotiator = new Negotiator({ 20 | headers: { 21 | accept: requestProperties._format || requestProperties._mediaType 22 | } 23 | }); 24 | 25 | let mediaTypeAccepted = negotiator.mediaType(Object.keys(mediaTypeFormatters)); 26 | let formatterAccepted = mediaTypeFormatters[mediaTypeAccepted]; 27 | 28 | if (mediaTypeAccepted && formatterAccepted && (formatterAccepted.prototype instanceof Formatters.BaseFormatter)) { 29 | return formatterAccepted; 30 | } 31 | else { 32 | return DefaultProperties.Formatter; 33 | } 34 | } 35 | else { 36 | return DefaultProperties.Formatter; 37 | return DefaultProperties.Formatter; 38 | } 39 | 40 | } 41 | } 42 | export default FormatNegotiator; -------------------------------------------------------------------------------- /apey-eye/Formatters.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by GlazedSolutions on 16/03/2015. 3 | */ 4 | import * as Decorators from './Decorators'; 5 | let MediaType = Decorators.MediaType; 6 | 7 | class BaseFormatter{ 8 | static format(data){ 9 | throw new Error("Formatters: .format() must be implemented.") 10 | } 11 | static getMediaType(){ 12 | return Decorators.getProperty(this, "mediaType") 13 | } 14 | } 15 | 16 | export {BaseFormatter} 17 | 18 | @MediaType('application/json') 19 | class JSONFormat extends BaseFormatter{ 20 | static format(data){ 21 | return JSON.stringify(data); 22 | } 23 | } 24 | export {JSONFormat}; 25 | 26 | export let FormattersList = [JSONFormat]; -------------------------------------------------------------------------------- /apey-eye/GenericResource.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by GlazedSolutions on 02/03/2015. 3 | */ 4 | import Resource from './Resource'; 5 | import * as Exceptions from './Exceptions'; 6 | import _ from 'underscore'; 7 | 8 | class GenericResource extends Resource { 9 | 10 | constructor(options = {}) { 11 | super(); 12 | super(async () => { 13 | let ResourceClass = this.constructor; 14 | 15 | ResourceClass.checkModel(); 16 | 17 | let properties = ResourceClass.joinProperties(options.requestProperties, ResourceClass.post), 18 | modelClass = ResourceClass.getModel(ResourceClass.post); 19 | 20 | if (!options.data) { 21 | options.data={}; 22 | } 23 | await ResourceClass.valid(options.data); 24 | 25 | if (modelClass) { 26 | let modelObj = await new modelClass({data: options.data, resourceProperties: properties}); 27 | return ResourceClass._serialize(modelObj.id, modelObj.obj) 28 | } 29 | else { 30 | throw new Exceptions.ModelNotFound(ResourceClass.name); 31 | } 32 | }); 33 | } 34 | 35 | static async fetch(options = {}) { 36 | let ResourceClass = this; 37 | 38 | ResourceClass.checkModel(); 39 | let modelClass = ResourceClass.getModel(ResourceClass.fetch), 40 | properties = ResourceClass.joinProperties(options.requestProperties, ResourceClass.fetch); 41 | 42 | if (modelClass) { 43 | let modelObj = await modelClass.fetch({resourceProperties:properties}); 44 | return ResourceClass._serializeArray(modelObj, properties); 45 | } 46 | else { 47 | throw new Exceptions.ModelNotFound(ResourceClass.name); 48 | } 49 | } 50 | 51 | static async fetchOne(options = {}) { 52 | let ResourceClass = this; 53 | 54 | ResourceClass.checkModel(); 55 | 56 | let modelClass = ResourceClass.getModel(ResourceClass.fetch), 57 | properties = ResourceClass.joinProperties(options.requestProperties, ResourceClass.fetch); 58 | 59 | if (modelClass) { 60 | let modelObj = await modelClass.fetchOne({id: options.id, resourceProperties: properties}); 61 | return ResourceClass._serialize(modelObj.id, modelObj.obj) 62 | } 63 | else { 64 | throw new Exceptions.ModelNotFound(ResourceClass.name); 65 | } 66 | } 67 | 68 | async put(options = {}) { 69 | let self = this, 70 | ResourceClass = this.constructor, 71 | modelClass = ResourceClass.getModel(ResourceClass.fetch), 72 | properties = ResourceClass.joinProperties(options.requestProperties, ResourceClass.fetch); 73 | 74 | if (modelClass) { 75 | await ResourceClass.valid(options.data, ResourceClass.prototype.put); 76 | 77 | let modelObj = await modelClass.fetchOne({id: self.id, resourceProperties: properties}); 78 | modelObj = await modelObj.put({data: options.data, resourceProperties: properties}); 79 | self.obj = modelObj.obj; 80 | return self; 81 | } 82 | else { 83 | throw new Exceptions.ModelNotFound(ResourceClass.name); 84 | } 85 | } 86 | 87 | async patch(options = {}) { 88 | let self = this, 89 | ResourceClass = this.constructor, 90 | modelClass = ResourceClass.getModel(ResourceClass.fetch), 91 | properties = ResourceClass.joinProperties(options.requestProperties, ResourceClass.fetch); 92 | 93 | if (modelClass) { 94 | let modelObj = await modelClass.fetchOne({id: self.id, resourceProperties: properties}); 95 | let futureData = _.extend(modelObj.obj, options.data); 96 | await ResourceClass.valid(futureData, ResourceClass.prototype.patch); 97 | 98 | modelObj = await modelObj.patch({data: options.data, resourceProperties: properties}); 99 | self.obj = modelObj.obj; 100 | return self; 101 | } 102 | else { 103 | throw new Exceptions.ModelNotFound(ResourceClass.name); 104 | } 105 | } 106 | 107 | async delete() { 108 | let self = this, 109 | ResourceClass = this.constructor, 110 | modelClass = ResourceClass.getModel(ResourceClass.fetch); 111 | 112 | if (modelClass) { 113 | let modelObj = await modelClass.fetchOne({id: self.id}); 114 | return await modelObj.delete(); 115 | } 116 | else { 117 | throw new Exceptions.ModelNotFound(ResourceClass.name); 118 | } 119 | } 120 | 121 | } 122 | export default GenericResource; -------------------------------------------------------------------------------- /apey-eye/HTTPCodes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by GlazedSolutions on 19/03/2015. 3 | */ 4 | 5 | export default { 6 | success: 200, 7 | noContent: 204, 8 | badRequest: 400, 9 | unauthorized: 401, 10 | forbidden: 403, 11 | notFound: 404, 12 | methodNotAllowed:405, 13 | internalServerError: 500, 14 | notImplemented: 501, 15 | temporaryRedirect: 307 16 | }; -------------------------------------------------------------------------------- /apey-eye/Input.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by GlazedSolutions on 23/03/2015. 3 | */ 4 | import Model from './Model.js'; 5 | import ModelRegister from './ModelRegister.js'; 6 | import * as Exceptions from './Exceptions.js'; 7 | import _ from 'underscore'; 8 | 9 | class Input { 10 | 11 | static URLPattern = /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/; 12 | static ISODatePattern = /^(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z$))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z$))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z$))/; 13 | static DefaultDateNow = 'now'; 14 | 15 | constructor(properties) { 16 | if (Input.validProperties(properties)) { 17 | this.properties = properties; 18 | } 19 | } 20 | 21 | static validProperties(properties) { 22 | if (!properties) { 23 | throw new Error("Input: Input must receive properties as argument."); 24 | } 25 | else if (!(properties instanceof Object) || properties instanceof Array) { 26 | throw new Error("Input: Input must receive an object as properties."); 27 | } 28 | else { 29 | Object.keys(properties).forEach(field => { 30 | var fieldProperties = properties[field]; 31 | 32 | //TYPE MUST BE DEFINED 33 | if (fieldProperties.type === undefined) { 34 | throw new Error(`Input: type of field ${field} must be indicated in properties.`); 35 | } 36 | //IF RELATION MUST HAVE A MODEL 37 | if (allowedRelations.indexOf(fieldProperties.type) > -1 && fieldProperties.model === undefined) { 38 | throw new Error(`Input: model of relation ${field} must be indicated in properties.`); 39 | } 40 | //IF RELATION IS COLLECTION MUST HAVE INVERSE 41 | if (fieldProperties.type === 'collection' && fieldProperties.inverse === undefined) { 42 | throw new Error(`Input: inverse of relation ${field} must be indicated in properties.`); 43 | } 44 | //IF RELATION IS MANYTOMANY MUST HAVE INVERSE AND THROUGH 45 | if (fieldProperties.type === 'manyToMany') { 46 | if (fieldProperties.inverse === undefined) { 47 | throw new Error(`Input: inverse of relation ${field} must be indicated in properties.`); 48 | } 49 | if (fieldProperties.through === undefined) { 50 | throw new Error(`Input: through model of relation ${field} must be indicated in properties.`); 51 | } 52 | } 53 | //TYPE PROPERTY MUST BE A STRING 54 | if (typeof fieldProperties.type != "string") { 55 | throw new Error("Input: value of type property must be a string."); 56 | } 57 | //TYPE OS PROPERTY MUST BE ALLOWED 58 | if (allowedTypes.indexOf(fieldProperties.type) === -1) { 59 | throw new Error(`Input: value of type property must be one of the following values ${allowedTypes}, received '${fieldProperties.type}'.`) 60 | } 61 | //REQUIRED PROPERTY MUST BE BOOLEAN 62 | if (fieldProperties.required != undefined && typeof fieldProperties.required != "boolean") { 63 | throw new Error("Input: value of required property must be a boolean."); 64 | } 65 | //REGEX MUST BE INSTANCE OF RegExp 66 | if (fieldProperties.regex != undefined && !(fieldProperties.regex instanceof RegExp)) { 67 | throw new Error("Input: value of regex property must be a RegExp."); 68 | } 69 | //VALID MUST BE A FUNCTION 70 | if (fieldProperties.valid != undefined && (typeof fieldProperties.valid !== "function")) { 71 | throw new Error("Input: value of valid property must be a function."); 72 | } 73 | //CHOICES MUST BE AN ARRAY AND THE TYPE OF ITS VALUS MUST BE EQUAL TO TYPE PROPERTY 74 | if (fieldProperties.choices != undefined) { 75 | if (!(fieldProperties.choices instanceof Array)) { 76 | throw new Error("Input: value of choices property must be an array."); 77 | } 78 | fieldProperties.choices.forEach(choice => { 79 | if (typeof choice !== fieldProperties.type) { 80 | throw new Error(`Input: value of choices property must be an array of ${fieldProperties.type}`); 81 | } 82 | }); 83 | } 84 | //MODEL PROPERTY MUST BE A STRING WITH NAME OF MODEL 85 | if (fieldProperties.model != undefined && typeof fieldProperties.model !== "string") { 86 | throw new Error("Input: value of model property must be a string."); 87 | } 88 | //THROUGH PROPERTY MUST BE A STRING WITH NAME OF MODEL 89 | if (fieldProperties.model != undefined && typeof fieldProperties.model !== "string") { 90 | throw new Error("Input: value of model property must be a string."); 91 | } 92 | //DEFAULT PROPERTY MUST HAVE THE SAME TYPE OF TYPE PROPERTY 93 | if (fieldProperties.default != undefined && typeof fieldProperties.default != fieldProperties.type) { 94 | if (!(fieldProperties.type === "date" && fieldProperties.default === "now")) { 95 | throw new Error(`Input: value of model property must be a ${fieldProperties.type}`); 96 | } 97 | } 98 | }); 99 | 100 | return true; 101 | } 102 | } 103 | 104 | async valid(data = {}) { 105 | 106 | let fieldsArray = []; 107 | fieldsArray = fieldsArray.concat(Object.keys(data)); 108 | fieldsArray = fieldsArray.concat(Object.keys(this.properties)); 109 | fieldsArray = _.without(fieldsArray, "id"); 110 | 111 | 112 | //TODO fields duplicados 113 | 114 | if (typeof data === 'object' && !Array.isArray(data)) { 115 | for (let field of fieldsArray) { 116 | let dataField = data[field]; 117 | //CHECK IF FIELD RECEIVED IS INCLUDED IN INPUT PROPERTIES 118 | if (!this.properties[field]) { 119 | throw new Exceptions.BadRequest(`Field '${field}' is not allowed`); 120 | } 121 | else { 122 | data[field] = await this.validField(field, dataField); 123 | } 124 | } 125 | return true; 126 | } 127 | else { 128 | throw new Exceptions.BadRequest(`Data received must be an object, received '${data}'`); 129 | } 130 | 131 | 132 | } 133 | 134 | async validField(field, dataField) { 135 | let fieldProperties = this.properties[field]; 136 | 137 | //ASSIGN DEFAULT VALUE 138 | if (dataField === undefined && fieldProperties.default) { 139 | if (fieldProperties.type === 'date' && fieldProperties.default === 'now') { 140 | dataField = new Date().toISOString(); 141 | } 142 | else{ 143 | dataField = fieldProperties.default; 144 | } 145 | } 146 | //CHECK REQUIRED 147 | if (fieldProperties.required === true && dataField === undefined) { 148 | throw new Exceptions.BadRequest(`Field '${field}' is required and its value is ${dataField}`) 149 | } 150 | else if (!fieldProperties.required && dataField == undefined) { 151 | return; 152 | } 153 | //CHECK DATA TYPE 154 | switch (fieldProperties.type) { 155 | case "string": 156 | { 157 | Input.validString(field, dataField); 158 | break; 159 | } 160 | case "number": 161 | { 162 | Input.validNumber(field, dataField); 163 | break; 164 | } 165 | case "boolean": 166 | { 167 | Input.validBoolean(field, dataField); 168 | break; 169 | } 170 | case "date": 171 | { 172 | Input.validDate(field, dataField); 173 | break; 174 | } 175 | case "reference": 176 | { 177 | dataField = await Input.validReference(field, dataField, fieldProperties.model); 178 | break; 179 | } 180 | case "collection": 181 | { 182 | dataField = await Input.validCollection(field, dataField, fieldProperties.model); 183 | break; 184 | } 185 | case "manyToMany": 186 | { 187 | dataField = await Input.validManyToMany(field, dataField, fieldProperties.model); 188 | break; 189 | } 190 | default : 191 | { 192 | throw new Exceptions.BadRequest("Invalid input type"); 193 | } 194 | } 195 | 196 | //TEST REGEX 197 | if (fieldProperties.regex && fieldProperties.regex instanceof RegExp) { 198 | if (!fieldProperties.regex.test(dataField)) { 199 | throw new Exceptions.BadRequest(`Field '${field}' value does not match with its regular expression '${fieldProperties.regex}'`); 200 | } 201 | } 202 | 203 | //TEST VALID FUNCTION 204 | if (fieldProperties.valid && typeof fieldProperties.valid === 'function') { 205 | fieldProperties.valid(dataField); 206 | } 207 | 208 | //CHECK CHOICES 209 | if (fieldProperties.choices && typeof Array.isArray(fieldProperties.choices)) { 210 | if (fieldProperties.choices.indexOf(dataField) === -1) { 211 | throw new Exceptions.BadRequest(`Field '${field} value don't match to choices property, ${fieldProperties.choices}.'`); 212 | } 213 | } 214 | return dataField; 215 | } 216 | 217 | static validString(field, dataField) { 218 | if (typeof dataField === "string") { 219 | return true; 220 | } 221 | else { 222 | throw new Exceptions.BadRequest(`Invalid string value in field '${field}'.`); 223 | } 224 | } 225 | 226 | static validNumber(field, dataField) { 227 | if (typeof dataField === "number") { 228 | return true; 229 | } 230 | else { 231 | throw new Exceptions.BadRequest(`Invalid number value in field '${field}'.`); 232 | } 233 | } 234 | 235 | static validBoolean(field, dataField) { 236 | 237 | if (typeof dataField === "boolean") { 238 | return true; 239 | } 240 | else { 241 | throw new Exceptions.BadRequest(`Invalid boolean value in field '${field}'.`); 242 | } 243 | } 244 | 245 | static validDate(field, dataField) { 246 | if (typeof dataField === "string") { 247 | if (Input.ISODatePattern.test(dataField)) { 248 | return true; 249 | } 250 | else { 251 | throw new Exceptions.BadRequest(`Date in field '${field}' must follow ISO8601 format.`); 252 | } 253 | } 254 | else { 255 | throw new Exceptions.BadRequest(`Invalid date value in field ${field}.`); 256 | } 257 | } 258 | 259 | static async validReference(field, dataField, modelName) { 260 | 261 | let ModelClass = ModelRegister.model(modelName); 262 | 263 | try { 264 | dataField = JSON.parse(dataField); 265 | } catch (e) { 266 | } 267 | 268 | if (typeof dataField === 'object' && !Array.isArray(dataField)) { 269 | if (await ModelClass.valid(dataField)) { 270 | return dataField; 271 | } 272 | } 273 | else { 274 | try { 275 | await ModelClass.fetchOne({id: dataField}) 276 | return dataField; 277 | } 278 | catch (err) { 279 | if (err instanceof Exceptions.NotFound) { 280 | throw new Exceptions.BadRequest(`Referenced value '${dataField}' in field '${field}' not found`); 281 | } 282 | else { 283 | throw err; 284 | } 285 | } 286 | } 287 | } 288 | 289 | static async validCollection(field, dataField, modelName) { 290 | 291 | if (dataField) { 292 | let collectionParsed; 293 | try { 294 | if (typeof dataField === 'string') { 295 | collectionParsed = JSON.parse(dataField); 296 | } 297 | else { 298 | collectionParsed = dataField; 299 | } 300 | } catch (e) { 301 | throw new Exceptions.BadRequest(`Value in field '${field}' must be an array of references`); 302 | } 303 | if (Array.isArray(collectionParsed)) { 304 | let ModelClass = ModelRegister.model(modelName); 305 | 306 | for (let reference of collectionParsed) { 307 | 308 | if (typeof reference === 'string') { 309 | try { 310 | var obj = await ModelClass.fetchOne({id: reference}) 311 | } 312 | catch (err) { 313 | if (err instanceof Exceptions.NotFound) { 314 | throw new Exceptions.BadRequest(` Value '${reference}' referenced in field '${field}' not found`); 315 | } 316 | else { 317 | throw err; 318 | } 319 | } 320 | } 321 | else if (typeof reference !== 'object') { 322 | throw new Exceptions.BadRequest(`Value in collection field must be a reference or an object, received ${reference}`); 323 | 324 | } 325 | } 326 | return collectionParsed; 327 | } 328 | else { 329 | throw new Exceptions.BadRequest(`Value in collection field must be an array, received ${dataField}`); 330 | } 331 | } 332 | } 333 | 334 | static async validManyToMany(field, dataField, modelName) { 335 | if (dataField) { 336 | let collectionParsed; 337 | try { 338 | if (typeof dataField === 'string') { 339 | collectionParsed = JSON.parse(dataField); 340 | } 341 | else { 342 | collectionParsed = dataField; 343 | } 344 | } catch (e) { 345 | throw new Exceptions.BadRequest(`Value in field '${field}' must be a json array of references`); 346 | } 347 | if (Array.isArray(collectionParsed)) { 348 | let ModelClass = ModelRegister.model(modelName); 349 | for (let i in collectionParsed) { 350 | let reference = collectionParsed[i]; 351 | 352 | if (typeof reference === 'string') { 353 | try { 354 | var obj = await ModelClass.fetchOne({id: reference}) 355 | } 356 | catch (err) { 357 | if (err instanceof Exceptions.NotFound) { 358 | throw new Exceptions.BadRequest(` Value '${reference}' referenced in field '${field}' not found`); 359 | } 360 | else { 361 | throw err; 362 | } 363 | } 364 | } 365 | } 366 | return collectionParsed; 367 | } 368 | else { 369 | throw new Exceptions.BadRequest(`Value in manyToMany field must be an array, received ${dataField}`); 370 | } 371 | } 372 | } 373 | } 374 | 375 | export default Input; 376 | 377 | const allowedTypes = ["string", "number", "date", "boolean", "reference", "collection", "manyToMany"]; 378 | const allowedRelations = ["reference", "collection", "manyToMany"]; 379 | 380 | -------------------------------------------------------------------------------- /apey-eye/Model.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by GlazedSolutions on 11/03/2015. 3 | */ 4 | 5 | import _ from 'underscore'; 6 | import async from 'async'; 7 | import bluebird from 'bluebird'; 8 | import ModelRegister from './ModelRegister.js'; 9 | import BaseClass from './BaseClass.js'; 10 | 11 | let asyncEach = bluebird.promisify(async.each); 12 | 13 | class Model extends BaseClass { 14 | 15 | static _serialize(id, data) { 16 | var ModelClass = this; 17 | 18 | let obj = { 19 | id: id, 20 | obj: data, 21 | constructor: ModelClass, 22 | oldObj: _.clone(data), 23 | valid(method) { 24 | return ModelClass.valid(this.obj, method); 25 | }, 26 | save(options) { 27 | var method = ModelClass.prototype.save.bind(this); 28 | return method(options); 29 | }, 30 | post(options){ 31 | var method = ModelClass.prototype.put.bind(this); 32 | return method(options); 33 | }, 34 | put(options) { 35 | var method = ModelClass.prototype.put.bind(this); 36 | return method(options); 37 | }, 38 | patch(options){ 39 | var method = ModelClass.prototype.patch.bind(this); 40 | return method(options); 41 | }, 42 | delete (){ 43 | var method = ModelClass.prototype.delete.bind(this); 44 | return method(); 45 | } 46 | }; 47 | if(ModelClass.actions){ 48 | Object.keys(ModelClass.actions.instance).forEach(action => { 49 | obj[action] = async () => { 50 | return ModelClass.prototype[action].apply(obj,arguments); 51 | }; 52 | }); 53 | } 54 | 55 | 56 | return obj; 57 | } 58 | 59 | static async _serializeArray(listObj, properties) { 60 | let ModelClass = this, 61 | serializedArray = []; 62 | 63 | await asyncEach(listObj, function (item, callback) { 64 | item = ModelClass._serialize(item.id, item); 65 | item = ModelClass.processOutput(item, properties) 66 | .then(item => { 67 | serializedArray.push(item); 68 | callback(); 69 | }); 70 | }); 71 | 72 | Object.defineProperty(serializedArray, 'obj', {value: listObj, enumerable: false}); 73 | return serializedArray; 74 | 75 | } 76 | 77 | async save(properties) { 78 | if (await this.valid()) { 79 | return await this.put({data: this.obj, resourceProperties: properties}) 80 | } 81 | } 82 | 83 | static async processRelatedData(serializedObj, data, update) { 84 | let self = this, 85 | input = this.getInput(); 86 | 87 | if (!data || !input) { 88 | return serializedObj; 89 | } 90 | await asyncEach(Object.keys(input.properties), function (field, callback) { 91 | self._processRelatedDataAux(input, data, ModelRegister, serializedObj, update, field) 92 | .then(item => { 93 | callback(); 94 | }); 95 | }); 96 | return serializedObj; 97 | } 98 | 99 | static async _processRelatedDataAux(input, data, ModelRegister, serializedObj, update, field) { 100 | let self = this, 101 | property = input.properties[field]; 102 | 103 | try { 104 | data[field] = JSON.parse(data[field]); 105 | } catch (e) { 106 | } 107 | 108 | if (property.type === 'reference') { 109 | if (typeof data[field] === 'object') { 110 | let RelatedModelClass = ModelRegister.model(property.model), 111 | embeddedObj = await new RelatedModelClass({data: data[field]}), 112 | _data = {}; 113 | 114 | _data[field] = embeddedObj.id; 115 | await serializedObj.patch({data: _data}); 116 | } 117 | } 118 | else if (property.type === 'collection') { 119 | let RelatedModelClass = ModelRegister.model(property.model); 120 | 121 | if (data[field]) { 122 | if (update) { 123 | let _filter = {}; 124 | _filter[property.inverse] = serializedObj.id; 125 | 126 | let relatedList = await RelatedModelClass.fetch({ 127 | resourceProperties: { 128 | _filter: _filter 129 | } 130 | }); 131 | 132 | for (let relatedObj of relatedList) { 133 | if (data[field].indexOf(relatedObj.id) === -1) { 134 | relatedObj.delete(); 135 | } 136 | } 137 | } 138 | 139 | await asyncEach(data[field], function (related, callback) { 140 | self.processCollectionData(related, property, serializedObj, RelatedModelClass) 141 | .then(item => { 142 | callback(); 143 | }); 144 | }); 145 | } 146 | } 147 | else if (property.type === 'manyToMany') { 148 | if (data[field]) { 149 | let ThroughModelClass = ModelRegister.model(property.through); 150 | 151 | if (!ThroughModelClass) { 152 | throw new Error(`BaseClass: through model '${property.through}' class for field '${field}' not exists.`); 153 | } 154 | else { 155 | let throughInput = ThroughModelClass.getInput(); 156 | 157 | if (throughInput) { 158 | 159 | let sourceModelName = this.getName(); 160 | let sourceField = _.findKey(throughInput.properties, function (obj) { 161 | return obj.model === sourceModelName; 162 | }); 163 | let targetField = _.findKey(throughInput.properties, function (obj) { 164 | return obj.model === property.model; 165 | }); 166 | 167 | if (update) { 168 | let _filter = {}; 169 | _filter[sourceField] = serializedObj.id; 170 | let relationList = await ThroughModelClass.fetch({ 171 | resourceProperties: { 172 | _filter: _filter 173 | } 174 | }); 175 | for (let elem of relationList) { 176 | if (data[field].indexOf(elem[targetField]) === -1) { 177 | elem.delete(); 178 | } 179 | } 180 | } 181 | 182 | if (data[field]) { 183 | await asyncEach(data[field], function (target, callback) { 184 | self.PostManyManyData(target, ModelRegister, property, targetField, sourceField, serializedObj, ThroughModelClass) 185 | .then(item => { 186 | callback(); 187 | }); 188 | }); 189 | } 190 | } 191 | } 192 | } 193 | 194 | } 195 | } 196 | 197 | static async processCollectionData(related, property, serializedObj, RelatedModelClass) { 198 | if (typeof related === 'object') { 199 | related[property.inverse] = serializedObj.id; 200 | await new RelatedModelClass({data: related}); 201 | } 202 | else if (typeof related === 'string') { 203 | 204 | let relatedObj = await RelatedModelClass.fetchOne({id: related}); 205 | if (relatedObj[property.inverse] != serializedObj.id) { 206 | let updateData = {}; 207 | updateData[property.inverse] = serializedObj.id; 208 | await relatedObj.patch({data: updateData}); 209 | } 210 | } 211 | } 212 | 213 | static async PostManyManyData(target, ModelRegister, property, targetField, sourceField, serializedObj, ThroughModelClass) { 214 | if (typeof target === 'object') { 215 | let TargetModelClass = ModelRegister.model(property.model); 216 | var targetObj = await new TargetModelClass({data: target}); 217 | target = targetObj.id; 218 | 219 | let newData = {}; 220 | newData[targetField] = target; 221 | newData[sourceField] = serializedObj.id; 222 | 223 | await new ThroughModelClass({data: newData}); 224 | } 225 | } 226 | } 227 | 228 | export default Model; -------------------------------------------------------------------------------- /apey-eye/ModelRegister.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by GlazedSolutions on 10/04/2015. 3 | */ 4 | 'use strict'; 5 | 6 | let singleton = Symbol(); 7 | let singletonEnforcer = Symbol(); 8 | 9 | class ModelRegister { 10 | constructor(enforcer) { 11 | if (enforcer !== singletonEnforcer) { 12 | throw "Cannot construct ModelRegister"; 13 | } 14 | this.models = {}; 15 | } 16 | static instance() { 17 | if (!this[singleton]) { 18 | this[singleton] = new ModelRegister(singletonEnforcer); 19 | } 20 | return this[singleton]; 21 | } 22 | register(modelName, ModelClass){ 23 | let Model = require('./Model'); 24 | 25 | if(!(ModelClass.prototype instanceof Model)){ 26 | throw new Error(`ModelRegister: ${ModelClass.name} class must be subclass of Model class`); 27 | } 28 | 29 | if(this.models[modelName] === undefined){ 30 | this.models[modelName] = ModelClass; 31 | } 32 | else{ 33 | throw new Error(`ModelRegister: already exists a model with name '${modelName}'`); 34 | } 35 | } 36 | model(modelName){ 37 | return this.models[modelName]; 38 | } 39 | empty(){ 40 | this.models = {}; 41 | } 42 | 43 | } 44 | export default ModelRegister.instance(); 45 | 46 | -------------------------------------------------------------------------------- /apey-eye/Resource.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by GlazedSolutions on 02/03/2015. 3 | */ 4 | 5 | import BaseClass from './BaseClass'; 6 | import * as Decorators from './Decorators'; 7 | import * as Exceptions from './Exceptions.js'; 8 | import * as DefaultProperties from './DefaultProperties.js'; 9 | import * as Formatters from './Formatters.js'; 10 | import RethinkDBModel from './RethinkDBModel'; 11 | import _ from 'underscore'; 12 | import ModelRegister from './ModelRegister.js'; 13 | 14 | class Resource extends BaseClass { 15 | static HTTPResourceMethods = { 16 | collection: { 17 | post: {method: "constructor", static: true}, 18 | get: {method: "fetch", static: true}, 19 | put: {method: "put", static: true}, 20 | patch: {method: "patch", static: true}, 21 | delete: {method: "delete", static: true}, 22 | options: {method: "options", static: true} 23 | }, 24 | instance: { 25 | post: {method: "post"}, 26 | get: {method: "fetchOne", static: true}, 27 | put: {method: "put"}, 28 | patch: {method: "patch"}, 29 | delete: {method: "delete"}, 30 | options: {method: "options"} 31 | } 32 | }; 33 | 34 | static options() { 35 | 36 | let ResourceClass = this, 37 | options = ResourceClass._options('collection'); 38 | 39 | return ResourceClass._serialize(undefined, options); 40 | } 41 | 42 | options() { 43 | 44 | let ResourceClass = this.constructor, 45 | options = ResourceClass._options('instance'); 46 | 47 | return ResourceClass._serialize(undefined, options); 48 | } 49 | 50 | static _options(pathType) { 51 | let ResourceClass = this, 52 | httpMethods = DefaultProperties.HTTPMethods, 53 | options = { 54 | collection: { 55 | query: ResourceClass.getQuery(), 56 | output: ResourceClass.getOutput(), 57 | input: ResourceClass.getInput(), 58 | allowed_roles: ResourceClass.getAllowedRoles(), 59 | auth: ResourceClass.getAuthentication(), 60 | documentation: ResourceClass.getDocumentation(), 61 | formatters: (ResourceClass.getFormat() && [ResourceClass.getFormat().getMediaType()]) || _.reduce(Formatters.FormattersList, function (memo, elem) { 62 | return memo.concat(elem.getMediaType()) 63 | }, []) 64 | }, 65 | methods: [], 66 | actions: {} 67 | }; 68 | 69 | httpMethods.forEach(httpMethod => { 70 | let resourceMethod = ResourceClass.getResourceMethod(pathType, httpMethod); 71 | if (resourceMethod) { 72 | 73 | let resourceMethodProperties = Resource.HTTPResourceMethods[pathType][httpMethod.toLowerCase()]; 74 | try{ 75 | if (Resource.allowedMethod.call(this, resourceMethodProperties)) { 76 | let methodOptions = { 77 | http_method: httpMethod.toUpperCase(), 78 | query: ResourceClass.getQuery.call(resourceMethod), 79 | output: ResourceClass.getOutput.call(resourceMethod), 80 | input: ResourceClass.getInput.call(resourceMethod), 81 | allowed_roles: ResourceClass.getAllowedRoles.call(resourceMethod), 82 | auth: ResourceClass.getAuthentication.call(resourceMethod), 83 | documentation: ResourceClass.getDocumentation.call(resourceMethod), 84 | formatters: ResourceClass.getFormat.call(resourceMethod) && [ResourceClass.getFormat.call(resourceMethod).getMediaType()] 85 | }; 86 | options.methods.push(methodOptions); 87 | } 88 | }catch(e){} 89 | } 90 | }); 91 | if(ResourceClass.actions) { 92 | 93 | Object.keys(ResourceClass.actions[pathType]).forEach(action => { 94 | 95 | let i = action.indexOf("_"); 96 | let method = action.substr(0, i); 97 | let actionName = action.substr(i + 1); 98 | 99 | if (method && actionName) { 100 | options.actions[actionName] = { 101 | http_method: method.toUpperCase(), 102 | path: `/${actionName}` 103 | }; 104 | } 105 | }); 106 | } 107 | return options; 108 | } 109 | 110 | render(requestProperties = {}) { 111 | 112 | var ResourceClass = this.constructor, 113 | FormatNegotiator = require('./FormatNegotiator'), 114 | ResourceFormatter = ResourceClass.getFormat(), 115 | FormatClass; 116 | 117 | if (ResourceFormatter) { 118 | FormatClass = ResourceFormatter; 119 | } 120 | else { 121 | FormatClass = FormatNegotiator.selectFormatter(requestProperties) 122 | } 123 | 124 | return { 125 | type: FormatClass.getMediaType(), 126 | data: FormatClass.format(this.obj) 127 | } 128 | } 129 | 130 | static _serialize(id, data) { 131 | let ResourceClass = this; 132 | let obj = { 133 | id: id, 134 | obj: data, 135 | constructor: ResourceClass, 136 | put(options) { 137 | let method = ResourceClass.prototype.put.bind(this); 138 | return method(options); 139 | }, 140 | patch(options){ 141 | let method = ResourceClass.prototype.patch.bind(this); 142 | return method(options); 143 | }, 144 | post(options){ 145 | let method = ResourceClass.prototype.post.bind(this); 146 | return method(options); 147 | }, 148 | options(options){ 149 | let method = ResourceClass.prototype.options.bind(this); 150 | return method(options); 151 | }, 152 | delete(){ 153 | let method = ResourceClass.prototype.delete.bind(this); 154 | return method(); 155 | }, 156 | render(){ 157 | let method = ResourceClass.prototype.render.bind(this); 158 | return method(); 159 | } 160 | }; 161 | if(ResourceClass.actions){ 162 | Object.keys(ResourceClass.actions.instance).forEach(action => { 163 | obj[action] = async () => { 164 | return ResourceClass.prototype[action].apply(obj, arguments); 165 | }; 166 | }); 167 | } 168 | 169 | return obj; 170 | } 171 | 172 | static _serializeArray(listObj) { 173 | let ResourceClass = this; 174 | 175 | let serializedArray = []; 176 | listObj.forEach(item=> { 177 | item = ResourceClass._serialize(item.id, item.obj); 178 | //item.obj = ResourceClass.selectFields(item.obj, properties._fields); 179 | serializedArray.push(item); 180 | }); 181 | 182 | let renderMethod = ResourceClass.prototype.render.bind(serializedArray); 183 | let listData = _.reduce(listObj, function (memo, elem) { 184 | return memo.concat(elem.obj) 185 | }, []); 186 | 187 | Object.defineProperty(serializedArray, 'obj', {value: listData, enumerable: false}); 188 | Object.defineProperty(serializedArray, 'render', {value: renderMethod, enumerable: false}); 189 | Object.defineProperty(serializedArray, 'constructor', {value: ResourceClass, enumerable: false}); 190 | 191 | return serializedArray; 192 | } 193 | 194 | static getMethods() { 195 | return Decorators.getProperty(this, "methods"); 196 | } 197 | 198 | static getFormat() { 199 | return Decorators.getProperty(this, "format"); 200 | } 201 | 202 | static getAuthentication(method) { 203 | return Decorators.getProperty(this, "authentication", method); 204 | } 205 | 206 | static getAllowedRoles(method) { 207 | return Decorators.getProperty(this, "roles", method); 208 | } 209 | 210 | static getModel(method) { 211 | return Decorators.getProperty(this, "model", method); 212 | } 213 | static getDocumentation(method) { 214 | return Decorators.getProperty(this, "documentation", method); 215 | } 216 | 217 | static _getActionMethod(options) { 218 | let ResourceClass = this; 219 | if(ResourceClass.actions){ 220 | if (options.pathType === 'instance_action') { 221 | let actionMethod = `${options.method}_${options.action}`; 222 | 223 | for (let key in ResourceClass.actions.instance) { 224 | if (typeof ResourceClass.prototype[key] === "function") { 225 | if (key.toLowerCase() === actionMethod.toLowerCase()) { 226 | return key; 227 | } 228 | } 229 | } 230 | throw new Exceptions.NotFound(options.action); 231 | } 232 | else if (options.pathType === 'instance') { 233 | 234 | let actionMethod = `${options.method}_${options.id}`; 235 | 236 | for (let key in ResourceClass.actions.collection) { 237 | if (typeof ResourceClass[key] === "function") { 238 | if (key.toLowerCase() === actionMethod.toLowerCase()) { 239 | options.action = options.id; 240 | delete options.id; 241 | return key; 242 | } 243 | } 244 | } 245 | } 246 | return false; 247 | } 248 | else{ 249 | return false; 250 | } 251 | } 252 | 253 | static getResourceMethod(options) { 254 | 255 | let ResourceClass = this, 256 | actionMethod = ResourceClass._getActionMethod(options); 257 | if (actionMethod) { 258 | if (options.pathType === 'instance_action') { 259 | return ResourceClass.prototype[actionMethod]; 260 | } 261 | else if (options.pathType === 'instance') { 262 | return ResourceClass[actionMethod]; 263 | } 264 | } 265 | 266 | var methodProperties = Resource.HTTPResourceMethods[options.pathType][options.method.toLowerCase()], 267 | resourceMethod; 268 | if (methodProperties) { 269 | if (methodProperties.static) { 270 | if (methodProperties.method === 'constructor') { 271 | resourceMethod = this; 272 | } 273 | else { 274 | resourceMethod = this[methodProperties.method]; 275 | } 276 | } 277 | else { 278 | resourceMethod = this.prototype[methodProperties.method]; 279 | } 280 | } 281 | return resourceMethod; 282 | } 283 | 284 | static allowedMethod(resourceMethodProperties) { 285 | let allowedMethods = this.getMethods(); 286 | if(resourceMethodProperties.method === 'options'){ 287 | return true; 288 | } 289 | else { 290 | if (resourceMethodProperties) { 291 | let methodDescriptor; 292 | if (resourceMethodProperties.method === 'constructor') { 293 | methodDescriptor = resourceMethodProperties.method; 294 | } 295 | else { 296 | methodDescriptor = ""; 297 | if (resourceMethodProperties.static) { 298 | methodDescriptor = methodDescriptor.concat("static."); 299 | } 300 | methodDescriptor = methodDescriptor.concat(resourceMethodProperties.method); 301 | } 302 | 303 | if (!allowedMethods || (allowedMethods && allowedMethods.indexOf(methodDescriptor) > -1)) { 304 | if (resourceMethodProperties.static) { 305 | if (resourceMethodProperties.method === 'constructor') { 306 | return true; 307 | } 308 | else { 309 | if (this[resourceMethodProperties.method]) { 310 | return true; 311 | } 312 | } 313 | } 314 | else { 315 | if (this.prototype[resourceMethodProperties.method]) { 316 | return true; 317 | } 318 | } 319 | throw new Exceptions.NotImplemented(); 320 | } 321 | else { 322 | throw new Exceptions.MethodNotAllowed(); 323 | } 324 | } 325 | else { 326 | throw new Exceptions.MethodNotAllowed(); 327 | } 328 | } 329 | } 330 | 331 | static async _handleRequest(options) { 332 | let ResourceClass = this, 333 | actionMethod = ResourceClass._getActionMethod(options); 334 | 335 | if (actionMethod) { 336 | if (options.pathType === 'instance_action') { 337 | let obj = await ResourceClass.fetchOne({id: options.id}); 338 | return obj[actionMethod]({data: options.data, requestProperties: options.requestProperties}); 339 | } 340 | else if (options.pathType === 'instance') { 341 | return ResourceClass[actionMethod]({data: options.data, requestProperties: options.requestProperties}); 342 | } 343 | } 344 | 345 | let resourceMethodProperties = Resource.HTTPResourceMethods[options.pathType][options.method.toLowerCase()]; 346 | if (this.allowedMethod(resourceMethodProperties)) { 347 | 348 | if (resourceMethodProperties.static) { 349 | if (resourceMethodProperties.method === 'constructor') { 350 | return new ResourceClass({data: options.data, requestProperties: options.requestProperties}); 351 | } 352 | else { 353 | if (ResourceClass[resourceMethodProperties.method]) { 354 | return ResourceClass[resourceMethodProperties.method]({ 355 | id: options.id, 356 | requestProperties: options.requestProperties 357 | }); 358 | } 359 | } 360 | } 361 | else { 362 | if (ResourceClass.prototype[resourceMethodProperties.method]) { 363 | let obj = await ResourceClass.fetchOne({id: options.id}); 364 | return obj[resourceMethodProperties.method]({ 365 | data: options.data, 366 | requestProperties: options.requestProperties 367 | }); 368 | } 369 | } 370 | throw new Exceptions.NotImplemented(); 371 | } 372 | else { 373 | throw new Exceptions.MethodNotAllowed(); 374 | } 375 | } 376 | 377 | static checkModel(method) { 378 | let ResourceClass = this; 379 | let ModelClass = ResourceClass.getModel(method); 380 | 381 | if (!ModelClass) { 382 | let resourceName = ResourceClass.getName(); 383 | if (resourceName) { 384 | ResourceClass._appendNewModel(resourceName, ResourceClass.noBackend); 385 | } 386 | else { 387 | throw new Error("GenericResource: Resource must have a name to allow automatic creation of a Model class."); 388 | } 389 | } 390 | } 391 | 392 | static _appendNewModel(resourceName, noBackend) { 393 | let ResourceClass = this; 394 | 395 | let ModelRegistered = ModelRegister.model(resourceName); 396 | if (ModelRegistered) { 397 | ResourceClass.annotations.model = ModelRegistered; 398 | } 399 | else { 400 | let input = ResourceClass.getInput(), 401 | generatedModel; 402 | 403 | if (input) { 404 | @Decorators.Name(resourceName) 405 | @Decorators.Input(input) 406 | class GeneratedModel extends RethinkDBModel { 407 | } 408 | 409 | generatedModel = GeneratedModel; 410 | } 411 | else { 412 | @Decorators.Name(resourceName) 413 | class GeneratedModel extends RethinkDBModel { 414 | } 415 | generatedModel = GeneratedModel; 416 | } 417 | 418 | generatedModel.noBackend = noBackend; 419 | 420 | ResourceClass.annotations.model = generatedModel; 421 | } 422 | 423 | 424 | } 425 | 426 | } 427 | export default Resource; -------------------------------------------------------------------------------- /apey-eye/RethinkDBAdapter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by GlazedSolutions on 05/03/2015. 3 | */ 4 | import rethink from 'rethinkdbdash'; 5 | import DatabaseConfig from './config/database.js'; 6 | 7 | var r = rethink({ 8 | pool: false 9 | }); 10 | 11 | 12 | class RethinkDBAdapter { 13 | static config = { 14 | host: DatabaseConfig.host, 15 | port: DatabaseConfig.port, 16 | db: DatabaseConfig.database, 17 | }; 18 | constructor(tableName) { 19 | this.tableName = tableName; 20 | } 21 | static async createDatabase() { 22 | 23 | try { 24 | let c = await r.connect(RethinkDBAdapter.config); 25 | await r.dbCreate(DatabaseConfig.database).run(c); 26 | } 27 | catch (err) { 28 | if (err.message.indexOf("already exists") == -1) { 29 | throw err; 30 | } 31 | } 32 | } 33 | static async checkTableExists(tableName) { 34 | 35 | await RethinkDBAdapter.createDatabase(); 36 | let c = await r.connect(RethinkDBAdapter.config); 37 | let list = await r.tableList().run(c); 38 | return list.indexOf(tableName) > -1; 39 | } 40 | 41 | static async createTable(tableName) { 42 | let c = await r.connect(RethinkDBAdapter.config); 43 | 44 | try { 45 | await r.tableCreate(tableName).run(c); 46 | } 47 | catch (err) { 48 | if (err.message.indexOf("already exists") == -1) { 49 | throw err; 50 | } 51 | } 52 | } 53 | 54 | async initializeQuery() { 55 | 56 | var table = this.tableName; 57 | 58 | try{ 59 | this.connection = await r.connect(RethinkDBAdapter.config); 60 | this.query = r.table(table); 61 | } 62 | catch(err){ 63 | throw new Error(`Database: Error creating a new connection to database. ${err.message}`); 64 | } 65 | } 66 | 67 | async runQuery() { 68 | return await this.query.run(this.connection) 69 | } 70 | 71 | async getCollection(properties = {}) { 72 | var self = this; 73 | await self.initializeQuery(); 74 | 75 | self.addQueryFilters(properties._filter); 76 | self.addQuerySort(properties._sort); 77 | self.addQueryPagination(properties._pagination); 78 | 79 | return await self.runQuery(); 80 | } 81 | 82 | async insertObject(data) { 83 | if (data) { 84 | var self = this; 85 | await self.initializeQuery(); 86 | self.addQueryInsert(data); 87 | var results = await self.runQuery(); 88 | 89 | if (results.inserted > 0) { 90 | return await results.changes[results.changes.length - 1].new_val; 91 | } 92 | else { 93 | if (results.errors > 0) { 94 | console.error(results.first_error) 95 | } 96 | throw new Error(`Database: object not inserted.`); 97 | } 98 | } 99 | else { 100 | throw new Error(`Database: undefined data received`); 101 | } 102 | } 103 | 104 | ; 105 | async getObject(id) { 106 | if (id) { 107 | var self = this; 108 | await self.initializeQuery(); 109 | self.addQueryGetData(id); 110 | return await self.runQuery(); 111 | } 112 | else { 113 | throw new Error(`Database: undefined id received`); 114 | } 115 | } 116 | 117 | async replaceObject(id, data) { 118 | if (id) { 119 | var self = this; 120 | await self.initializeQuery(); 121 | data.id = id; 122 | self.addQueryGetData(id); 123 | self.addQueryReplace(data); 124 | await self.runQuery(); 125 | return await self.getObject(id); 126 | } 127 | else { 128 | throw new Error(`Database: undefined id received`); 129 | } 130 | } 131 | 132 | async updateObject(id, data) { 133 | if (id) { 134 | var self = this; 135 | await self.initializeQuery(); 136 | data.id = id; 137 | self.addQueryGetData(id); 138 | self.addQueryUpdate(data); 139 | await self.runQuery(); 140 | return await self.getObject(id); 141 | } 142 | else { 143 | throw new Error(`Database: undefined id received`); 144 | } 145 | } 146 | 147 | async deleteObject(id) { 148 | if (id) { 149 | var self = this; 150 | await self.initializeQuery(); 151 | self.addQueryGetData(id); 152 | self.addQueryDelete(); 153 | let obj = await self.runQuery(); 154 | 155 | if (obj.deleted === 1) { 156 | return true; 157 | } 158 | else { 159 | throw new Error("RethinkDBAdapter: error deleting object from database."); 160 | } 161 | } 162 | else { 163 | throw new Error(`Database: undefined id received`); 164 | } 165 | } 166 | 167 | addQueryFilters(filters) { 168 | if (filters) { 169 | this.query = this.query.filter(filters); 170 | } 171 | } 172 | 173 | addQuerySort(sort) { 174 | if (sort) { 175 | let sortArray = []; 176 | sort.forEach(s => { 177 | let sortField = Object.keys(s)[0]; 178 | let sortOrder = s[sortField]; 179 | 180 | if (sortOrder === 1) { 181 | sortArray.push(r.asc(sortField)) 182 | } 183 | else if (sortOrder === -1) { 184 | sortArray.push(r.desc(sortField)) 185 | } 186 | }); 187 | 188 | this.query = this.query.orderBy.apply(this.query, sortArray); 189 | } 190 | } 191 | 192 | addQueryPagination(pagination) { 193 | 194 | if (pagination) { 195 | var page = pagination._page || 1; 196 | var pageSize = pagination._page_size; 197 | } 198 | 199 | if (page && pageSize) { 200 | let lowerBound = (page - 1) * pageSize; 201 | let upBound = page * pageSize; 202 | this.query = this.query.slice(lowerBound, upBound); 203 | 204 | } 205 | } 206 | 207 | addQueryFields(fields) { 208 | 209 | if (fields) { 210 | this.query = this.query.pluck.apply(this.query, fields); 211 | } 212 | } 213 | 214 | addQueryInsert(data) { 215 | if (data) { 216 | this.query = this.query.insert(data, {returnChanges: true}); 217 | } 218 | } 219 | 220 | addQueryGetData(id) { 221 | if (id) { 222 | this.query = this.query.get(id); 223 | } 224 | } 225 | 226 | addQueryReplace(data) { 227 | if (data) { 228 | this.query = this.query.replace(data); 229 | } 230 | } 231 | 232 | addQueryUpdate(data) { 233 | if (data) { 234 | this.query = this.query.update(data); 235 | } 236 | } 237 | 238 | addQueryDelete() { 239 | this.query = this.query.delete(); 240 | } 241 | } 242 | 243 | export default RethinkDBAdapter; 244 | 245 | 246 | 247 | -------------------------------------------------------------------------------- /apey-eye/RethinkDBModel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by GlazedSolutions on 03/03/2015. 3 | */ 4 | 5 | 6 | import Model from './Model'; 7 | import RethinkDBAdapter from './RethinkDBAdapter'; 8 | import * as Exceptions from './Exceptions'; 9 | import _ from "underscore"; 10 | 11 | class RethinkDBModel extends Model { 12 | constructor(options = {}) { 13 | super(); 14 | super(async () => { 15 | let ModelClass = this.constructor; 16 | 17 | await ModelClass._checkDataTable(true); 18 | await ModelClass.valid(options.data); 19 | 20 | let tableName = ModelClass.getName(), 21 | properties = ModelClass.joinProperties(options.resourceProperties, ModelClass.post), 22 | db = new RethinkDBAdapter(tableName); 23 | 24 | let obj = await db.insertObject(options.data, properties); 25 | 26 | obj = ModelClass._serialize(obj.id, obj); 27 | await ModelClass.processRelatedData(obj, options.data); 28 | await ModelClass.processOutput(obj, properties); 29 | return obj; 30 | }); 31 | } 32 | 33 | static async fetch(options = {}) { 34 | 35 | let ModelClass = this, 36 | tableName = ModelClass.getName(ModelClass.fetch), 37 | properties = ModelClass.joinProperties(options.resourceProperties, ModelClass.fetch), 38 | db = new RethinkDBAdapter(tableName); 39 | 40 | await ModelClass._checkDataTable(false); 41 | let list = await db.getCollection(properties); 42 | return await ModelClass._serializeArray(list, properties); 43 | } 44 | 45 | static async fetchOne(options = {}) { 46 | let ModelClass = this, 47 | tableName = ModelClass.getName(ModelClass.fetchOne), 48 | properties = ModelClass.joinProperties(options.resourceProperties, ModelClass.fetchOne), 49 | db = new RethinkDBAdapter(tableName); 50 | await ModelClass._checkDataTable(false); 51 | let obj = await db.getObject(options.id, properties); 52 | if (!obj) { 53 | throw new Exceptions.NotFound(options.id); 54 | } 55 | else { 56 | obj = ModelClass._serialize(options.id, obj); 57 | await ModelClass.processOutput(obj, properties); 58 | 59 | return obj; 60 | } 61 | } 62 | 63 | async put(options = {}) { 64 | let ModelClass = this.constructor, 65 | tableName = ModelClass.getName(ModelClass.prototype.put), 66 | properties = ModelClass.joinProperties(options.resourceProperties, ModelClass.prototype.put), 67 | db = new RethinkDBAdapter(tableName); 68 | 69 | await ModelClass.valid(options.data); 70 | 71 | let newObj = await db.replaceObject(this.id, options.data, properties); 72 | 73 | this.oldObj = _.clone(newObj); 74 | this.obj = newObj; 75 | 76 | await ModelClass.processRelatedData(this, options.data, true); 77 | await ModelClass.processOutput(this, properties); 78 | 79 | return this; 80 | 81 | } 82 | 83 | async patch(options = {}) { 84 | let ModelClass = this.constructor, 85 | tableName = ModelClass.getName(ModelClass.prototype.patch), 86 | properties = ModelClass.joinProperties(options.resourceProperties, ModelClass.prototype.patch), 87 | db = new RethinkDBAdapter(tableName); 88 | 89 | 90 | let obj = await db.getObject(this.id, properties); 91 | if (!obj) { 92 | throw new Exceptions.NotFound(this.id); 93 | } 94 | else { 95 | 96 | let futureData = _.extend(obj, options.data); 97 | 98 | await ModelClass.valid(futureData); 99 | let newObj = await db.updateObject(this.id, options.data, properties); 100 | 101 | this.oldObj = _.clone(this.obj); 102 | this.obj = newObj; 103 | await ModelClass.processRelatedData(this, options.data, true); 104 | await ModelClass.processOutput(this, properties); 105 | 106 | return this; 107 | } 108 | } 109 | async delete() { 110 | let ModelClass = this.constructor, 111 | tableName = ModelClass.getName(ModelClass.prototype.patch), 112 | db = new RethinkDBAdapter(tableName); 113 | 114 | return await db.deleteObject(this.id); 115 | } 116 | 117 | static async _checkDataTable(create) { 118 | let ModelClass = this; 119 | 120 | if (!ModelClass.noBackend && !ModelClass.tableCreated) { 121 | let tableCreated = await RethinkDBAdapter.checkTableExists(this.getName()); 122 | if (!tableCreated) { 123 | await RethinkDBAdapter.createTable(this.getName()); 124 | } 125 | ModelClass.tableCreated = true; 126 | } 127 | else if (ModelClass.noBackend && !ModelClass.tableCreated && create) { 128 | let tableCreated = await RethinkDBAdapter.checkTableExists(this.getName()); 129 | if (!tableCreated) { 130 | await RethinkDBAdapter.createTable(this.getName()); 131 | } 132 | ModelClass.tableCreated = true; 133 | } 134 | else if (!ModelClass.tableCreated && !create && ModelClass.noBackend) { 135 | let tableCreated = await RethinkDBAdapter.checkTableExists(this.getName()); 136 | if (!tableCreated) { 137 | throw new Exceptions.NotFound(); 138 | } 139 | } 140 | } 141 | } 142 | 143 | export default RethinkDBModel; 144 | -------------------------------------------------------------------------------- /apey-eye/bluebird-extended.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Used from https://github.com/rektide/laissez-bird 3 | */ 4 | 5 | import Bluebird from 'bluebird'; 6 | import util from 'util'; 7 | 8 | export default Promise; 9 | 10 | function Promise(factory) { 11 | if (!factory) 12 | return false; 13 | if (!(this instanceof Promise)) 14 | return new Promise(factory) 15 | 16 | this.factory = factory 17 | var defer = this.defer = Bluebird.defer() 18 | 19 | // copy pasted from Bluebird because Bluebird is fascist miserable adamntitely awful software which throws an error if you 20 | // try to innovate with it in the slightest. what the fuck bluebird? who died and made you the only permissible promise implementation? 21 | // https://github.com/petkaantonov/bluebird/blob/3e3a96aaa8586b0b6aa3b7ca432c03f58c295613/src/promise.js#L51 22 | this._bitField = 0; 23 | this._fulfillmentHandler0 = void 0; 24 | this._rejectionHandler0 = void 0; 25 | this._promise0 = void 0; 26 | this._receiver0 = void 0; 27 | this._settledValue = void 0; 28 | this._boundTo = void 0; 29 | this._resolveFromResolver(function (resolve, reject) { 30 | defer.promise.then(function (val) { 31 | resolve(val) 32 | }, function (err) { 33 | reject(err) 34 | }) 35 | }) 36 | } 37 | util.inherits(Promise, Bluebird) 38 | 39 | Promise.prototype._then = (function _then(didFulfill, didReject, didProgress, received, internalDate) { 40 | if (this.factory) { 41 | // run the factory 42 | var underlying = this.factory() 43 | this.factory = null 44 | 45 | // set our own value 46 | if (underlying instanceof Error) // it's bad 47 | this.defer.reject(underlying) 48 | else // simple case 49 | this.defer.resolve(underlying) 50 | 51 | // call any thenevers 52 | for (var i = this.thenevers ? this.thenevers.length : 0; i > 0;) { 53 | this.defer.then.apply(this.defer, this.thenevers[--i]) 54 | } 55 | } 56 | return Bluebird.prototype._then.call(this, didFulfill, didReject, didProgress, received, internalDate) 57 | }) -------------------------------------------------------------------------------- /apey-eye/config/database.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by GlazedSolutions on 13/05/2015. 3 | */ 4 | export default { 5 | host: 'localhost', 6 | port: 28015, 7 | database: 'db1' 8 | }; -------------------------------------------------------------------------------- /apey-eye/config/router.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by GlazedSolutions on 12/05/2015. 3 | */ 4 | 5 | export default { 6 | //basePath: '/classes' 7 | }; -------------------------------------------------------------------------------- /apey-eye/config/server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by GlazedSolutions on 12/05/2015. 3 | */ 4 | 5 | export default { 6 | apiVersion: "1.0", 7 | documentationPath: '/api-docs', 8 | documentationEndpoint: '/docs' 9 | }; -------------------------------------------------------------------------------- /apey-eye/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by GlazedSolutions on 26/05/2015. 3 | */ 4 | import * as Decorators from './Decorators'; 5 | 6 | import Model from './Model.js'; 7 | import RethinkDBModel from './RethinkDBModel.js'; 8 | import RethinkDBAdapter from './RethinkDBAdapter.js'; 9 | 10 | import Resource from './Resource.js'; 11 | import GenericResource from './GenericResource.js'; 12 | 13 | import * as Formatters from './Formatters.js'; 14 | 15 | import Input from './Input.js'; 16 | 17 | import UserModel from './models/UserModel.js'; 18 | import RoleModel from './models/RoleModel.js'; 19 | 20 | import BaseRouter from './BaseRouter.js'; 21 | 22 | import KoaGenericRouter from './routers/KoaGenericRouter.js'; 23 | import KoaRouter from './routers/KoaRouter.js'; 24 | 25 | import HapiGenericRouter from './routers/HapiGenericRouter.js'; 26 | import HapiRouter from './routers/HapiRouter.js'; 27 | 28 | import RouterConfig from './config/router.js'; 29 | import DatabaseConfig from './config/database.js'; 30 | import ServerConfig from './config/server.js'; 31 | 32 | export default { 33 | Decorators: Decorators, 34 | Model: Model, 35 | RethinkDBModel: RethinkDBModel, 36 | RethinkDBAdapter: RethinkDBAdapter, 37 | Resource: Resource, 38 | GenericResource: GenericResource, 39 | Formatters : Formatters, 40 | Input : Input, 41 | UserModel: UserModel, 42 | RoleModel: RoleModel, 43 | BaseRouter: BaseRouter, 44 | KoaGenericRouter: KoaGenericRouter, 45 | KoaRouter: KoaRouter, 46 | HapiGenericRouter: HapiGenericRouter, 47 | HapiRouter: HapiRouter, 48 | RouterConfig : RouterConfig, 49 | DatabaseConfig : DatabaseConfig, 50 | ServerConfig : ServerConfig 51 | } 52 | 53 | -------------------------------------------------------------------------------- /apey-eye/models/RoleModel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by GlazedSolutions on 20/04/2015. 3 | */ 4 | import Database from './../RethinkDBAdapter.js' 5 | import RethinkDBModel from './../RethinkDBModel.js' 6 | import * as Decorators from './../Decorators.js'; 7 | import Input from './../Input.js'; 8 | 9 | let roleInput = new Input({ 10 | id: {type: 'string', required:true}, 11 | childRoles: {type: "collection", model: "role", inverse: "parentRole"}, 12 | parentRole: {type: "reference", model: "role"} 13 | }); 14 | 15 | @Decorators.Input(roleInput) 16 | @Decorators.Name('role') 17 | class RoleModel extends RethinkDBModel { 18 | } 19 | 20 | export default RoleModel; -------------------------------------------------------------------------------- /apey-eye/models/UserModel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by GlazedSolutions on 19/04/2015. 3 | */ 4 | import RethinkDBModel from './../RethinkDBModel.js' 5 | import * as Decorators from './../Decorators.js'; 6 | import Input from './../Input.js'; 7 | import RoleModel from './RoleModel.js'; 8 | 9 | let userInput = new Input({ 10 | username: {type: 'string'}, 11 | password: {type: 'string'}, 12 | role: {type:"reference", model:"role"} 13 | }); 14 | 15 | @Decorators.Input(userInput) 16 | @Decorators.Name('user') 17 | class UserModel extends RethinkDBModel { 18 | } 19 | 20 | export default UserModel; -------------------------------------------------------------------------------- /apey-eye/routers/HapiGenericRouter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by GlazedSolutions on 03/03/2015. 3 | */ 4 | import HapiRouter from './HapiRouter.js'; 5 | import * as Exceptions from './../Exceptions.js'; 6 | import Joi from 'hapi/node_modules/joi'; 7 | import RouterConfig from '../config/router.js'; 8 | import * as DefaultProperties from './../DefaultProperties.js'; 9 | 10 | import _ from 'underscore'; 11 | 12 | class HapiGenericRouter extends HapiRouter { 13 | constructor() { 14 | super(); 15 | this.appendGenericRouter(); 16 | } 17 | 18 | static pathTypes() { 19 | let basePath = RouterConfig.basePath || ''; 20 | 21 | if (basePath && basePath.slice(-1) === '/') { 22 | throw new Error("Base path wouldn't end without character '/'"); 23 | } 24 | 25 | return { 26 | collection: `${basePath}/{path}`, 27 | instance: `${basePath}/{path}/{id}`, 28 | instance_action: `${basePath}/{path}/{id}/{action}` 29 | }; 30 | } 31 | rootOptions(baseUri){ 32 | let options = super.rootOptions(baseUri); 33 | options.no_backend = `${baseUri}${RouterConfig.basePath}/{path}`; 34 | return options; 35 | } 36 | appendGenericRouter() { 37 | let self = this; 38 | 39 | let httpMethods = DefaultProperties.HTTPMethods; 40 | 41 | Object.keys(HapiGenericRouter.pathTypes()).forEach(pathType => { 42 | httpMethods.forEach(httpMethod => { 43 | 44 | var path = HapiGenericRouter.pathTypes()[pathType]; 45 | let route = { 46 | path: path, 47 | method: httpMethod, 48 | config: { 49 | handler: { 50 | async: async function (request, reply) { 51 | try { 52 | let path = request.path.slice(RouterConfig.basePath.length); 53 | let resourceName = HapiRouter.resourceName(path); 54 | let ResourceClass; 55 | if(self.entries[resourceName]){ 56 | throw new Exceptions.NotImplemented(); 57 | } 58 | if (resourceName) { 59 | ResourceClass = HapiRouter.createGenericResourceClass(resourceName); 60 | 61 | let oldListLength = self.routesList.length; 62 | 63 | self.register([{ 64 | path: `${resourceName}`, 65 | resource: ResourceClass 66 | }]); 67 | 68 | let newRoutesList = self.routesList.slice(oldListLength); 69 | request.server.route(newRoutesList); 70 | 71 | reply().redirect(request.url.path).rewritable(false).temporary(true); 72 | } 73 | else { 74 | throw new Exceptions.NotFound(); 75 | } 76 | } 77 | catch (error) { 78 | reply(HapiRouter.errorHandling(error)); 79 | } 80 | 81 | } 82 | } 83 | } 84 | }; 85 | this.addGenericRouteDocumentation({ 86 | pathType: pathType, 87 | httpMethod: httpMethod 88 | 89 | }, route); 90 | this.routesList.push(route); 91 | }); 92 | }); 93 | } 94 | 95 | addGenericRouteDocumentation(options, route) { 96 | let validate = {params: {}}; 97 | 98 | validate.params.path = Joi.string().required().description("resource name"); 99 | 100 | if (options.pathType === 'instance') { 101 | validate.params.id = Joi.string().required().description('instance ID or action name'); 102 | } 103 | else if (options.pathType === 'instance_action ') { 104 | validate.params.id = Joi.string().required().description('instance ID'); 105 | validate.params.action = Joi.string().required().description('action name'); 106 | } 107 | 108 | if (options.pathType === 'collection' && options.httpMethod.toUpperCase() === 'GET') { 109 | validate.query = { 110 | _sort: Joi.string(), 111 | _filter: Joi.string(), 112 | _page_size: Joi.string() 113 | } 114 | } 115 | 116 | if (['POST', 'PUT', 'PATCH'].indexOf(options.httpMethod) != -1) { 117 | validate.payload = Joi.object().description('Payload for request'); 118 | } 119 | 120 | if (options.pathType !== 'instance_action') { 121 | validate.query = validate.query || {}; 122 | validate.query._embedded = Joi.string(); 123 | validate.query._fields = Joi.string(); 124 | } 125 | 126 | route.config.description = "No Backend"; 127 | route.config.notes = "This route allows to add a new resource to API in runtime, if no match route exists a new one will be added to server."; 128 | route.config.tags = ['api', 'nobackend']; 129 | route.config.validate = validate; 130 | } 131 | 132 | } 133 | export default HapiGenericRouter; -------------------------------------------------------------------------------- /apey-eye/routers/HapiRouter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by GlazedSolutions on 20/04/2015. 3 | */ 4 | 5 | import BaseRouter from './../BaseRouter.js'; 6 | import Resource from './../Resource'; 7 | import HTTPCodes from './../HTTPCodes'; 8 | import Auth from './../Auth.js'; 9 | import * as Decorators from './../Decorators'; 10 | import * as Exceptions from './../Exceptions'; 11 | import * as DefaultProperties from './../DefaultProperties.js'; 12 | 13 | import Joi from 'hapi/node_modules/joi'; 14 | import Hapi from 'hapi'; 15 | import _ from 'underscore'; 16 | import Boom from 'Boom'; 17 | 18 | 19 | import ServerConfig from '../config/server.js'; 20 | import RouterConfig from '../config/router.js'; 21 | 22 | 23 | class HapiRouter extends BaseRouter { 24 | 25 | constructor() { 26 | super(); 27 | 28 | this.entries = {}; 29 | this.routesList = []; 30 | 31 | this.addRootRoute(); 32 | 33 | this.passport = require("passport"); 34 | 35 | } 36 | start(options, callback){ 37 | let self = this; 38 | let server = new Hapi.Server({ 39 | connections: { 40 | router: { 41 | stripTrailingSlash: true 42 | } 43 | } 44 | }); 45 | server.connection({ 46 | port: options.port 47 | }); 48 | 49 | let authenticationScheme = function () { 50 | return { 51 | authenticate: function (request, reply) { 52 | return self.checkAuthentication(request, reply); 53 | } 54 | }; 55 | }; 56 | 57 | server.auth.scheme('custom', authenticationScheme); 58 | server.auth.strategy('default', 'custom'); 59 | 60 | server.register([ 61 | require('hapi-async-handler'), 62 | { 63 | register: require('hapi-swagger'), 64 | options: { 65 | apiVersion: ServerConfig.apiVersion, 66 | documentationPath: (RouterConfig.basePath || '') + ServerConfig.documentationPath, 67 | endpoint: (RouterConfig.basePath || '') + ServerConfig.documentationEndpoint, 68 | pathPrefixSize: ((RouterConfig.basePath || '').match(/\//g) || []).length+1 69 | } 70 | } 71 | 72 | ], 73 | function (err) { 74 | if (err) { 75 | throw err; 76 | } 77 | server.route(self.routes()); 78 | server.start(function(err){ 79 | callback(err, server); 80 | }); 81 | }); 82 | } 83 | static pathTypes(path) { 84 | let basePath = RouterConfig.basePath || ''; 85 | 86 | if (basePath && basePath.slice(-1) === '/') { 87 | throw new Error("Base path wouldn't end without character '/'"); 88 | } 89 | 90 | return { 91 | collection: `${basePath}/${path}`, 92 | instance: `${basePath}/${path}/{id}`, 93 | instance_action: `${basePath}/${path}/{id}/{action}` 94 | }; 95 | } 96 | 97 | addRootRoute() { 98 | let self = this; 99 | let route = { 100 | path: RouterConfig.basePath || '/', 101 | method: 'OPTIONS', 102 | config: { 103 | handler: { 104 | async: async function (request, reply) { 105 | reply(self.rootOptions(request.server.info.uri)); 106 | } 107 | } 108 | } 109 | }; 110 | this.routesList.push(route); 111 | } 112 | 113 | routes() { 114 | return this.routesList; 115 | } 116 | 117 | 118 | appendBaseMethods(entry) { 119 | 120 | let ResourceClass = entry.resource, 121 | pathTypes = HapiRouter.pathTypes(entry.path); 122 | 123 | if (ResourceClass.actions) { 124 | Object.keys(ResourceClass.actions.collection).forEach(action => { 125 | 126 | let i = action.indexOf("_"); 127 | let method = action.substr(0, i); 128 | let actionName = action.substr(i + 1); 129 | if (method && actionName) { 130 | this.addRoute({ 131 | path: `${pathTypes.collection}/${actionName}`, 132 | httpMethod: method, 133 | ResourceClass: ResourceClass, 134 | pathType: 'instance', 135 | action: actionName 136 | }); 137 | } 138 | }); 139 | Object.keys(ResourceClass.actions.instance).forEach(action => { 140 | let i = action.indexOf("_"); 141 | let method = action.substr(0, i); 142 | let actionName = action.substr(i + 1); 143 | 144 | if (method && actionName) { 145 | this.addRoute({ 146 | path: `${pathTypes.instance}/${actionName}`, 147 | httpMethod: method, 148 | ResourceClass: ResourceClass, 149 | pathType: 'instance_action', 150 | action: actionName 151 | }); 152 | } 153 | }); 154 | } 155 | 156 | let httpMethods = DefaultProperties.HTTPMethods; 157 | Object.keys(pathTypes).forEach(pathType => { 158 | httpMethods.forEach(httpMethod => { 159 | 160 | let path = pathTypes[pathType]; 161 | 162 | if (pathType !== 'instance_action') { 163 | 164 | let resourceMethod = ResourceClass.getResourceMethod({ 165 | pathType: pathType, 166 | method : httpMethod 167 | }); 168 | 169 | if (resourceMethod) { 170 | this.addRoute({ 171 | path: path, 172 | httpMethod: httpMethod, 173 | ResourceClass: ResourceClass, 174 | pathType: pathType, 175 | resourceMethod: resourceMethod 176 | }); 177 | } 178 | } 179 | }); 180 | }); 181 | } 182 | 183 | addRoute(options) { 184 | let self = this; 185 | 186 | let route = { 187 | path: options.path, 188 | method: options.httpMethod, 189 | config: { 190 | pre: [{ 191 | method: function (req, reply) { 192 | if (options.pathType === 'instance_action') { 193 | req.params.action = options.action; 194 | } 195 | else if (options.pathType === 'instance' && options.action) { 196 | req.params.id = options.action; 197 | } 198 | return self.defaultMiddlewares(req, function (error) { 199 | if (error) { 200 | reply(HapiRouter.errorHandling(error)).takeover(); 201 | } 202 | else { 203 | reply.continue(); 204 | } 205 | }); 206 | } 207 | }], 208 | auth: ((options.httpMethod === 'OPTIONS') ? false : 'default'), 209 | handler: { 210 | async: async function (request, reply) { 211 | await HapiRouter.handleRequest(options.ResourceClass, options.pathType, request, reply); 212 | } 213 | } 214 | } 215 | }; 216 | this.addRouteDocumentation(options, route); 217 | this.routesList.push(route); 218 | } 219 | addRouteDocumentation(options, route){ 220 | let self = this, 221 | resourceDocumentation = options.ResourceClass.getDocumentation.call(options.resourceMethod), 222 | tags = ['api'], 223 | resourceName = options.ResourceClass.getName(), 224 | validate = { 225 | params: {} 226 | }; 227 | 228 | 229 | if (resourceName) { 230 | tags.push(resourceName); 231 | } 232 | 233 | if ((options.pathType === 'instance' && !options.action) || options.pathType === 'instance_action') { 234 | 235 | validate.params.id = Joi.string() 236 | .required() 237 | .description('instance ID'); 238 | 239 | } 240 | if ( options.pathType === 'instance_action') { 241 | 242 | validate.params.action = Joi.string() 243 | .required() 244 | .description('instance ID'); 245 | 246 | } 247 | if(options.pathType === 'collection' && options.httpMethod.toUpperCase() === 'GET'){ 248 | validate.query = { 249 | _sort : Joi.string(), 250 | _filter: Joi.string(), 251 | _page_size: Joi.string() 252 | } 253 | } 254 | 255 | if (['POST', 'PUT', 'PATCH'].indexOf(options.httpMethod) != -1){ 256 | validate.payload = Joi.object().description('Payload for request'); 257 | } 258 | 259 | if(!options.action){ 260 | validate.query = validate.query || {}; 261 | validate.query._embedded = Joi.string(); 262 | validate.query._fields = Joi.string(); 263 | } 264 | 265 | route.config.description = resourceDocumentation && resourceDocumentation.title; 266 | route.config.notes = resourceDocumentation && resourceDocumentation.description; 267 | route.config.tags = tags; 268 | route.config.validate = validate; 269 | } 270 | 271 | static async handleRequest(resourceClass, pathType, request, reply) { 272 | try { 273 | let result = await resourceClass._handleRequest({ 274 | method: request.method, 275 | pathType: pathType, 276 | requestProperties: request.requestProperties, 277 | id: request.params.id, 278 | action: request.params.action, 279 | data: request.payload 280 | }); 281 | if (result) { 282 | if (result.obj) { 283 | let resultRendered = result.render(request.requestProperties); 284 | reply(resultRendered.data).type(resultRendered.type).code(HTTPCodes.success); 285 | } 286 | else { 287 | reply().code(HTTPCodes.noContent); 288 | } 289 | } 290 | } 291 | catch (error) { 292 | try { 293 | reply(HapiRouter.errorHandling(error)); 294 | } 295 | catch (e) { 296 | console.error(e.stack); 297 | reply(Boom.wrap(e, 500)) 298 | } 299 | } 300 | } 301 | 302 | defaultMiddlewares(request, done) { 303 | try { 304 | request.requestProperties = request.requestProperties || {}; 305 | this.fetchResource(request); 306 | this.checkRoles(request, err => { 307 | if (!err) { 308 | HapiRouter.fetchRequestProperties(request); 309 | done(); 310 | } else { 311 | done(err); 312 | } 313 | }); 314 | 315 | } catch (err) { 316 | done(err); 317 | } 318 | 319 | } 320 | 321 | static errorHandling(error) { 322 | let statusCode; 323 | if (error instanceof Exceptions.NotFound) { 324 | statusCode = HTTPCodes.notFound; 325 | } 326 | else if (error instanceof Exceptions.MethodNotAllowed) { 327 | statusCode = HTTPCodes.methodNotAllowed; 328 | } 329 | else if (error instanceof Exceptions.NotImplemented) { 330 | statusCode = HTTPCodes.notImplemented; 331 | } 332 | else if (error instanceof Exceptions.BadRequest) { 333 | statusCode = HTTPCodes.badRequest; 334 | } 335 | else if (error instanceof Exceptions.Unauthorized) { 336 | statusCode = HTTPCodes.unauthorized; 337 | } 338 | else if (error instanceof Exceptions.Forbidden) { 339 | statusCode = HTTPCodes.forbidden; 340 | } 341 | else { 342 | console.error(error.stack); 343 | statusCode = HTTPCodes.internalServerError; 344 | } 345 | return Boom.wrap(error, statusCode || 500); 346 | } 347 | 348 | fetchResource(request) { 349 | let path = request.path.slice(RouterConfig.basePath.length); 350 | let resourceName = HapiRouter.resourceName(path), 351 | ResourceClass = this.entries[resourceName], 352 | RouterClass = this.constructor; 353 | 354 | if(request.params.id){ 355 | let pathRegEx = /[^\/]+(?=\/$|$)/; 356 | let match = request.route.path.match(pathRegEx); 357 | if(match && match[0] != "{id}"){ 358 | request.params.action = match[0]; 359 | } 360 | } 361 | 362 | if (!request.resourceClass || !request.resourceMethod) { 363 | let resourceMethod = RouterClass.getResourceMethod(request, ResourceClass); 364 | if (!resourceMethod) { 365 | throw new Exceptions.NotImplemented(); 366 | } 367 | else { 368 | request.resourceMethod = resourceMethod; 369 | } 370 | request.resourceClass = ResourceClass; 371 | } 372 | } 373 | 374 | static fetchRequestProperties(request) { 375 | let requestProperties = { 376 | query: request.query, 377 | headers: { 378 | accept: request.headers.accept 379 | } 380 | }; 381 | request.requestProperties = _.extend(request.requestProperties, HapiRouter.parseRequest(requestProperties)); 382 | } 383 | 384 | checkAuthentication(request, reply) { 385 | try { 386 | this.fetchResource(request); 387 | let authenticationMethod = request.resourceClass.getAuthentication(request.resourceMethod); 388 | 389 | if (authenticationMethod && authenticationMethod != 'none') { 390 | let auth = new Auth(this.passport); 391 | auth.authenticate(authenticationMethod, {session: false}, function (err, user) { 392 | if (err) return reply(Boom.unauthorized(err)); 393 | if (user === false) { 394 | return reply(Boom.unauthorized(null)); 395 | } else { 396 | request.requestProperties = request.requestProperties || {}; 397 | request.requestProperties.user = user; 398 | } 399 | return reply.continue({credentials: {}}); 400 | })(request, reply); 401 | } 402 | else { 403 | return reply.continue({credentials: {}}); 404 | } 405 | } 406 | catch (error) { 407 | try { 408 | return reply(HapiRouter.errorHandling(error)); 409 | } 410 | catch (e) { 411 | return reply(Boom.wrap(e, 500)); 412 | } 413 | } 414 | } 415 | 416 | checkRoles(request, done) { 417 | let allowedRoles = request.resourceClass.getAllowedRoles(request.resourceMethod); 418 | if (request.method.toUpperCase() !== 'OPTIONS') { 419 | BaseRouter.checkUserRole(request.requestProperties.user, allowedRoles).then(function () { 420 | done(); 421 | }).catch(function (err) { 422 | done(err); 423 | }); 424 | } 425 | else { 426 | done(); 427 | } 428 | 429 | } 430 | } 431 | export default HapiRouter; -------------------------------------------------------------------------------- /apey-eye/routers/KoaGenericRouter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by GlazedSolutions on 03/03/2015. 3 | */ 4 | import KoaRouter from './KoaRouter'; 5 | import HTTPCodes from './../HTTPCodes'; 6 | import RouterConfig from '../config/router.js'; 7 | 8 | class KoaGenericRouter extends KoaRouter{ 9 | constructor() { 10 | super(); 11 | this.appendGenericRouter(); 12 | } 13 | 14 | appendGenericRouter() { 15 | var self = this; 16 | 17 | this.router.use(function*(next) { 18 | 19 | yield next; 20 | if (this.status === HTTPCodes.notFound) { 21 | let path = this.path.slice(RouterConfig.basePath.length); 22 | var resourceName = KoaRouter.resourceName(path); 23 | if(resourceName){ 24 | var newResourceClass = KoaRouter.createGenericResourceClass(resourceName); 25 | 26 | self.register([{ 27 | path: `${resourceName}`, 28 | resource: newResourceClass 29 | }]); 30 | 31 | this.redirect(this.request.url); 32 | this.status = HTTPCodes.temporaryRedirect; 33 | } 34 | 35 | } 36 | }); 37 | } 38 | } 39 | export default KoaGenericRouter; -------------------------------------------------------------------------------- /apey-eye/routers/KoaRouter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by GlazedSolutions on 03/03/2015. 3 | */ 4 | import BaseRouter from './../BaseRouter.js'; 5 | import Resource from './../Resource'; 6 | import * as Exceptions from './../Exceptions'; 7 | import HTTPCodes from './../HTTPCodes'; 8 | import Auth from './../Auth.js'; 9 | import RouterConfig from '../config/router.js'; 10 | 11 | import koa from 'koa'; 12 | import _ from 'underscore'; 13 | import router from 'koa-router'; 14 | import compose from 'koa-compose'; 15 | import bodyParser from 'koa-body-parser'; 16 | import http from 'http'; 17 | 18 | class KoaRouter extends BaseRouter { 19 | constructor() { 20 | super(); 21 | this.router = router(); 22 | this.router.use(this.errorHandling); 23 | this.entries = {}; 24 | 25 | KoaRouter.passport = require('koa-passport'); 26 | } 27 | start(options, callback){ 28 | var app = koa(); 29 | 30 | app.use(bodyParser()); 31 | app.use(KoaRouter.passport.initialize()); 32 | app.use(this.routes()); 33 | 34 | app.listen(options.port); 35 | 36 | callback(false, app); 37 | } 38 | routes() { 39 | return this.router.routes(); 40 | } 41 | static pathTypes(path) { 42 | let basePath = RouterConfig.basePath || ''; 43 | 44 | if (basePath && basePath.slice(-1) === '/') { 45 | throw new Error("Base path wouldn't end without character '/'"); 46 | } 47 | 48 | return { 49 | collection: `${basePath}/${path}`, 50 | instance: `${basePath}/${path}/:id`, 51 | instance_action: `${basePath}/${path}/:id/:action` 52 | }; 53 | } 54 | appendBaseMethods(entry) { 55 | 56 | var resourceClass = entry.resource; 57 | 58 | 59 | Object.keys(KoaRouter.pathTypes(entry.path)).forEach(pathType => { 60 | var path = KoaRouter.pathTypes(entry.path)[pathType]; 61 | 62 | this.router.all(path, 63 | this.defaultMiddlewares(resourceClass), 64 | function*(next) { 65 | var result = yield resourceClass._handleRequest({ 66 | method: this.method, 67 | pathType: pathType, 68 | requestProperties: this.requestProperties, 69 | id: this.params.id, 70 | action: this.params.action, 71 | data: this.request.body 72 | }); 73 | 74 | if (result) { 75 | if (result.obj) { 76 | this.status = 200; 77 | var resultRendered = result.render(this.requestProperties); 78 | 79 | this.body = resultRendered.data; 80 | this.type = resultRendered.type; 81 | } 82 | else { 83 | this.status = 204; 84 | } 85 | } 86 | 87 | yield next; 88 | } 89 | ); 90 | 91 | }); 92 | } 93 | 94 | defaultMiddlewares(resourceClass) { 95 | var RouterClass = this.constructor; 96 | var stack = []; 97 | 98 | stack.push(function*(next) { 99 | let resourceMethod = RouterClass.getResourceMethod(this, resourceClass); 100 | if (!resourceMethod) { 101 | throw new Exceptions.NotImplemented(); 102 | } 103 | else { 104 | this.resourceMethod = resourceMethod; 105 | } 106 | this.resourceClass = resourceClass; 107 | this.requestProperties = {}; 108 | yield next; 109 | }); 110 | stack.push(RouterClass.checkAuthentication); 111 | stack.push(RouterClass.checkRoles); 112 | stack.push(function*(next) { 113 | yield KoaRouter.fetchRequestProperties.call(this, next, RouterClass); 114 | yield next; 115 | }); 116 | 117 | return compose(stack); 118 | } 119 | 120 | * errorHandling(next) { 121 | try { 122 | yield next; 123 | } 124 | catch (err) { 125 | if (err instanceof Exceptions.NotFound) { 126 | this.status = HTTPCodes.notFound; 127 | } 128 | else if (err instanceof Exceptions.MethodNotAllowed) { 129 | this.status = HTTPCodes.methodNotAllowed; 130 | } 131 | else if (err instanceof Exceptions.NotImplemented) { 132 | this.status = HTTPCodes.notImplemented; 133 | } 134 | else if (err instanceof Exceptions.BadRequest) { 135 | this.status = HTTPCodes.badRequest; 136 | } 137 | else if (err instanceof Exceptions.Unauthorized) { 138 | this.status = HTTPCodes.unauthorized; 139 | } 140 | else if (err instanceof Exceptions.Forbidden) { 141 | this.status = HTTPCodes.forbidden; 142 | } 143 | else { 144 | console.error(err.stack); 145 | this.status = HTTPCodes.internalServerError; 146 | } 147 | this.body = err.message; 148 | } 149 | } 150 | 151 | static *fetchRequestProperties(next, RouterClass) { 152 | 153 | let koaObj = this; 154 | let request = { 155 | query: koaObj.request.query, 156 | headers: { 157 | accept: koaObj.get('Accept') 158 | } 159 | }; 160 | this.requestProperties = _.extend(this.requestProperties, RouterClass.parseRequest(request)); 161 | 162 | yield next; 163 | } 164 | 165 | static *checkAuthentication(next) { 166 | let ctx = this; 167 | 168 | let authenticationMethod = this.resourceClass.getAuthentication(this.resourceMethod); 169 | 170 | if(authenticationMethod && authenticationMethod != 'none'){ 171 | let auth = new Auth(KoaRouter.passport); 172 | yield* auth.authenticate(authenticationMethod, {session: false}, function*(err, user, info) { 173 | if (err) throw new Exceptions.BadRequest(err); 174 | if (user === false) { 175 | throw new Exceptions.Unauthorized(); 176 | } else { 177 | ctx.requestProperties.user = user; 178 | yield next; 179 | } 180 | }).call(this, next) 181 | } 182 | else{ 183 | yield next; 184 | } 185 | } 186 | 187 | static *checkRoles(next) { 188 | let allowedRoles = this.resourceClass.getAllowedRoles(this.resourceMethod); 189 | 190 | yield BaseRouter.checkUserRole(this.requestProperties.user, allowedRoles); 191 | 192 | yield next; 193 | } 194 | } 195 | export default KoaRouter; -------------------------------------------------------------------------------- /example/index-hapi.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by GlazedSolutions on 02/03/2015. 3 | */ 4 | 5 | import ApeyEye from '../apey-eye'; 6 | 7 | let HapiRouter = ApeyEye.HapiRouter; 8 | let HapiGenericRouter = ApeyEye.HapiGenericRouter; 9 | 10 | import CategoryResource from './resources/CategoryResource.js'; 11 | import ClientResource from './resources/ClientResource.js'; 12 | import CourierResource from './resources/CourierResource.js'; 13 | import OrderProductResource from './resources/OrderProductResource.js'; 14 | import OrderResource from './resources/OrderResource.js'; 15 | import ProductResource from './resources/ProductResource.js'; 16 | import RestaurantResource from './resources/RestaurantResource.js'; 17 | import ScheduleResource from './resources/ScheduleResource.js'; 18 | 19 | let router = new HapiGenericRouter(); 20 | router.register([ 21 | { 22 | path: 'restaurant', 23 | resource: RestaurantResource 24 | }, 25 | { 26 | path: "orders", 27 | resource: OrderResource 28 | }, 29 | { 30 | resource: ProductResource 31 | }, 32 | { 33 | resource: ScheduleResource 34 | }, 35 | { 36 | resource: CategoryResource 37 | }, 38 | { 39 | path: "couriers", 40 | resource: CourierResource 41 | }, 42 | { 43 | path: "clients", 44 | resource: CourierResource 45 | }, 46 | { 47 | resource: OrderProductResource 48 | } 49 | ]); 50 | 51 | router.start({port: 3000}, function (err, server) { 52 | if (!err) { 53 | console.log('Server running at', server.info.uri); 54 | } 55 | else { 56 | console.log('Error starting server'); 57 | } 58 | }); -------------------------------------------------------------------------------- /example/index-koa.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by GlazedSolutions on 02/03/2015. 3 | */ 4 | 5 | import ApeyEye from '../apey-eye'; 6 | 7 | let KoaRouter = ApeyEye.KoaRouter; 8 | let KoaGenericRouter = ApeyEye.KoaGenericRouter; 9 | 10 | import CategoryResource from './resources/CategoryResource.js'; 11 | import ClientResource from './resources/ClientResource.js'; 12 | import CourierResource from './resources/CourierResource.js'; 13 | import OrderProductResource from './resources/OrderProductResource.js'; 14 | import OrderResource from './resources/OrderResource.js'; 15 | import ProductResource from './resources/ProductResource.js'; 16 | import RestaurantResource from './resources/RestaurantResource.js'; 17 | import ScheduleResource from './resources/ScheduleResource.js'; 18 | 19 | let router = new KoaGenericRouter(); 20 | router.register([ 21 | { 22 | path: 'restaurant', 23 | resource: RestaurantResource 24 | }, 25 | { 26 | path: "orders", 27 | resource: OrderResource 28 | }, 29 | { 30 | resource: ProductResource 31 | }, 32 | { 33 | resource: ScheduleResource 34 | }, 35 | { 36 | resource: CategoryResource 37 | }, 38 | { 39 | path: "couriers", 40 | resource: CourierResource 41 | }, 42 | { 43 | path: "clients", 44 | resource: CourierResource 45 | }, 46 | { 47 | resource: OrderProductResource 48 | } 49 | ]); 50 | 51 | 52 | router.start({port:3000},function (err, server) { 53 | if(!err){ 54 | console.log('Server running at'); 55 | } 56 | else{ 57 | console.log('Error starting server'); 58 | } 59 | }); 60 | 61 | -------------------------------------------------------------------------------- /example/models/CategoryModel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by GlazedSolutions on 12/05/2015. 3 | */ 4 | 5 | import ApeyEye from '../../apey-eye'; 6 | 7 | let Decorators = ApeyEye.Decorators; 8 | let Input = ApeyEye.Input; 9 | let RethinkDBModel = ApeyEye.RethinkDBModel; 10 | 11 | var categoryInput = new Input({ 12 | name: {type: "string", required: true}, 13 | products: {type: "collection", model: "product", inverse: "categories"} 14 | }); 15 | 16 | @Decorators.Input(categoryInput) 17 | @Decorators.Name("category") 18 | class CategoryModel extends RethinkDBModel { 19 | } 20 | 21 | export default CategoryModel; -------------------------------------------------------------------------------- /example/models/ClientModel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by GlazedSolutions on 12/05/2015. 3 | */ 4 | import ApeyEye from '../../apey-eye'; 5 | 6 | let Decorators = ApeyEye.Decorators; 7 | let Input = ApeyEye.Input; 8 | let RethinkDBModel = ApeyEye.RethinkDBModel; 9 | 10 | var clientInput = new Input({ 11 | state: {type: "number", required: true}, 12 | location: {type: "string", required:false} 13 | }); 14 | 15 | @Decorators.Input(clientInput) 16 | @Decorators.Name("client") 17 | class ClientModel extends RethinkDBModel { 18 | } 19 | 20 | export default ClientModel; -------------------------------------------------------------------------------- /example/models/CourierModel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by GlazedSolutions on 12/05/2015. 3 | */ 4 | import ApeyEye from '../../apey-eye'; 5 | 6 | let Decorators = ApeyEye.Decorators; 7 | let Input = ApeyEye.Input; 8 | let RethinkDBModel = ApeyEye.RethinkDBModel; 9 | 10 | var courierInput = new Input({ 11 | state: {type: "number", required: true}, 12 | location: {type: "string", required:false}, 13 | orders : {type: "collection", model:"order",inverse:"courier"} 14 | }); 15 | 16 | @Decorators.Input(courierInput) 17 | @Decorators.Name("courier") 18 | class CourierModel extends RethinkDBModel { 19 | } 20 | 21 | export default CourierModel; -------------------------------------------------------------------------------- /example/models/OrderModel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by GlazedSolutions on 12/05/2015. 3 | */ 4 | import ApeyEye from '../../apey-eye'; 5 | 6 | let Decorators = ApeyEye.Decorators; 7 | let Input = ApeyEye.Input; 8 | let RethinkDBModel = ApeyEye.RethinkDBModel; 9 | 10 | let orderInput = new Input({ 11 | code : {type: "number", required:true}, 12 | orderDate : {type: "date", default : "now"}, 13 | deliveryDate : {type: "date", required:false}, 14 | state : {type: "number", required:true}, 15 | invoice : {type: "string", required:false}, 16 | deliveryAddress : {type: "string", required:true}, 17 | payed : {type: "boolean", required:true}, 18 | products: {type: "manyToMany", model: "product", inverse: "orders", through:"orderProduct"}, 19 | 20 | courier : {type: "reference", model:"courier"} 21 | }); 22 | 23 | @Decorators.Input(orderInput) 24 | @Decorators.Name("order") 25 | @Decorators.Query({ 26 | _sort: ['code'], 27 | _page_size: 10 28 | }) 29 | class OrderModel extends RethinkDBModel { 30 | } 31 | 32 | export default OrderModel; -------------------------------------------------------------------------------- /example/models/OrderProductModel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by GlazedSolutions on 12/05/2015. 3 | */ 4 | import ApeyEye from '../../apey-eye'; 5 | 6 | let Decorators = ApeyEye.Decorators; 7 | let Input = ApeyEye.Input; 8 | let RethinkDBModel = ApeyEye.RethinkDBModel; 9 | 10 | var orderProductInput = new Input({ 11 | product: {type: "reference", model: "product"}, 12 | order: {type: "reference", model: "order"} 13 | }); 14 | 15 | @Decorators.Input(orderProductInput) 16 | @Decorators.Name("orderProduct") 17 | class OrderProductModel extends RethinkDBModel { 18 | } 19 | export default OrderProductModel; -------------------------------------------------------------------------------- /example/models/ProductModel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by GlazedSolutions on 12/05/2015. 3 | */ 4 | import ApeyEye from '../../apey-eye'; 5 | 6 | let Decorators = ApeyEye.Decorators; 7 | let Input = ApeyEye.Input; 8 | let RethinkDBModel = ApeyEye.RethinkDBModel; 9 | 10 | var productInput = new Input({ 11 | name: {type: "string", required: true}, 12 | price: {type: "number", required: true}, 13 | VAT: {type: "number", required: true}, 14 | restaurant: {type : "reference", model:"restaurant", required:true}, 15 | category: {type: "reference", model: "category"}, 16 | orders: {type: "manyToMany", model: "order", inverse: "products", through:"orderProduct"} 17 | 18 | }); 19 | 20 | @Decorators.Input(productInput) 21 | @Decorators.Name("product") 22 | class ProductModel extends RethinkDBModel { 23 | } 24 | 25 | export default ProductModel; -------------------------------------------------------------------------------- /example/models/RestaurantModel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by GlazedSolutions on 12/05/2015. 3 | */ 4 | import ApeyEye from '../../apey-eye'; 5 | 6 | let Decorators = ApeyEye.Decorators; 7 | let Input = ApeyEye.Input; 8 | let RethinkDBModel = ApeyEye.RethinkDBModel; 9 | 10 | let restaurantInput = new Input({ 11 | name: {type: "string", required: true}, 12 | phone: {type: "string", required: false}, 13 | address: {type: "string", required:true}, 14 | rating : {type: "number", default: 0}, 15 | photo: {type:"string"}, 16 | schedules: {type: "collection", model: "schedule", inverse: "restaurant"}, 17 | products: {type: "collection", model: "product", inverse: "restaurant"} 18 | }); 19 | 20 | @Decorators.Input(restaurantInput) 21 | @Decorators.Name("restaurant") 22 | @Decorators.Query({ 23 | _sort: ['name', '-address'], 24 | _page_size: 10 25 | }) 26 | class RestaurantModel extends RethinkDBModel { 27 | } 28 | 29 | export default RestaurantModel; -------------------------------------------------------------------------------- /example/models/ScheduleModel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by GlazedSolutions on 03/06/2015. 3 | */ 4 | import ApeyEye from '../../apey-eye'; 5 | 6 | let Decorators = ApeyEye.Decorators; 7 | let Input = ApeyEye.Input; 8 | let RethinkDBModel = ApeyEye.RethinkDBModel; 9 | 10 | var scheduleInput = new Input({ 11 | startTime: {type: "string", required: true}, 12 | endTime: {type: "string", required: true}, 13 | restaurant : {type: "reference", model:"restaurant", required:true} 14 | }); 15 | 16 | @Decorators.Input(scheduleInput) 17 | @Decorators.Name("schedule") 18 | class ScheduleModel extends RethinkDBModel { 19 | } 20 | 21 | export default ScheduleModel; -------------------------------------------------------------------------------- /example/resources/CategoryResource.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by GlazedSolutions on 12/05/2015. 3 | */ 4 | 5 | import ApeyEye from '../../apey-eye'; 6 | 7 | let Decorators = ApeyEye.Decorators; 8 | let GenericResource = ApeyEye.GenericResource; 9 | let Input = ApeyEye.Input; 10 | 11 | import CategoryModel from '../models/CategoryModel.js'; 12 | 13 | @Decorators.Model(CategoryModel) 14 | @Decorators.Name("category") 15 | @Decorators.Authentication("basic") 16 | @Decorators.Roles(["restaurant_owner"]) 17 | class CategoryResource extends GenericResource { 18 | } 19 | 20 | export default CategoryResource; -------------------------------------------------------------------------------- /example/resources/ClientResource.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by GlazedSolutions on 12/05/2015. 3 | */ 4 | 5 | import ApeyEye from '../../apey-eye'; 6 | 7 | let Decorators = ApeyEye.Decorators; 8 | let Formatters = ApeyEye.Formatters; 9 | let GenericResource = ApeyEye.GenericResource; 10 | let Input = ApeyEye.Input; 11 | 12 | import ClientModel from '../models/ClientModel.js'; 13 | 14 | @Decorators.Model(ClientModel) 15 | @Decorators.Name("client") 16 | @Decorators.Methods(["constructor", "static.fetchOne"]) 17 | class ClientResource extends GenericResource { 18 | @Decorators.Action() 19 | async post_register(options){ 20 | //DO SOME STUFF 21 | return "Registered" 22 | } 23 | @Decorators.Action() 24 | static async post_register(options){ 25 | //DO SOME STUFF 26 | return "Registered static" 27 | } 28 | } 29 | 30 | export default ClientResource; -------------------------------------------------------------------------------- /example/resources/CourierResource.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by GlazedSolutions on 12/05/2015. 3 | */ 4 | 5 | import ApeyEye from '../../apey-eye'; 6 | 7 | let Decorators = ApeyEye.Decorators; 8 | let Formatters = ApeyEye.Formatters; 9 | let GenericResource = ApeyEye.GenericResource; 10 | let Input = ApeyEye.Input; 11 | 12 | import CourierModel from '../models/CourierModel.js'; 13 | 14 | @Decorators.Model(CourierModel) 15 | @Decorators.Name("courier") 16 | @Decorators.Methods(["constructor", "static.fetchOne","static.fetch"]) 17 | class CourierResource extends GenericResource { 18 | @Decorators.Action() 19 | async post_register(options){ 20 | //DO SOME STUFF 21 | return "Registered" 22 | } 23 | @Decorators.Action() 24 | static async post_register(options){ 25 | //DO SOME STUFF 26 | return "Registered static" 27 | } 28 | } 29 | 30 | export default CourierResource; -------------------------------------------------------------------------------- /example/resources/OrderProductResource.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by GlazedSolutions on 12/05/2015. 3 | */ 4 | 5 | import ApeyEye from '../../apey-eye'; 6 | 7 | let Decorators = ApeyEye.Decorators; 8 | let Resource = ApeyEye.Resource; 9 | let Input = ApeyEye.Input; 10 | 11 | import OrderProductModel from '../models/OrderProductModel.js'; 12 | 13 | @Decorators.Name("orderProduct") 14 | @Decorators.Output({ 15 | _fields: ["product", "order"] 16 | }) 17 | class OrderProductResource extends Resource { 18 | static async fetch(options = {}) { 19 | let ResourceClass = this, 20 | properties = ResourceClass.joinProperties(options.requestProperties, ResourceClass.fetch); 21 | 22 | let modelObj = await CategoryRestaurantModel.fetch({resourceProperties:properties}); 23 | return ResourceClass._serializeArray(modelObj, properties); 24 | } 25 | } 26 | 27 | export default OrderProductResource; -------------------------------------------------------------------------------- /example/resources/OrderResource.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by GlazedSolutions on 12/05/2015. 3 | */ 4 | 5 | import ApeyEye from '../../apey-eye'; 6 | 7 | let Decorators = ApeyEye.Decorators; 8 | let GenericResource = ApeyEye.GenericResource; 9 | let Input = ApeyEye.Input; 10 | 11 | import OrderModel from '../models/OrderModel.js'; 12 | 13 | @Decorators.Model(OrderModel) 14 | @Decorators.Name("order") 15 | @Decorators.Authentication("basic") 16 | @Decorators.Roles(["client", "courier"]) 17 | class OrderResource extends GenericResource { 18 | } 19 | 20 | export default OrderResource; -------------------------------------------------------------------------------- /example/resources/ProductResource.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by GlazedSolutions on 12/05/2015. 3 | */ 4 | 5 | import ApeyEye from '../../apey-eye'; 6 | 7 | let Decorators = ApeyEye.Decorators; 8 | let GenericResource = ApeyEye.GenericResource; 9 | let Input = ApeyEye.Input; 10 | 11 | import ProductModel from '../models/ProductModel.js'; 12 | 13 | @Decorators.Model(ProductModel) 14 | @Decorators.Name("product") 15 | @Decorators.Authentication("basic") 16 | @Decorators.Roles(["restaurant_owner"]) 17 | class ProductResource extends GenericResource { 18 | @Decorators.Roles(["basic_client"]) 19 | static async fetch(options){ 20 | return await GenericResource.fetch(options); 21 | } 22 | @Decorators.Roles(["basic_client"]) 23 | static async fetchOne(options){ 24 | return await GenericResource.fetch(options); 25 | } 26 | } 27 | 28 | export default ProductResource; -------------------------------------------------------------------------------- /example/resources/RestaurantResource.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by GlazedSolutions on 12/05/2015. 3 | */ 4 | 5 | import ApeyEye from '../../apey-eye'; 6 | 7 | let Decorators = ApeyEye.Decorators; 8 | let Formatters = ApeyEye.Formatters; 9 | let GenericResource = ApeyEye.GenericResource; 10 | let Input = ApeyEye.Input; 11 | 12 | import RestaurantModel from '../models/RestaurantModel.js'; 13 | 14 | @Decorators.Model(RestaurantModel) 15 | @Decorators.Name("Restaurant") 16 | @Decorators.Documentation({ 17 | title: "Restaurant Resource", 18 | description: "This resource is the entry point to access restaurants information" 19 | }) 20 | @Decorators.Output({ 21 | _embedded: ['schedules','products'] 22 | }) 23 | @Decorators.Authentication("basic") 24 | @Decorators.Roles(["restaurant_owner", "admin"]) 25 | class RestaurantResource extends GenericResource { 26 | @Decorators.Action() 27 | static get_schema(){ 28 | let ResourceClass = this, 29 | input = RestaurantModel.getInput(); 30 | return input; 31 | } 32 | @Decorators.Roles(["basic_client"]) 33 | static async fetch(options){ 34 | return await GenericResource.fetch(options); 35 | } 36 | @Decorators.Roles(["basic_client"]) 37 | static async fetchOne(options){ 38 | return await GenericResource.fetch(options); 39 | } 40 | } 41 | 42 | export default RestaurantResource; -------------------------------------------------------------------------------- /example/resources/ScheduleResource.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by GlazedSolutions on 12/05/2015. 3 | */ 4 | 5 | import ApeyEye from '../../apey-eye'; 6 | 7 | let Decorators = ApeyEye.Decorators; 8 | let GenericResource = ApeyEye.GenericResource; 9 | let Input = ApeyEye.Input; 10 | 11 | import ScheduleModel from '../models/ScheduleModel.js'; 12 | 13 | @Decorators.Model(ScheduleModel) 14 | @Decorators.Name("schedule") 15 | @Decorators.Authentication("basic") 16 | @Decorators.Roles(["restaurant_owner"]) 17 | class ScheduleResource extends GenericResource { 18 | } 19 | 20 | export default ScheduleResource; -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'), 2 | babel = require('gulp-babel'); 3 | 4 | 5 | gulp.task('babel', function () { 6 | 7 | var includeFolders = [ 8 | "apey-eye", 9 | "test", 10 | "example" 11 | ]; 12 | 13 | includeFolders.forEach(function(folder){ 14 | gulp.src(["./"+folder+"/**/*"]) 15 | .pipe(babel({ 16 | stage: 0, 17 | optional: ["es7.decorators", "es7.asyncFunctions", "runtime"] 18 | })) 19 | .pipe(gulp.dest("build/"+folder)); 20 | }) 21 | 22 | }); 23 | 24 | gulp.task('watch', function () { 25 | var includeFolders = [ 26 | "apey-eye", 27 | "test", 28 | "example" 29 | ]; 30 | 31 | includeFolders.forEach(function(folder){ 32 | gulp.watch(["./"+folder+"/**/*"], ['babel']); 33 | }) 34 | }); 35 | 36 | gulp.task('default', ['babel',"watch"]); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apey-eye", 3 | "version": "0.0.5", 4 | "description": "An Object-Resource Mapping Node REST Framework", 5 | "main": "./build/apey-eye/index.js", 6 | "author": "GlazedSolutions", 7 | "license": "MIT", 8 | "homepage": "https://github.com/glazedSolutions/apey-eye", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/glazedSolutions/apey-eye" 12 | }, 13 | "keywords": [ 14 | "hapi", 15 | "async", 16 | "es7", 17 | "rest", 18 | "http", 19 | "framework", 20 | "rethinkdb", 21 | "web", 22 | "orm", 23 | "decorators", 24 | "swagger" 25 | ], 26 | "bugs": { 27 | "url": "https://github.com/glazedSolutions/apey-eye/issues" 28 | }, 29 | "scripts": { 30 | "build": "node node_modules/gulp/bin/gulp.js babel", 31 | "postinstall" : "npm run-script build", 32 | "test-cov": "node --harmony node_modules/istanbul-harmony/lib/cli.js cover node_modules/mocha/bin/_mocha ./build/test", 33 | "test" : "node --harmony node_modules/mocha/bin/_mocha ./build/test", 34 | "start-koa": "node --harmony build/example/index-koa.js", 35 | "start-hapi": "node --harmony build/example/index-hapi.js" 36 | }, 37 | "dependencies": { 38 | "async": "^0.9.0", 39 | "babel-runtime": "^5.4.7", 40 | "bluebird": "^2.9.20", 41 | "boom": "^2.7.1", 42 | "co": "^4.5.1", 43 | "gulp": "^3.8.11", 44 | "gulp-babel": "^5.1.0", 45 | "hapi": "^8.4.0", 46 | "hapi-async-handler": "^1.0.2", 47 | "hapi-swagger": "^0.7.3", 48 | "koa": "^0.18.1", 49 | "koa-body-parser": "^1.1.1", 50 | "koa-compose": "^2.3.0", 51 | "koa-generic-session": "^1.8.0", 52 | "koa-mount": "^1.3.0", 53 | "koa-passport": "^1.1.6", 54 | "koa-router": "^4.2.0", 55 | "negotiator": "^0.5.1", 56 | "passport": "^0.2.1", 57 | "passport-http": "^0.2.2", 58 | "passport-local": "^1.0.0", 59 | "request-promise": "^0.4.2", 60 | "rethinkdbdash": "^1.16.12", 61 | "shortid": "^2.2.2", 62 | "underscore": "^1.8.2", 63 | "util": "^0.10.3", 64 | "chai": "^2.1.1", 65 | "chai-as-promised": "^4.3.0", 66 | "chai-things": "^0.2.0", 67 | "istanbul": "^0.3.13", 68 | "istanbul-harmony": "^0.3.12", 69 | "mocha": "^2.2.4", 70 | "mochawait": "^2.0.0" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /runbabel.cmd: -------------------------------------------------------------------------------- 1 | babel %* -------------------------------------------------------------------------------- /test/Input.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by GlazedSolutions on 11/03/2015. 3 | */ 4 | 5 | import chai from 'chai'; 6 | 7 | import ApeyEye from '../apey-eye'; 8 | 9 | let Input = ApeyEye.Input; 10 | let Model = ApeyEye.Model; 11 | 12 | let expect = chai.expect; 13 | let assert = chai.assert; 14 | 15 | 16 | describe('Input', function(){ 17 | class TestModel extends Model{} 18 | 19 | var restaurantInput; 20 | before(function(){ 21 | var validLocation = function(val){ 22 | if(val !== "Rua Costa Cabral"){ 23 | throw new Error("Invalid location.") 24 | } 25 | else{ 26 | return true; 27 | } 28 | }; 29 | 30 | restaurantInput = new Input({ 31 | name: {type: "string", required:true}, 32 | address: {type: "string", required:true, valid:validLocation}, 33 | phone: {type: "number"}, 34 | photo: {type: "string", regex: Input.URLPattern}, 35 | date: {type: "date"}, 36 | location: {type:"string"}, 37 | language: {type:"string", choices: ["PT", "EN"]} 38 | }); 39 | }); 40 | 41 | 42 | it('should throw an expection when valid properties is not an object', function*(){ 43 | assert.throw(function() { 44 | new Input(); 45 | }, Error); 46 | assert.throw(function() { 47 | new Input([]); 48 | }, Error); 49 | assert.throw(function() { 50 | new Input("invalidObj"); 51 | }, Error); 52 | assert.throw(function() { 53 | new Input(true); 54 | }, Error); 55 | assert.throw(function() { 56 | new Input(123); 57 | }, Error); 58 | assert.doesNotThrow(function() { 59 | new Input({ name: {type:"string"}}); 60 | }); 61 | }); 62 | it('should throw an expection when required properties aren\'t defined', function(){ 63 | assert.doesNotThrow(function() { 64 | new Input({ name: {type:"string"}}); 65 | }); 66 | assert.throw(function() { 67 | new Input({ name: {required:false}}); 68 | }); 69 | assert.doesNotThrow(function() { 70 | new Input({ name: {type:"number"}}); 71 | }); 72 | assert.throw(function() { 73 | new Input({ name: {type:"reference"}}); 74 | }); 75 | assert.doesNotThrow(function() { 76 | new Input({ name: {type:"reference", model: 'modelName'}}); 77 | }); 78 | }); 79 | it('should throw an expection when required properties aren\'t defined with correct type values', function(){ 80 | assert.doesNotThrow(function() { 81 | new Input({ name: {type:"string"}}); 82 | }); 83 | assert.doesNotThrow(function() { 84 | new Input({ name: {type:"reference", model: "TestModel"}}); 85 | }); 86 | assert.throw(function() { 87 | new Input({ name: {type:"strong"}}); 88 | }); 89 | assert.throw(function() { 90 | new Input({ name: {type:"string", required:"false"}}); 91 | }); 92 | assert.doesNotThrow(function() { 93 | new Input({ name: {type:"string", required:false}}); 94 | }); 95 | assert.doesNotThrow(function() { 96 | new Input({ name: {type:"string", regex:Input.ISODatePattern}}); 97 | }); 98 | assert.throw(function() { 99 | new Input({ name: {type:"string", regex:false}}); 100 | }); 101 | assert.throw(function() { 102 | new Input({ name: {type:"string", regex:"lalal"}}); 103 | }); 104 | assert.throw(function() { 105 | new Input({ name: {type:"string", valid:true}}); 106 | }); 107 | assert.doesNotThrow(function() { 108 | new Input({ name: {type:"string", valid:function(){}}}); 109 | }); 110 | assert.throw(function() { 111 | new Input({ name: {type:"string", default:123}}); 112 | }); 113 | assert.doesNotThrow(function() { 114 | new Input({ name: {type:"string", default:"stringValue"}}); 115 | }); 116 | 117 | assert.throw(function() { 118 | new Input({ name: {type:"string", choices:123}}); 119 | }); 120 | assert.throw(function() { 121 | new Input({ name: {type:"string", choices:[123, false]}}); 122 | }); 123 | assert.doesNotThrow(function() { 124 | new Input({ name: {type:"string", choices:["A","B"]}}); 125 | }); 126 | 127 | assert.throw(function() { 128 | new Input({ name: {type:"collection",model:"resourceName"}}); 129 | }); 130 | assert.throw(function() { 131 | new Input({ name: {type:"reference", model:123}}); 132 | }); 133 | assert.throw(function() { 134 | new Input({ name: {type:"reference"}}); 135 | }); 136 | assert.doesNotThrow(function() { 137 | new Input({ name: {type:"collection",model:"modelname",inverse:"inverseField", many:false}}); 138 | }); 139 | }); 140 | it('should return true if data received is valid or false if not',async function(){ 141 | var validData = { 142 | name: "name", 143 | address:"Rua Costa Cabral", 144 | phone:123, 145 | photo:"http://google.com/", 146 | date: "2015-03-10T14:27:44.031Z" 147 | }; 148 | 149 | expect(restaurantInput.valid(validData)).to.eventually.not.throw(); 150 | 151 | let invalid = JSON.parse(JSON.stringify(validData)); 152 | invalid.name=12; 153 | 154 | expect(restaurantInput.valid(invalid)).to.eventually.throw(); 155 | 156 | invalid = JSON.parse(JSON.stringify(validData)); 157 | invalid.address="Rua Dr Roberto Frias"; 158 | 159 | expect(restaurantInput.valid(invalid)).to.eventually.throw(); 160 | 161 | invalid = JSON.parse(JSON.stringify(validData)); 162 | invalid.phone="93404404"; 163 | 164 | expect(restaurantInput.valid(restaurantInput.valid(invalid))).to.eventually.throw(); 165 | 166 | invalid = JSON.parse(JSON.stringify(validData)); 167 | invalid.date="5th July 2015"; 168 | 169 | expect(restaurantInput.valid(restaurantInput.valid(invalid))).to.eventually.throw(); 170 | 171 | invalid = JSON.parse(JSON.stringify(validData)); 172 | invalid.language="SS"; 173 | 174 | expect(restaurantInput.valid(restaurantInput.valid(invalid))).to.eventually.throw(); 175 | 176 | validData.language="PT"; 177 | 178 | expect(restaurantInput.valid(restaurantInput.valid(validData))).to.eventually.not.throw(); 179 | 180 | 181 | }); 182 | }); -------------------------------------------------------------------------------- /test/models.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by GlazedSolutions on 11/03/2015. 3 | */ 4 | import chai from 'chai'; 5 | import chaiAsPromised from 'chai-as-promised'; 6 | import _ from 'underscore'; 7 | import 'mochawait'; 8 | import ModelRegister from '../apey-eye/ModelRegister.js'; 9 | 10 | import ApeyEye from '../apey-eye'; 11 | 12 | let Model = ApeyEye.Model; 13 | let RethinkDBModel = ApeyEye.RethinkDBModel; 14 | let Decorators = ApeyEye.Decorators; 15 | let Input = ApeyEye.Input; 16 | 17 | chai.use(chaiAsPromised); 18 | 19 | let expect = chai.expect, 20 | assert = chai.assert, 21 | should = chai.should; 22 | 23 | describe('Models', function () { 24 | 25 | 26 | var restaurantInput, 27 | restaurantInput2; 28 | 29 | before(function () { 30 | restaurantInput = new Input({ 31 | name: {type: "string", required: true}, 32 | address: {type: "string", required: true}, 33 | phone: {type: "number"}, 34 | photo: {type: "string", regex: Input.URLPattern}, 35 | date: {type: "date"}, 36 | location: {type: "string"}, 37 | language: {type: "string", choices: ["PT", "EN"]} 38 | }); 39 | 40 | restaurantInput2 = new Input({ 41 | name: {type: "string", required: true}, 42 | address: {type: "string", required: true}, 43 | phone: {type: "number"} 44 | }); 45 | }); 46 | 47 | beforeEach(function () { 48 | ModelRegister.empty(); 49 | }); 50 | 51 | describe('Model Declaration', function () { 52 | it('should access annotations properties from models', function () { 53 | 54 | @Decorators.Input(restaurantInput) 55 | @Decorators.Name("dataTableName") 56 | class TestModel extends Model { 57 | } 58 | 59 | assert.isDefined(TestModel.annotations); 60 | assert.isObject(TestModel.annotations); 61 | 62 | expect(Object.keys(TestModel.annotations).length).to.equal(2); 63 | assert.isDefined(TestModel.getInput()); 64 | assert.isDefined(TestModel.getInput()); 65 | 66 | expect(TestModel.getInput().properties).to.have.property("address"); 67 | expect(TestModel.getInput().properties).to.have.deep.property("address.type", "string"); 68 | expect(TestModel.getInput().properties).to.have.deep.property("phone.type", "number"); 69 | 70 | 71 | }); 72 | it('shouln\'t have the same properties in methods and in class', function () { 73 | 74 | @Decorators.Input(restaurantInput) 75 | @Decorators.Name("dataTableName") 76 | class TestModel extends Model { 77 | @Decorators.Input(restaurantInput2) 78 | static 79 | 80 | list() { 81 | } 82 | 83 | static post() { 84 | } 85 | 86 | @Decorators.Input(restaurantInput) 87 | static 88 | 89 | get() { 90 | } 91 | } 92 | 93 | assert.isDefined(TestModel.annotations); 94 | assert.isObject(TestModel.annotations); 95 | 96 | expect(Object.keys(TestModel.annotations).length).to.equal(2); 97 | 98 | assert.isDefined(TestModel.list.annotations); 99 | assert.isUndefined(TestModel.post.annotations); 100 | 101 | expect(TestModel.getInput()).to.deep.equal(TestModel.getInput(TestModel.post)); 102 | expect(TestModel.getInput()).to.not.deep.equal(TestModel.getInput(TestModel.list)); 103 | }); 104 | }); 105 | describe('Model properties', function () { 106 | 107 | var TestModel; 108 | before(function (done) { 109 | @Decorators.Input(restaurantInput) 110 | @Decorators.Name("restaurant") 111 | @Decorators.Query({ 112 | _sort: ['name', '-address'], 113 | _filter: {name: "name", phone: 20}, 114 | _page_size: 10 115 | }) 116 | @Decorators.Output({ 117 | _fields: ['id', 'name', 'address', 'phone', 'date'], 118 | _embedded: ['schedule', 'products'] 119 | }) 120 | class TestModelClass extends RethinkDBModel { 121 | } 122 | 123 | TestModel = TestModelClass; 124 | 125 | //done(); 126 | }); 127 | 128 | 129 | it('Model.joinRequest properties should join model properties with request properties', function () { 130 | 131 | var resourceProperties = { 132 | _sort: ['name'], 133 | _filter: {address: "Rua Costa Cabral"}, 134 | _pagination: {_page_size: 12}, 135 | _fields: ['name', 'language', 'phone'], 136 | _embedded: ['schedule'] 137 | }; 138 | var modelProperties = _.extend(TestModel.getOutput(), TestModel.getQuery()); 139 | 140 | 141 | var joinedProperties = TestModel.joinProperties(resourceProperties); 142 | expect(modelProperties).to.not.deep.equal(joinedProperties); 143 | expect(joinedProperties._sort).to.deep.equal(['name']); 144 | expect(joinedProperties._filter).to.deep.equal({name: "name", phone: 20, address: "Rua Costa Cabral"}); 145 | expect(joinedProperties._pagination._page_size).to.deep.equal(10); 146 | expect(joinedProperties._fields).to.deep.equal(['name', 'phone']); 147 | expect(joinedProperties._embedded).to.deep.equal(['schedule']); 148 | 149 | }); 150 | it('Model.joinRequest properties should join model properties with request properties', function () { 151 | 152 | @Decorators.Input(restaurantInput) 153 | @Decorators.Name("restaurant") 154 | @Decorators.Query({ 155 | _filter: {address: "Rua Sousa Aroso"}, 156 | _sort: ['name', '-address'] 157 | }) 158 | @Decorators.Output({ 159 | _embedded: ['schedule', 'products'] 160 | }) 161 | class TestModel extends Model { 162 | constructor(executor) { 163 | super(executor) 164 | } 165 | } 166 | 167 | var resourceProperties = { 168 | _filter: {address: "Rua Costa Cabral"}, 169 | _pagination: {_page_size: 12}, 170 | _fields: ['name', 'language', 'phone'] 171 | }; 172 | var modelProperties = _.extend(TestModel.getOutput(), TestModel.getQuery()); 173 | 174 | 175 | var joinedProperties = TestModel.joinProperties(resourceProperties); 176 | 177 | expect(modelProperties).to.not.deep.equal(joinedProperties); 178 | expect(joinedProperties._sort).to.deep.equal([{'name': 1}, {'address': -1}]); 179 | expect(joinedProperties._filter).to.deep.equal({address: "Rua Sousa Aroso"}); 180 | expect(joinedProperties._pagination._page_size).to.deep.equal(12); 181 | expect(joinedProperties._pagination._page).to.deep.equal(1); 182 | expect(joinedProperties._fields).to.deep.equal(['name', 'language', 'phone']); 183 | expect(joinedProperties._embedded).to.deep.equal(['schedule', 'products']); 184 | }); 185 | 186 | it('Model.fetch returns an array', function () { 187 | 188 | return TestModel.fetch().then(function (list) { 189 | expect(list).to.be.instanceOf(Array); 190 | expect(list).to.have.length.below(11); 191 | }); 192 | }); 193 | it('Model.fetch returns a serialized array ', function () { 194 | return TestModel.fetch().then(function (list) { 195 | list.should.all.contain.keys('id', 'obj', 'oldObj', 'put', 'patch', 'delete'); 196 | }); 197 | }); 198 | it('Model.post returns an object', function () { 199 | 200 | var data = { 201 | name: "restaurantName", 202 | address: "restaurantAddress", 203 | phone: 9492123 204 | }; 205 | 206 | return (new TestModel({data: data})).then(function (obj) { 207 | expect(obj.obj).to.be.instanceOf(Object); 208 | expect(obj.obj).to.have.property('address'); 209 | expect(obj.obj).to.have.property('phone'); 210 | expect(obj.obj).to.have.property('name'); 211 | }); 212 | }); 213 | it('Model.post invalid data may return an exception', async function () { 214 | 215 | var data = { 216 | name: "restaurantName", 217 | address: "restaurantAddress", 218 | phone: 9492123 219 | }; 220 | 221 | var invalidData1 = { 222 | name: 123, 223 | address: "restaurantAddress", 224 | phone: 9492123 225 | }; 226 | 227 | var invalidData2 = { 228 | name: "restaurantName", 229 | address: "restaurantAddress", 230 | phone: "invalidPhone" 231 | }; 232 | 233 | 234 | expect(new TestModel({data: data})).to.be.fulfilled; 235 | expect(new TestModel({data: invalidData1})).to.be.rejected; 236 | expect(new TestModel({data: invalidData2})).to.be.rejected; 237 | 238 | 239 | }); 240 | it('Model.fetchOne for a before inserted object returns the same object', async function () { 241 | 242 | var data = { 243 | name: "restaurantName", 244 | address: "restaurantAddress", 245 | phone: 9492123 246 | }; 247 | let postedObject = await new TestModel({data: data}); 248 | let obj = await TestModel.fetchOne({id: postedObject.obj.id}); 249 | 250 | expect(obj.obj).to.deep.equal(postedObject.obj); 251 | }); 252 | it('Model.put replace an object inserted before', async function () { 253 | 254 | var data = { 255 | name: "restaurantName", 256 | address: "restaurantAddress", 257 | phone: 9492123 258 | }; 259 | 260 | let obj = await new TestModel({data: data}); 261 | await obj.put({data: {name: "restaurantName2", address: "Rua Costa Cabral"}}); 262 | 263 | expect(obj.obj).to.not.have.property("phone"); 264 | expect(obj.obj).to.have.property("name", "restaurantName2"); 265 | expect(obj.obj).to.have.property("address", "Rua Costa Cabral"); 266 | 267 | }); 268 | it('Model.patch update an object inserted before', async function () { 269 | 270 | var data = { 271 | name: "restaurantName", 272 | address: "restaurantAddress", 273 | phone: 9492123 274 | }; 275 | 276 | let obj = await new TestModel({data: data}); 277 | await obj.patch({data: {name: "restaurantName2", address: "Rua Costa Cabral"}}); 278 | 279 | expect(obj.obj).to.have.property("phone", 9492123); 280 | expect(obj.obj).to.have.property("name", "restaurantName2"); 281 | expect(obj.obj).to.have.property("address", "Rua Costa Cabral"); 282 | 283 | }); 284 | it('Model.delete delete an object from database, so it is impossible to access him again', async function () { 285 | 286 | var data = { 287 | name: "restaurantName", 288 | address: "restaurantAddress", 289 | phone: 9492123 290 | }; 291 | let obj = await new TestModel({data: data}); 292 | let res = await obj.delete(); 293 | 294 | expect(res).to.be.true; 295 | expect(TestModel.fetchOne({id: obj.id})).to.eventually.throw(); 296 | 297 | }); 298 | it('Model.save makes a patch with object .obj data', async function () { 299 | 300 | var data = { 301 | name: "restaurantName", 302 | address: "restaurantAddress", 303 | phone: 9492123 304 | }; 305 | 306 | let obj = await new TestModel({data: data}); 307 | 308 | obj.obj.name = "restaurantName2"; 309 | obj.obj.address = "Rua Costa Cabral"; 310 | 311 | expect(obj.oldObj).to.have.property("name", "restaurantName"); 312 | expect(obj.oldObj).to.have.property("address", "restaurantAddress"); 313 | 314 | await obj.save(); 315 | 316 | expect(obj.obj).to.have.property("phone", 9492123); 317 | expect(obj.obj).to.have.property("name", "restaurantName2"); 318 | expect(obj.obj).to.have.property("address", "Rua Costa Cabral"); 319 | 320 | expect(obj.oldObj).to.have.property("name", "restaurantName2"); 321 | expect(obj.oldObj).to.have.property("address", "Rua Costa Cabral"); 322 | 323 | }); 324 | 325 | it('Model.save with invalid data may throw exceptions', async function () { 326 | 327 | var data = { 328 | name: "restaurantName", 329 | address: "restaurantAddress", 330 | phone: 9492123 331 | }; 332 | 333 | let obj = await new TestModel({data: data}); 334 | obj.obj.name = "restaurantName2"; 335 | obj.obj.address = 123123; 336 | 337 | expect(obj.save()).to.eventually.throw(); 338 | 339 | obj.obj.address = "restaurantAddress"; 340 | 341 | expect(obj.save()).to.eventually.not.throw(); 342 | }); 343 | it('Operations that returns the object may receive object with only a set of fields', async function () { 344 | 345 | var data = { 346 | name: "restaurantName", 347 | address: "restaurantAddress", 348 | phone: 9492123 349 | }; 350 | 351 | let obj = await new TestModel({data: data}); 352 | 353 | expect(obj.obj).to.have.keys("id", "name", "address", "phone"); 354 | 355 | obj = await new TestModel({data: data, resourceProperties: {_fields: ['id', 'name']}}); 356 | 357 | expect(obj.obj).to.have.keys("id", "name"); 358 | 359 | await obj.patch({ 360 | data: {name: "restaurantName2", address: "Rua Costa Cabral"}, 361 | resourceProperties: {_fields: ['id']} 362 | }); 363 | 364 | expect(obj.obj).to.not.have.keys("id", "name", "address", "phone"); 365 | expect(obj.obj).to.have.keys("id"); 366 | }); 367 | }); 368 | }); -------------------------------------------------------------------------------- /test/requests-hapi.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by GlazedSolutions on 11/03/2015. 3 | */ 4 | 5 | import chai from 'chai'; 6 | import chaiAsPromised from 'chai-as-promised'; 7 | import chaiThings from 'chai-things'; 8 | import Hapi from 'hapi'; 9 | import request from 'request-promise'; 10 | import shortid from 'shortid'; 11 | import 'mochawait'; 12 | import ModelRegister from '../apey-eye/ModelRegister.js'; 13 | 14 | import ApeyEye from '../apey-eye'; 15 | 16 | 17 | let HapiRouter = ApeyEye.HapiRouter; 18 | let HapiGenericRouter = ApeyEye.HapiGenericRouter; 19 | let KoaRouter = ApeyEye.KoaRouter; 20 | let BaseRouter = ApeyEye.BaseRouter; 21 | let Decorators = ApeyEye.Decorators; 22 | let GenericResource = ApeyEye.GenericResource; 23 | let RethinkDBModel = ApeyEye.RethinkDBModel; 24 | let Input = ApeyEye.Input; 25 | let UserModel = ApeyEye.UserModel; 26 | let RoleModel = ApeyEye.RoleModel; 27 | 28 | shortid.characters('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_'); 29 | 30 | chai.use(chaiAsPromised); 31 | chai.should(); 32 | chai.use(chaiThings); 33 | 34 | let expect = chai.expect, 35 | assert = chai.assert, 36 | should = chai.should; 37 | 38 | 39 | let server, 40 | userData; 41 | 42 | describe("hapi", () => { 43 | before((done)=> { 44 | ModelRegister.empty(); 45 | 46 | let restaurantInput = new Input({ 47 | name: {type: "string", required: true}, 48 | dateCreated: {type: "date", default: "now"}, 49 | phone: {type: "number"} 50 | }); 51 | 52 | @Decorators.Input(restaurantInput) 53 | @Decorators.Name("restaurant") 54 | @Decorators.Query({ 55 | _sort: ['name', '-address'], 56 | _page_size: 10 57 | }) 58 | class RestaurantModel extends RethinkDBModel { 59 | } 60 | 61 | @Decorators.Model(RestaurantModel) 62 | @Decorators.Methods(['static.fetch', 'constructor', 'static.fetchOne', 'delete']) 63 | class RestaurantResource extends GenericResource { 64 | @Decorators.Action() 65 | static async get_first() { 66 | let data = {name: "First Restaurant"}; 67 | return data; 68 | } 69 | 70 | @Decorators.Action() 71 | async get_name() { 72 | let obj = this.obj; 73 | this.obj = {name: obj.name}; 74 | return this.obj; 75 | } 76 | 77 | @Decorators.Authentication('basic') 78 | async delete(options) { 79 | return await super.delete(options); 80 | } 81 | @Decorators.Authentication('local') 82 | static async delete(options) { 83 | return true; 84 | } 85 | } 86 | 87 | let router = new HapiGenericRouter(); 88 | router.register([{ 89 | path: 'restaurant', 90 | resource: RestaurantResource 91 | } 92 | ]); 93 | 94 | server = new Hapi.Server({ 95 | connections: { 96 | router: { 97 | stripTrailingSlash: true 98 | } 99 | } 100 | }); 101 | server.connection({port: 3001}); 102 | 103 | let scheme = function (server, options) { 104 | return { 105 | authenticate: function (request, reply) { 106 | return router.checkAuthentication(request, reply); 107 | } 108 | }; 109 | }; 110 | 111 | server.auth.scheme('custom', scheme); 112 | server.auth.strategy('default', 'custom'); 113 | 114 | server.register([ 115 | require('hapi-async-handler') 116 | ], function (err) { 117 | if (err) { 118 | throw err; 119 | } 120 | server.route(router.routes()); 121 | server.start(function () { 122 | //console.log('Server running at:', server.info.uri); 123 | done(); 124 | }); 125 | }); 126 | }); 127 | after((done)=> { 128 | server.stop({timeout: 60 * 1000}, function () { 129 | //console.log('Server stopped'); 130 | done(); 131 | }); 132 | }); 133 | describe('server', ()=> { 134 | describe('Basic', ()=> { 135 | before(async () => { 136 | ModelRegister.register("role", RoleModel); 137 | ModelRegister.register("user", UserModel); 138 | try { 139 | let role = await RoleModel.fetchOne({id: "admin"}); 140 | } 141 | catch (e) { 142 | let role = await new RoleModel({data: {id: "admin"}}); 143 | } 144 | userData = { 145 | username: "userTest" + shortid.generate(), 146 | password: "userPassword", 147 | role: "admin" 148 | }; 149 | await new UserModel({data: userData}); 150 | }); 151 | it("Fetch", async ()=> { 152 | let list = await request({url: server.info.uri + '/restaurant/', json: true}); 153 | expect(list).to.not.be.undefined; 154 | expect(list).to.be.instanceof(Array); 155 | }); 156 | it("POST and Fetch", async ()=> { 157 | let restaurantData = {name: "restaurantName"}; 158 | 159 | let obj = await request.post({ 160 | url: server.info.uri + '/restaurant/', 161 | json: true, 162 | body: restaurantData 163 | }); 164 | expect(obj).to.not.be.undefined; 165 | expect(obj.id).to.not.be.undefined; 166 | expect(obj.name).to.equal(restaurantData.name); 167 | let obj2 = await request.get({ 168 | url: server.info.uri + '/restaurant/' + obj.id, 169 | json: true 170 | }); 171 | expect(obj).to.deep.equal(obj2); 172 | 173 | expect(request.patch({ 174 | url: server.info.uri + '/restaurant/' + obj.id, 175 | json: true 176 | })).to.be.rejected; 177 | }); 178 | it("Test instance action", async ()=> { 179 | let restaurantData = {name: "restaurantName", phone: 123123}; 180 | 181 | let obj = await request.post({ 182 | url: server.info.uri + '/restaurant/', 183 | json: true, 184 | body: restaurantData 185 | }); 186 | 187 | let resultAction = await request.get({ 188 | url: server.info.uri + '/restaurant/' + obj.id + '/name/', 189 | json: true, 190 | body: restaurantData 191 | }); 192 | expect(resultAction).to.deep.equal({name: restaurantData.name}); 193 | }); 194 | it("Test autentication basic fails", async ()=> { 195 | let restaurantData = {name: "restaurantName", phone: 123123}; 196 | 197 | let obj = await request.post({ 198 | url: server.info.uri + '/restaurant/', 199 | json: true, 200 | body: restaurantData 201 | }); 202 | 203 | expect(request.del({ 204 | url: server.info.uri + '/restaurant/' + obj.id 205 | })).to.be.rejected; 206 | 207 | }); 208 | it("Test autentication basic succeeds ", async ()=> { 209 | 210 | let restaurantData = {name: "restaurantName", phone: 123123}; 211 | 212 | let obj = await request.post({ 213 | url: server.info.uri + '/restaurant/', 214 | json: true, 215 | body: restaurantData 216 | }); 217 | 218 | expect(request.del(server.info.uri + '/restaurant/' + obj.id)).to.be.rejected; 219 | expect(request.del(server.info.uri + '/restaurant/' + obj.id).auth(userData.username, "invalidPassword"+shortid.generate(), true)).to.be.rejected; 220 | expect(request.del(server.info.uri + '/restaurant/' + obj.id).auth("invalidUsername"+shortid.generate(), userData.password, true)).to.be.rejected; 221 | expect(request.del(server.info.uri + '/restaurant/' + obj.id).auth(userData.username, userData.password, true)).to.be.fulfilled; 222 | 223 | }); 224 | 225 | it("Test autentication local fails", async ()=> { 226 | let restaurantData = {name: "restaurantName", phone: 123123}; 227 | 228 | let obj = await request.post({ 229 | url: server.info.uri + '/restaurant/', 230 | json: true, 231 | body: restaurantData 232 | }); 233 | 234 | expect(request.del({ 235 | url: server.info.uri + '/restaurant/' 236 | })).to.be.rejected; 237 | 238 | }); 239 | it("Test autentication local succeeds ", async ()=> { 240 | 241 | let restaurantData = {name: "restaurantName", phone: 123123}; 242 | 243 | let obj = await request.post({ 244 | url: server.info.uri + '/restaurant/', 245 | json: true, 246 | body: restaurantData 247 | }); 248 | 249 | expect(request.del({ 250 | uri:server.info.uri + '/restaurant/', 251 | })).to.be.rejected; 252 | 253 | expect(request.del({ 254 | uri:server.info.uri + '/restaurant/', 255 | qs: { 256 | username:shortid.generate(), 257 | password:userData.password 258 | } 259 | })).to.be.rejected; 260 | 261 | expect(request.del({ 262 | uri:server.info.uri + '/restaurant/', 263 | qs: { 264 | username:userData.username, 265 | password:shortid.generate() 266 | } 267 | })).to.be.rejected; 268 | 269 | expect(request.del({ 270 | uri:server.info.uri + '/restaurant/', 271 | qs: { 272 | username:userData.username, 273 | password:userData.password 274 | } 275 | })).to.be.fulfilled; 276 | }); 277 | 278 | }); 279 | 280 | 281 | describe('No Backend', () => { 282 | 283 | it("Fetch", async ()=> { 284 | let resourceName = shortid.generate(); 285 | expect(request({url: server.info.uri + `/${resourceName}/`, json: true})).to.be.rejected; 286 | }); 287 | it("POST and Fetch", async ()=> { 288 | let resourceName = shortid.generate(); 289 | expect(request({url: server.info.uri + `/${resourceName}/`, json: true})).to.be.rejected; 290 | 291 | let restaurantData = {name: "restaurantName"}; 292 | 293 | let obj = await request.post({ 294 | url: server.info.uri + `/${resourceName}/`, 295 | json: true, 296 | body: restaurantData 297 | }); 298 | 299 | expect(obj).to.not.be.undefined; 300 | expect(obj.id).to.not.be.undefined; 301 | expect(obj.name).to.equal(restaurantData.name); 302 | 303 | 304 | let obj2 = await request.get({ 305 | url: server.info.uri + `/${resourceName}/` + obj.id, 306 | json: true 307 | }); 308 | 309 | expect(obj).to.deep.equal(obj2); 310 | 311 | expect(request({url: server.info.uri + `/${resourceName}/`, json: true})).to.be.fulfilled; 312 | 313 | 314 | }); 315 | }); 316 | }); 317 | }); 318 | 319 | -------------------------------------------------------------------------------- /test/router.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by GlazedSolutions on 11/03/2015. 3 | */ 4 | 5 | import chai from 'chai'; 6 | import chaiAsPromised from 'chai-as-promised'; 7 | import chaiThings from 'chai-things'; 8 | 9 | import ModelRegister from '../apey-eye/ModelRegister.js'; 10 | import 'mochawait'; 11 | 12 | import ApeyEye from '../apey-eye'; 13 | 14 | let HapiRouter = ApeyEye.HapiRouter; 15 | let KoaRouter = ApeyEye.KoaRouter; 16 | let BaseRouter = ApeyEye.BaseRouter; 17 | let Decorators = ApeyEye.Decorators; 18 | let GenericResource = ApeyEye.GenericResource; 19 | let RethinkDBModel = ApeyEye.RethinkDBModel; 20 | let Input = ApeyEye.Input; 21 | 22 | chai.use(chaiAsPromised); 23 | chai.should(); 24 | chai.use(chaiThings); 25 | 26 | let expect = chai.expect, 27 | assert = chai.assert, 28 | should = chai.should; 29 | 30 | describe("Router",() => { 31 | 32 | 33 | describe('Base Router', ()=>{ 34 | let TestResource; 35 | 36 | before(function (done) { 37 | ModelRegister.empty(); 38 | 39 | let restaurantInput = new Input({ 40 | name: {type: "string", required: true}, 41 | address: {type: "string", required: true}, 42 | phone: {type: "number"}, 43 | photo: {type: "string", regex: Input.URLPattern}, 44 | date: {type: "date"}, 45 | location: {type: "string"}, 46 | language: {type: "string", choices: ["PT", "EN"]} 47 | }); 48 | 49 | @Decorators.Input(restaurantInput) 50 | @Decorators.Name("restaurant") 51 | @Decorators.Query({ 52 | _sort: ['name', '-address'], 53 | _filter: {name: "name", phone: 20}, 54 | _page_size: 10 55 | }) 56 | @Decorators.Output({ 57 | _fields: ['id', 'name', 'address', 'phone', 'date'], 58 | _embedded: ['schedule', 'products'] 59 | }) 60 | class TestModel extends RethinkDBModel { 61 | } 62 | 63 | @Decorators.Model(TestModel) 64 | @Decorators.Name('testResourceName') 65 | class TestResourceClass extends GenericResource { 66 | } 67 | 68 | TestResource = TestResourceClass; 69 | 70 | done(); 71 | }); 72 | 73 | it("Get Resource Method",()=>{ 74 | expect(BaseRouter.getResourceMethod({params: {id : false}, method: 'POST'}, TestResource)).to.deep.equal(TestResource); 75 | expect(BaseRouter.getResourceMethod({params: {id : true}, method: 'GET'}, TestResource)).to.deep.equal(TestResource.fetchOne); 76 | expect(BaseRouter.getResourceMethod({params: {id : false},method: 'GET'}, TestResource)).to.deep.equal(TestResource.fetch); 77 | expect(BaseRouter.getResourceMethod({params: {id : true}, method: 'DELETE'}, TestResource)).to.deep.equal(TestResource.prototype['delete']); 78 | expect(BaseRouter.getResourceMethod({params: {id : true}, method: 'PATCH'}, TestResource)).to.deep.equal(TestResource.prototype.patch); 79 | }); 80 | it('Parse Request', () => { 81 | let request = { 82 | query: {}, 83 | headers: {} 84 | }; 85 | 86 | expect(BaseRouter.parseRequest(request)).to.deep.equal({ 87 | _filter:undefined, 88 | _sort:undefined, 89 | _pagination: undefined, 90 | _fields: undefined, 91 | _embedded: undefined, 92 | _format:undefined, 93 | _mediaType:undefined 94 | }); 95 | 96 | }); 97 | it('Parse Filters', () => { 98 | let validFilters = "{\"name\":\"restaurantName\"}", 99 | invalidFilters = "{name: \"restaurantName\"}"; 100 | 101 | expect(BaseRouter.parseFilters(validFilters)).to.deep.equal({name:"restaurantName"}); 102 | expect(BaseRouter.parseFilters(invalidFilters)).to.equal(undefined); 103 | 104 | }); 105 | it('Parse Sort', () => { 106 | let validSort = "[\"-name\",\"address\"]", 107 | invalidSort = "[123,\"address\"]", 108 | invalidSort2 = "[123,{name: \"restaurantName\"}]", 109 | invalidSort3 = "{\"name\": \"restaurantName\"}"; 110 | 111 | 112 | expect(BaseRouter.parseSort(validSort)).to.deep.equal([{name:-1},{address:1}]); 113 | expect(BaseRouter.parseSort(invalidSort)).to.equal(undefined); 114 | expect(BaseRouter.parseSort(invalidSort2)).to.equal(undefined); 115 | expect(BaseRouter.parseSort(invalidSort3)).to.equal(undefined); 116 | 117 | }); 118 | it('Parse Pagination', () => { 119 | let validPage = "1", 120 | invalidPage = "{\"page\": 1}", 121 | validPageSize = "10", 122 | invalidPageSize = "{\"pageSize\": 10}"; 123 | 124 | 125 | expect(BaseRouter.parsePagination(validPage,validPageSize)).to.deep.equal({_page:1, _page_size:10}); 126 | expect(BaseRouter.parsePagination(invalidPage,invalidPageSize)).to.equal(undefined); 127 | expect(BaseRouter.parsePagination(undefined,undefined)).to.equal(undefined); 128 | expect(BaseRouter.parsePagination(validPage,undefined)).to.deep.equal({_page:1, _page_size:undefined}); 129 | expect(BaseRouter.parsePagination(undefined,validPageSize)).to.deep.equal({_page:undefined,_page_size:10}); 130 | 131 | }); 132 | it('Parse Fields', () => { 133 | let validFields = "[\"name\",\"address\"]", 134 | invalidFields = "[123,\"address\"]", 135 | invalidFields2 = "[123,{name: \"restaurantName\"}]", 136 | invalidFields3 = "{\"name\": \"restaurantName\"}"; 137 | 138 | 139 | expect(BaseRouter.parseFields(validFields)).to.deep.equal(["name","address"]); 140 | expect(BaseRouter.parseFields(invalidFields)).to.equal(undefined); 141 | expect(BaseRouter.parseFields(invalidFields2)).to.equal(undefined); 142 | expect(BaseRouter.parseFields(invalidFields3)).to.equal(undefined); 143 | 144 | }); 145 | it('Parse Embedded', () => { 146 | let validEmbedded = "[\"name\",\"address\"]", 147 | invalidEmbedded = "[123,\"address\"]", 148 | invalidEmbedded2 = "[123,{name: \"restaurantName\"}]", 149 | invalidEmbedded3 = "{\"name\": \"restaurantName\"}"; 150 | 151 | expect(BaseRouter.parseEmbedded(validEmbedded)).to.deep.equal(["name","address"]); 152 | expect(BaseRouter.parseEmbedded(invalidEmbedded)).to.equal(undefined); 153 | expect(BaseRouter.parseEmbedded(invalidEmbedded2)).to.equal(undefined); 154 | expect(BaseRouter.parseEmbedded(invalidEmbedded3)).to.equal(undefined); 155 | 156 | }); 157 | it('Parse Format', () => { 158 | let validFormat = "application/json", 159 | invalidFormat = {format: "application/json"}; 160 | 161 | expect(BaseRouter.parseFormat(validFormat)).to.equal("application/json"); 162 | expect(BaseRouter.parseFormat(invalidFormat)).to.equal(undefined); 163 | }); 164 | }); 165 | describe('Router Hapi', () =>{ 166 | let TestResource, 167 | router; 168 | before((done) => { 169 | ModelRegister.empty(); 170 | 171 | router = new HapiRouter(); 172 | 173 | let restaurantInput = new Input({ 174 | name: {type: "string", required: true}, 175 | address: {type: "string", required: true}, 176 | phone: {type: "number"}, 177 | photo: {type: "string", regex: Input.URLPattern}, 178 | date: {type: "date"}, 179 | location: {type: "string"}, 180 | language: {type: "string", choices: ["PT", "EN"]} 181 | }); 182 | 183 | @Decorators.Input(restaurantInput) 184 | @Decorators.Name("restaurant") 185 | @Decorators.Query({ 186 | _sort: ['name', '-address'], 187 | _filter: {name: "name", phone: 20}, 188 | _page_size: 10 189 | }) 190 | @Decorators.Output({ 191 | _fields: ['id', 'name', 'address', 'phone', 'date'], 192 | _embedded: ['schedule', 'products'] 193 | }) 194 | class TestModel extends RethinkDBModel { 195 | } 196 | 197 | @Decorators.Model(TestModel) 198 | @Decorators.Name('testResourceName') 199 | class TestResourceClass extends GenericResource { 200 | } 201 | 202 | TestResource = TestResourceClass; 203 | 204 | done(); 205 | }); 206 | beforeEach((done) => { 207 | router.entries = {}; 208 | done() 209 | 210 | }); 211 | 212 | it('Router register errors', ()=>{ 213 | 214 | expect(function () { 215 | router.register(); 216 | }).to.throw(Error); 217 | 218 | expect(function () { 219 | router.register([]); 220 | }).to.not.throw(Error); 221 | 222 | expect(function () { 223 | router.register(TestResource); 224 | }).to.throw(Error); 225 | 226 | expect(function () { 227 | router.register("TestResource"); 228 | }).to.throw(Error); 229 | 230 | expect(function () { 231 | router.register([{ 232 | path: "path", 233 | resource: "TestResource" 234 | }]); 235 | }).to.throw(Error); 236 | 237 | expect(function () { 238 | router.register([{ 239 | path: "path", 240 | resource: TestResource 241 | }]); 242 | }).to.not.throw(Error); 243 | expect(function () { 244 | router.register([{ 245 | resource: TestResource 246 | }]); 247 | }).to.not.throw(Error); 248 | 249 | expect(function () { 250 | router.register([{ 251 | path: 123, 252 | resource: TestResource 253 | }]); 254 | }).to.throw(Error); 255 | expect(function () { 256 | router.register([{ 257 | path: {path: "pathInvalid"}, 258 | resource: TestResource 259 | }]); 260 | }).to.throw(Error); 261 | }); 262 | it('Router register resources', () => { 263 | router.register([{ 264 | resource: TestResource 265 | }]); 266 | 267 | expect(Object.keys(router.entries).length).to.equal(1); 268 | expect(TestResource.getName()).to.equal("testResourceName"); 269 | expect(Object.keys(router.entries)[0]).to.equal('testResourceName'); 270 | expect(router.entries["testResourceName"]).to.deep.equal(TestResource); 271 | }); 272 | }); 273 | describe('Router Koa', () =>{ 274 | let TestResource, 275 | router; 276 | before(function (done) { 277 | ModelRegister.empty(); 278 | 279 | router = new KoaRouter(); 280 | 281 | let restaurantInput = new Input({ 282 | name: {type: "string", required: true}, 283 | address: {type: "string", required: true}, 284 | phone: {type: "number"}, 285 | photo: {type: "string", regex: Input.URLPattern}, 286 | date: {type: "date"}, 287 | location: {type: "string"}, 288 | language: {type: "string", choices: ["PT", "EN"]} 289 | }); 290 | 291 | @Decorators.Input(restaurantInput) 292 | @Decorators.Name("restaurant") 293 | @Decorators.Query({ 294 | _sort: ['name', '-address'], 295 | _filter: {name: "name", phone: 20}, 296 | _page_size: 10 297 | }) 298 | @Decorators.Output({ 299 | _fields: ['id', 'name', 'address', 'phone', 'date'], 300 | _embedded: ['schedule', 'products'] 301 | }) 302 | class TestModel extends RethinkDBModel { 303 | } 304 | 305 | @Decorators.Model(TestModel) 306 | @Decorators.Name('testResourceName') 307 | class TestResourceClass extends GenericResource { 308 | } 309 | 310 | TestResource = TestResourceClass; 311 | 312 | done(); 313 | }); 314 | beforeEach(function (done) { 315 | router.entries = {}; 316 | done() 317 | 318 | }); 319 | 320 | it('Router register errors', function(){ 321 | 322 | expect(function () { 323 | router.register(); 324 | }).to.throw(Error); 325 | 326 | expect(function () { 327 | router.register([]); 328 | }).to.not.throw(Error); 329 | 330 | expect(function () { 331 | router.register(TestResource); 332 | }).to.throw(Error); 333 | 334 | expect(function () { 335 | router.register("TestResource"); 336 | }).to.throw(Error); 337 | 338 | expect(function () { 339 | router.register([{ 340 | path: "path", 341 | resource: "TestResource" 342 | }]); 343 | }).to.throw(Error); 344 | 345 | expect(function () { 346 | router.register([{ 347 | path: "path", 348 | resource: TestResource 349 | }]); 350 | }).to.not.throw(Error); 351 | expect(function () { 352 | router.register([{ 353 | resource: TestResource 354 | }]); 355 | }).to.not.throw(Error); 356 | 357 | expect(function () { 358 | router.register([{ 359 | path: 123, 360 | resource: TestResource 361 | }]); 362 | }).to.throw(Error); 363 | expect(function () { 364 | router.register([{ 365 | path: {path: "pathInvalid"}, 366 | resource: TestResource 367 | }]); 368 | }).to.throw(Error); 369 | }); 370 | it('Router register resources', function(){ 371 | router.register([{ 372 | resource: TestResource 373 | }]); 374 | 375 | expect(Object.keys(router.entries).length).to.equal(1); 376 | expect(TestResource.getName()).to.equal("testResourceName"); 377 | expect(Object.keys(router.entries)[0]).to.equal('testResourceName'); 378 | expect(router.entries["testResourceName"]).to.deep.equal(TestResource); 379 | }); 380 | }); 381 | }); 382 | --------------------------------------------------------------------------------