├── .gitignore ├── package.json ├── src ├── utils │ └── prepare-attributes.js ├── index.js ├── schemas │ ├── middleware.js │ └── data-provider.js ├── users │ ├── data-provider.js │ └── middleware.js └── objects │ ├── data-provider.js │ └── middleware.js ├── LICENSE ├── PATENTS └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | node_modules 4 | npm-debug.log 5 | /lib/ 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "open-parse", 3 | "version": "0.1.2", 4 | "description": "The collection of middleware which provides REST API interface for data and schema access, users and security management.", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "build": "babel src --out-dir lib --source-maps", 8 | "prepublish": "npm run build" 9 | }, 10 | "license": "BSD-3-Clause", 11 | "homepage": "https://github.com/StartupMakers/open-parse", 12 | "bugs": "https://github.com/StartupMakers/open-parse/issues", 13 | "files": [ 14 | "LICENSE", 15 | "PATENTS", 16 | "README.md", 17 | "lib/" 18 | ], 19 | "engines": { 20 | "node": "4.1.1", 21 | "npm": "0.3.x" 22 | }, 23 | "devDependencies": { 24 | "babel": "^5.8.25", 25 | "babel-core": "^5.8.25" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/utils/prepare-attributes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015, Startup Makers, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 'use strict'; 10 | 11 | /** 12 | * Convert reserved dates to ISO format and remove `objectId` field 13 | * @param {Object} data 14 | * @returns {Object} of prepared attributes 15 | */ 16 | export default function prepareAttributes(data) { 17 | const result = Object.assign({}, data); 18 | delete result['objectId']; 19 | // Apply ISO Dates 20 | const keysForISODate = ['createdAt', 'updatedAt', 'deletedAt']; 21 | keysForISODate.forEach(key => { 22 | if (typeof result[key] !== 'undefined') { 23 | const formatted = new Date(result[key]).toISOString(); 24 | if (!isNaN(formatted)) { 25 | result[key] = formatted; 26 | } 27 | } 28 | }); 29 | // Push it back 30 | return result; 31 | } 32 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015, Startup Makers, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 'use strict'; 10 | 11 | import ObjectsDataProvider from './objects/data-provider'; 12 | import SchemasDataProvider from './schemas/data-provider'; 13 | import UsersDataProvider from './users/data-provider'; 14 | import { 15 | handleObjectsList, 16 | handleObjectCreate, 17 | handleObjectFetch, 18 | handleObjectUpdate, 19 | handleObjectDelete 20 | } from './objects/middleware'; 21 | import { 22 | handleSchemaFetch 23 | } from './schemas/middleware'; 24 | import { 25 | handleUserSignUp, 26 | handleUserLogin, 27 | handleUserFetch, 28 | handleUserLogout, 29 | userAuthRequired, 30 | userFetched 31 | } from './users/middleware'; 32 | 33 | export { 34 | ObjectsDataProvider, 35 | handleObjectsList, 36 | handleObjectCreate, 37 | handleObjectFetch, 38 | handleObjectUpdate, 39 | handleObjectDelete, 40 | 41 | SchemasDataProvider, 42 | handleSchemaFetch, 43 | 44 | UsersDataProvider, 45 | handleUserSignUp, 46 | handleUserLogin, 47 | handleUserFetch, 48 | handleUserLogout, 49 | userAuthRequired, 50 | userFetched 51 | }; 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD License 2 | For Open Parse software 3 | 4 | Copyright (c) 2015, Startup Makers, Inc. 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without modification, 8 | are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | * Neither the name Startup Makers nor the names of its contributors may be used to 18 | endorse or promote products derived from this software without specific 19 | prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 22 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 23 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 25 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 26 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 28 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /src/schemas/middleware.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015, Startup Makers, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 'use strict'; 10 | 11 | import prepareAttributes from '../utils/prepare-attributes'; 12 | 13 | export function handleSchemaFetch({ dataProvider, logger }) { 14 | return function *() { 15 | const errors = []; 16 | const { params } = this; 17 | if (logger) { 18 | logger.debug('handleSchemaFetch()', params); 19 | } 20 | const className = params['className']; 21 | if (typeof className === 'undefined' || className === '') { 22 | errors.push({ 23 | 'title': 'required field is missing', 24 | 'source': { 25 | 'parameter': 'className' 26 | } 27 | }); 28 | } 29 | if (errors.length === 0) { 30 | const fetched = yield dataProvider.fetch(className, { 31 | 'limit': 1 32 | }); 33 | if (Array.isArray(fetched)) { 34 | if (fetched.length > 0) { 35 | this.body = { 36 | 'data': { 37 | 'type': 'schema', 38 | 'id': className, 39 | 'attributes': prepareAttributes(fetched[0]) 40 | } 41 | }; 42 | return; 43 | } 44 | errors.push({ 45 | 'title': 'entry is not found', 46 | 'source': { 47 | 'parameter': 'className' 48 | } 49 | }); 50 | } else { 51 | errors.push({ 52 | 'title': 'could not fetch schema', 53 | 'source': { 54 | 'parameter': 'className' 55 | } 56 | }); 57 | } 58 | } 59 | this.body = { 60 | 'errors': errors 61 | }; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /PATENTS: -------------------------------------------------------------------------------- 1 | Additional Grant of Patent Rights Version 2 2 | 3 | "Software" means the Open Parse software distributed by Startup Makers, Inc. 4 | 5 | Startup Makers, Inc. ("Startup Makers") hereby grants to each recipient of 6 | the Software ("you") a perpetual, worldwide, royalty-free, non-exclusive, 7 | irrevocable (subject to the termination provision below) license under any 8 | Necessary Claims, to make, have made, use, sell, offer to sell, import, and 9 | otherwise transfer the Software. For avoidance of doubt, no license is granted 10 | under Startup Makers's rights in any patent claims that are infringed by (i) 11 | modifications to the Software made by you or any third party or (ii) the Software 12 | in combination with any software or other technology. 13 | 14 | The license granted hereunder will terminate, automatically and without notice, 15 | if you (or any of your subsidiaries, corporate affiliates or agents) initiate 16 | directly or indirectly, or take a direct financial interest in, any Patent 17 | Assertion: (i) against Startup Makers or any of its subsidiaries or corporate 18 | affiliates, (ii) against any party if such Patent Assertion arises in whole or 19 | in part from any software, technology, product or service of Startup Makers or 20 | any of its subsidiaries or corporate affiliates, or (iii) against any party 21 | relating to the Software. Notwithstanding the foregoing, if Startup Makers or 22 | any of its subsidiaries or corporate affiliates files a lawsuit alleging patent 23 | infringement against you in the first instance, and you respond by filing a 24 | patent infringement counterclaim in that lawsuit against that party that is 25 | unrelated to the Software, the license granted hereunder will not terminate 26 | under section (i) of this paragraph due to such counterclaim. 27 | 28 | A "Necessary Claim" is a claim of a patent owned by Startup Makers that is 29 | necessarily infringed by the Software standing alone. 30 | 31 | A "Patent Assertion" is any lawsuit or other action alleging direct, indirect, 32 | or contributory infringement or inducement to infringe any patent, including a 33 | cross-claim or counterclaim. 34 | -------------------------------------------------------------------------------- /src/schemas/data-provider.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015, Startup Makers, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 'use strict'; 10 | 11 | export default class SchemasDataProvider { 12 | /** 13 | * @param {Object} collection is MongoDB collection 14 | * @param {Object} [logger] 15 | * @param {Object} [initialCache] a cache hash in format `cache[className]` 16 | */ 17 | constructor({ collection, logger, initialCache }) { 18 | this.collection = collection; 19 | this.logger = logger; 20 | this._cache = Object.assign({}, initialCache); 21 | } 22 | 23 | /** 24 | * @param {Object|String} criteria is filter set or className 25 | * @param {String} [criteria.className] 26 | * @param {Object} [options] custom params 27 | * @param {Boolean} [options.cache=true] 28 | * @param {Number} [options.skip=0] 29 | * @param {Number} [options.limit=100] 30 | * @returns {Array.<*>} 31 | */ 32 | async fetchObjects(criteria, options) { 33 | let finalOptions = Object.assign({ 34 | cache: true, 35 | skip: 0, 36 | limit: 100 37 | }, options); 38 | let finalCriteria; 39 | if (typeof criteria === 'string') { 40 | finalCriteria = { 41 | 'className': criteria 42 | }; 43 | } else { 44 | finalCriteria = Object.assign({}, criteria); 45 | } 46 | // try to fetch from local cache 47 | if (finalOptions.cache && finalCriteria['className']) { 48 | const className = finalCriteria['className']; 49 | if (this._cache[className]) { 50 | return [this._cache[className]]; 51 | } 52 | } 53 | // otherwise fetch from database 54 | if (typeof finalCriteria['objectId'] !== 'undefined') { 55 | try { 56 | finalCriteria['_id'] = this.collection.ObjectId(finalCriteria['objectId']); 57 | } catch (err) { 58 | if (this.logger) { 59 | this.logger.error('could not parse `objectId` in fetchObjects()', { 60 | 'objectId': finalCriteria['objectId'], 61 | 'error': err 62 | }); 63 | } 64 | return false; 65 | } 66 | delete finalCriteria['objectId']; 67 | } 68 | if (this.logger) { 69 | this.logger.debug('find by fetchObjects()', { 70 | 'criteria': finalCriteria, 71 | 'options': finalOptions 72 | }); 73 | } 74 | let result; 75 | try { 76 | const cursor = await this.collection.find(finalCriteria); 77 | result = cursor.limit(finalOptions.limit) 78 | .skip(finalOptions.skip) 79 | .toArray(); 80 | if (this.logger) { 81 | this.logger.debug('found by fetchObjects()', { 82 | 'count': result.length 83 | }); 84 | } 85 | } catch (err) { 86 | if (this.logger) { 87 | this.logger.error('could not find by fetchObjects()', { 88 | 'criteria': finalCriteria, 89 | 'error': err 90 | }); 91 | } 92 | } 93 | if (typeof result !== 'undefined') { 94 | return result; 95 | } 96 | return false; 97 | } 98 | 99 | /** 100 | * @param {String} className 101 | * @returns {Object|Boolean} data or false 102 | */ 103 | async fetchObject(className) { 104 | const found = await this.fetch({ 105 | 'className': className 106 | }, { 107 | 'limit': 1 108 | }); 109 | if (typeof found === 'object' && found[0]) { 110 | return found[0]; 111 | } 112 | return false; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/users/data-provider.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015, Startup Makers, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 'use strict'; 10 | import bcrypt from 'bcrypt'; 11 | 12 | function encodePassword(password) { 13 | return bcrypt.hashSync(password, 10); 14 | } 15 | function comparePassword(password, hash) { 16 | return bcrypt.compareSync(password, hash); 17 | } 18 | 19 | export default class UsersDataProvider { 20 | /** 21 | * @param {Object} collection like MongoDB 22 | * @param {Object} logger 23 | * @param {Object} [initialCache] a cache hash in format `cache[className]` 24 | */ 25 | constructor({ logger, collection, initialCache }) { 26 | this.logger = logger; 27 | this.collection = collection; 28 | this._cache = Object.assign({}, initialCache); 29 | } 30 | 31 | /** 32 | * @param {Object} data 33 | * @returns {{objectId: number, createdAt: number}|boolean} 34 | */ 35 | async insert(data) { 36 | const createdTime = Date.now(); 37 | const document = Object.assign({}, data, { 38 | 'createdAt': createdTime, 39 | 'password': encodePassword(data['password']) 40 | }); 41 | if (this.logger) { 42 | this.logger.debug('try to insert by insert()', document); 43 | } 44 | let created; 45 | try { 46 | created = await this.collection.insert(document); 47 | } catch (err) { 48 | if (this.logger) { 49 | this.logger.error('could not insert by create()', { 50 | 'document': document, 51 | 'errors': err 52 | }); 53 | } 54 | } 55 | if (typeof created === 'object') { 56 | return { 57 | objectId: created['_id'], 58 | createdAt: createdTime 59 | }; 60 | } 61 | return false; 62 | } 63 | 64 | /** 65 | * @param {object} criteria 66 | * @param {string} [criteria.objectId] 67 | * @param {string} [criteria.password] 68 | * @returns {object|boolean} data or `false` 69 | */ 70 | async findOne(criteria) { 71 | const finalCriteria = Object.assign({}, criteria); 72 | if (typeof finalCriteria['objectId'] !== 'undefined') { 73 | finalCriteria['_id'] = this.collection.ObjectId(finalCriteria['objectId']); 74 | delete finalCriteria['objectId']; 75 | } 76 | let password; 77 | if (typeof finalCriteria['password'] !== 'undefined') { 78 | password = finalCriteria['password']; 79 | delete finalCriteria['password']; 80 | } 81 | if (this.logger) { 82 | this.logger.debug('try to fetch by findOne()', { 83 | 'criteria': finalCriteria 84 | }); 85 | } 86 | const foundUser = await this.collection.findOne(finalCriteria); 87 | if (foundUser && typeof password !== 'undefined') { 88 | const isValidPassword = await comparePassword(password, foundUser['password']); 89 | if (!isValidPassword) { 90 | if (this.logger) { 91 | this.logger.debug('specified password is not valid', { 92 | 'password': password, 93 | 'original': foundUser['password'] 94 | }); 95 | } 96 | return false; 97 | } 98 | } 99 | if (foundUser === null) { 100 | return false; 101 | } 102 | const result = Object.assign({}, foundUser); 103 | if (typeof result['_id'] !== 'undefined') { 104 | result['objectId'] = result['_id']; 105 | delete result['_id']; 106 | } 107 | return result; 108 | } 109 | 110 | /** 111 | * @param {object} criteria 112 | * @param {string} [criteria.objectId] 113 | * @param {object} changes 114 | * @returns {{updatedAt: number}|boolean} 115 | */ 116 | async update(criteria, changes) { 117 | const updatedTime = Date.now(); 118 | const finalCriteria = Object.assign({}, criteria); 119 | if (typeof finalCriteria['objectId'] !== 'undefined') { 120 | finalCriteria['_id'] = this.collection.ObjectId(finalCriteria['objectId']); 121 | delete finalCriteria['objectId']; 122 | } 123 | const finalChanges = Object.assign({}, changes, { 124 | 'updatedAt': updatedTime 125 | }); 126 | if (this.logger) { 127 | this.logger.debug('try to update by update()', { 128 | 'criteria': finalCriteria, 129 | 'changes': finalChanges 130 | }); 131 | } 132 | let writeResult; 133 | try { 134 | writeResult = await this.collection.update(finalCriteria, { 135 | '$set': finalChanges 136 | }, { multi: true }); 137 | } catch (err) { 138 | if (this.logger) { 139 | this.logger.debug('could not update by update()', { 140 | 'criteria': finalCriteria, 141 | 'error': err 142 | }); 143 | } 144 | } 145 | if (typeof writeResult !== 'undefined') { 146 | return { 147 | updatedAt: updatedTime 148 | }; 149 | } else { 150 | return false; 151 | } 152 | } 153 | }; 154 | -------------------------------------------------------------------------------- /src/objects/data-provider.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015, Startup Makers, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 'use strict'; 10 | 11 | export default class ObjectsDataProvider { 12 | /** 13 | * @param {Object} collection is MongoDB collection 14 | * @param {Object} [logger] 15 | * @param {Object} [initialCache] a cache hash in format `cache[className][objectId]` 16 | */ 17 | constructor({ collection, logger, initialCache }) { 18 | this.collection = collection; 19 | this.logger = logger; 20 | this._cache = Object.assign({}, initialCache); 21 | } 22 | 23 | /** 24 | * @param {string} className 25 | * @param {object} data 26 | * @returns {{objectId: number, createdAt: number}|boolean} 27 | */ 28 | async createObject(className, data) { 29 | const createdTime = Date.now(); 30 | const document = Object.assign({}, data, { 31 | 'className': className, 32 | 'createdAt': createdTime 33 | }); 34 | if (this.logger) { 35 | this.logger.debug('insert by createObject()', { 36 | 'document': document 37 | }); 38 | } 39 | let created; 40 | try { 41 | created = await this.collection.insert(document); 42 | } catch (err) { 43 | if (this.logger) { 44 | this.logger.error('could not insert by createObject()', { 45 | 'document': document, 46 | 'error': err 47 | }); 48 | } 49 | } 50 | if (typeof created === 'object') { 51 | return { 52 | 'objectId': created['_id'], 53 | 'createdAt': createdTime 54 | }; 55 | } 56 | return false; 57 | } 58 | 59 | /** 60 | * @param {object|string} criteria is filter set or className 61 | * @param {string} [criteria.className] 62 | * @param {string} [criteria.objectId] 63 | * @param {object} [options] custom options or object id 64 | * @param {boolean} [options.cache=true] 65 | * @param {number} [options.skip=0] 66 | * @param {number} [options.limit=100] 67 | * @returns {Array.<*>|boolean} 68 | */ 69 | async fetchObjects(criteria, options) { 70 | const finalOptions = { 71 | cache: true, 72 | skip: 0, 73 | limit: 100 74 | }; 75 | let finalCriteria; 76 | if (typeof arguments[0] === 'string') { 77 | finalCriteria = { 78 | 'className': arguments[0], 79 | 'objectId': arguments[1] 80 | }; 81 | } else { 82 | finalCriteria = Object.assign({}, criteria); 83 | Object.assign(finalOptions, options); 84 | } 85 | // try to fetch data from local cache and return it 86 | if (finalOptions.cache && finalCriteria['className'] && finalCriteria['objectId']) { 87 | const className = finalCriteria['className']; 88 | const objectId = finalCriteria['objectId']; 89 | if (this._cache[className] && this._cache[className][objectId]) { 90 | return [this._cache[className][objectId]]; 91 | } 92 | } 93 | // otherwise fetch data from database 94 | if (typeof finalCriteria['objectId'] !== 'undefined') { 95 | try { 96 | finalCriteria['_id'] = this.collection.ObjectId(finalCriteria['objectId']); 97 | } catch (err) { 98 | if (this.logger) { 99 | this.logger.error('could not parse `objectId` in fetchObjects()', { 100 | 'objectId': finalCriteria['objectId'], 101 | 'error': err 102 | }); 103 | } 104 | return false; 105 | } 106 | delete finalCriteria['objectId']; 107 | } 108 | if (this.logger) { 109 | this.logger.debug('find by fetchObjects()', { 110 | 'criteria': finalCriteria, 111 | 'options': finalOptions 112 | }); 113 | } 114 | let result; 115 | try { 116 | result = await this.collection.find(finalCriteria) 117 | .limit(finalOptions['limit']) 118 | .skip(finalOptions['skip']) 119 | .toArray(); 120 | // Reduce as mutable data to optimize it 121 | result.forEach((it) => { 122 | it['objectId'] = it['_id']; 123 | delete it['_id']; 124 | }); 125 | if (this.logger) { 126 | this.logger.debug('found by fetchObjects()', { 127 | count: result.length 128 | }); 129 | } 130 | } catch (err) { 131 | if (this.logger) { 132 | this.logger.error('could not find by fetchObjects()', { 133 | 'criteria': finalCriteria, 134 | 'error': err 135 | }); 136 | } 137 | } 138 | if (typeof result !== 'undefined') { 139 | return result; 140 | } 141 | return false; 142 | } 143 | 144 | /** 145 | * @param {String} className 146 | * @param {String} objectId 147 | * @returns {Object|Boolean} data or false 148 | */ 149 | async fetchObject(className, objectId) { 150 | const found = await this.fetchObjects({ 151 | 'className': className, 152 | 'objectId': objectId 153 | }, { 154 | 'limit': 1 155 | }); 156 | if (typeof found === 'object' && found[0]) { 157 | return found[0]; 158 | } 159 | return false; 160 | } 161 | 162 | /** 163 | * @param {Object} criteria 164 | * @param {String} [criteria.className] 165 | * @param {String} [criteria.objectId] 166 | * @param {Object} data 167 | * @returns {{updatedAt: number}|Boolean} 168 | */ 169 | async updateObject(criteria, data) { 170 | const updatedTime = Date.now(); 171 | const finalCriteria = Object.assign({}, criteria); 172 | if (typeof finalCriteria['objectId'] !== 'undefined') { 173 | finalCriteria['_id'] = this.collection.ObjectId(finalCriteria['objectId']); 174 | delete finalCriteria['objectId']; 175 | } 176 | const changes = Object.assign({}, data, { 177 | 'updatedAt': updatedTime 178 | }); 179 | if (this.logger) { 180 | this.logger.debug('update by updateObject()', { 181 | 'criteria': finalCriteria, 182 | 'changes': changes 183 | }); 184 | } 185 | let writeResult; 186 | try { 187 | writeResult = await this.collection.update(finalCriteria, { 188 | '$set': changes 189 | }, { multi: true }); 190 | } catch (err) { 191 | if (this.logger) { 192 | this.logger.error('could not update by updateObject()', { 193 | 'criteria': finalCriteria, 194 | 'changes': changes, 195 | 'error': err 196 | }); 197 | } 198 | } 199 | if (typeof writeResult !== 'undefined') { 200 | return { 201 | updatedAt: updatedTime 202 | }; 203 | } 204 | return false; 205 | } 206 | 207 | /** 208 | * @param {Object} params 209 | * @param {String} params.className 210 | * @param {String} params.objectId 211 | * @returns {{updatedAt: Number}|Boolean} 212 | */ 213 | async deleteObject({ className, objectId }) { 214 | const deletedTime = Date.now(); 215 | const criteria = { 216 | 'className': className, 217 | 'objectId': objectId 218 | }; 219 | const updated = await this.updateObject(criteria, { 220 | 'deletedAt': deletedTime 221 | }); 222 | if (typeof updated === 'object' && updated['updatedAt']) { 223 | return { 224 | 'deletedAt': deletedTime 225 | } 226 | } 227 | return false; 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /src/users/middleware.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015, Startup Makers, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 'use strict'; 10 | 11 | /** 12 | * @param {Object} dataProvider 13 | * @param {Object} [logger] 14 | * @param [loginField=email] 15 | * @param [loginIfSuccess=true] 16 | * @returns {Function} 17 | */ 18 | export function handleUserSignUp({ dataProvider, logger, loginField, loginIfSuccess }) { 19 | return function *() { 20 | const body = this.request['body']; 21 | const request = (body['data'] && body['data']['attributes']) || {}; 22 | const errors = []; 23 | // Validate request attributes 24 | // @todo yield schemasCollection.fetchOne('UserRegistration'); 25 | //if (!attributes['name']) { 26 | // errors.push({ 27 | // 'title': 'Please fill out this field', 28 | // 'source': { 29 | // 'parameter': 'name' 30 | // } 31 | // }); 32 | //} 33 | const finalLoginField = loginField || 'email'; 34 | if (!request[finalLoginField]) { 35 | errors.push({ 36 | 'title': 'Required field is missing', 37 | 'source': { 38 | 'parameter': finalLoginField 39 | } 40 | }); 41 | } 42 | if (!request['password']) { 43 | errors.push({ 44 | 'title': 'Required field is missing', 45 | 'source': { 46 | 'parameter': 'password' 47 | } 48 | }); 49 | } 50 | if (errors.length === 0) { 51 | let createdUser; 52 | try { 53 | const document = Object.assign({}, request); 54 | createdUser = yield dataProvider.insert(document); 55 | if (logger) { 56 | logger.debug('created new user by handleUserLogin()', createdUser); 57 | } 58 | } catch (err) { 59 | if (logger) { 60 | logger.error('failed to create new user by handleUserLogin()', { 61 | 'error': err 62 | }); 63 | } 64 | } 65 | 66 | if (typeof createdUser === 'object') { 67 | if (loginIfSuccess !== false) { 68 | this.session['userId'] = createdUser['objectId']; 69 | } 70 | this.status = 201; 71 | this.body = { 72 | 'data': [{ 73 | 'type': 'users', 74 | 'id': createdUser['objectId'], 75 | 'attributes': { 76 | 'created_at': (new Date(createdUser['createdAt'])).toISOString() 77 | } 78 | }] 79 | }; 80 | //this.redirect('/users/' + createdUser['id']); 81 | return; 82 | } 83 | 84 | errors.push({ 85 | 'title': 'The user is already existing', 86 | 'source': { 87 | 'parameter': finalLoginField 88 | } 89 | }); 90 | } 91 | this.body = { 92 | 'errors': errors 93 | }; 94 | }; 95 | } 96 | 97 | /** 98 | * @param {Object} dataProvider 99 | * @param {String} loginField 100 | * @returns {Function} 101 | */ 102 | export function handleUserLogin({ dataProvider, loginField }) { 103 | return function *() { 104 | const errors = []; 105 | const { query } = this; 106 | if (typeof query[loginField] !== 'string' || query[loginField].trim() === '') { 107 | errors.push({ 108 | 'title': 'Please fill out this field', 109 | 'source': { 110 | 'parameter': loginField 111 | } 112 | }); 113 | } 114 | if (typeof query['password'] !== 'string' || query['password'].trim() === '') { 115 | errors.push({ 116 | 'title': 'Please fill out this field', 117 | 'source': { 118 | 'parameter': 'password' 119 | } 120 | }); 121 | } 122 | if (errors.length === 0) { 123 | const foundUser = yield dataProvider.findOne({ 124 | 'email': query['email'], 125 | 'password': query['password'] 126 | }); 127 | 128 | if (typeof foundUser === 'object' && foundUser !== null) { 129 | const userId = foundUser['objectId']; 130 | this.session['userId'] = userId; 131 | 132 | // Prepare fetched data for response 133 | const attributes = Object.assign({}, foundUser); 134 | delete attributes['objectId']; 135 | delete attributes['password']; 136 | 137 | this.body = { 138 | 'data': { 139 | 'type': 'users', 140 | 'id': userId, 141 | 'attributes': attributes 142 | } 143 | }; 144 | return; 145 | } 146 | 147 | errors.push({ 148 | 'title': 'Invalid login or password', 149 | 'source': { 150 | 'parameter': 'email' 151 | } 152 | }); 153 | } 154 | this.body = { 155 | 'errors': errors 156 | }; 157 | }; 158 | } 159 | 160 | /** 161 | * Fetch user specified in params['id'] or just current user 162 | * @param {Object} dataProvider 163 | * @returns {Function} 164 | */ 165 | export function handleUserFetch({ dataProvider }) { 166 | return function *() { 167 | const errors = []; 168 | const { params, session } = this; 169 | const userId = params['id'] || (session && session['userId']); 170 | if (userId) { 171 | const foundUser = yield dataProvider.findOne({ 172 | 'objectId': userId 173 | }); 174 | if (typeof foundUser === 'object' && foundUser !== null) { 175 | const attributes = Object.assign({}, foundUser); 176 | delete attributes['objectId']; 177 | delete attributes['password']; 178 | 179 | this.body = { 180 | 'data': { 181 | 'type': 'users', 182 | 'id': this.session['userId'], 183 | 'attributes': attributes 184 | } 185 | }; 186 | return; 187 | } 188 | errors.push({ 189 | 'title': 'The user is not found' 190 | }); 191 | } else { 192 | errors.push({ 193 | 'title': 'The user is not logged in' 194 | }); 195 | } 196 | this.body = { 197 | 'errors': errors 198 | }; 199 | }; 200 | } 201 | 202 | /** 203 | * @returns {Function} 204 | */ 205 | export function handleUserLogout() { 206 | return function *() { 207 | const { session } = this; 208 | if (session && session['userId']) { 209 | const userId = session['userId']; 210 | this.session = null; 211 | this.body = { 212 | 'data': { 213 | 'type': 'users', 214 | 'id': userId 215 | } 216 | }; 217 | return; 218 | } 219 | this.body = { 220 | 'errors': [{ 221 | 'title': 'The user is not logged in yet' 222 | }] 223 | }; 224 | } 225 | } 226 | 227 | /** 228 | * @param {Object} dataProvider 229 | * @returns {Function} 230 | */ 231 | export function userFetched({ dataProvider }) { 232 | return function *() { 233 | const { session } = this; 234 | if (session && session['userId']) { 235 | const foundUser = yield dataProvider.findOne({ 236 | 'objectId': session['userId'] 237 | }); 238 | const me = Object.assign({}, foundUser); 239 | delete me['password']; 240 | this.me = me; 241 | } else { 242 | this.me = {}; 243 | } 244 | }; 245 | } 246 | 247 | /** 248 | * @param {String} [loginView] i.e. 'login-page' 249 | * @param {String} [loginURL] i.e. '/login' 250 | * @returns {Function} 251 | */ 252 | export function userAuthRequired({ loginView, loginURL }) { 253 | return function *() { 254 | if (typeof this.session['userId']) { 255 | yield next; 256 | } else { 257 | if (loginView) { 258 | yield this.render(loginView, { 259 | 'forwardURL': this.request.url 260 | }); 261 | } 262 | if (loginURL) { 263 | this.redirect(loginURL); 264 | } 265 | } 266 | }; 267 | } 268 | -------------------------------------------------------------------------------- /src/objects/middleware.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015, Startup Makers, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 'use strict'; 10 | 11 | import prepareAttributes from '../utils/prepare-attributes'; 12 | 13 | const RESOURCE_TYPE_PREFIX = 'object_'; 14 | 15 | /** 16 | * @param {Object} dataProvider 17 | * @param {Object} [logger] 18 | * @returns {Function} 19 | */ 20 | export function handleObjectsList({ dataProvider, logger }) { 21 | return function *() { 22 | const errors = []; 23 | const { params } = this; 24 | if (logger) { 25 | logger.debug('handleObjectsList()', params); 26 | } 27 | const className = params['className']; 28 | if (typeof className === 'undefined' || className === '') { 29 | errors.push({ 30 | 'title': 'Required field is missing', 31 | 'source': { 32 | 'parameter': 'className' 33 | } 34 | }); 35 | } 36 | if (errors.length === 0) { 37 | const criteria = {}; 38 | const { query } = this; 39 | if (typeof query['filter'] === 'object') { 40 | const filter = query['filter']; 41 | Object.keys(filter).forEach(key => { 42 | if (key.indexOf('$') === -1) { 43 | criteria[key] = filter[key]; 44 | } 45 | }); 46 | } 47 | criteria['className'] = className; 48 | const fetched = yield dataProvider.fetchObjects(criteria); 49 | if (Array.isArray(fetched)) { 50 | this.body = { 51 | 'data': fetched.map(it => { 52 | return { 53 | 'type': RESOURCE_TYPE_PREFIX + className, 54 | 'id': it['objectId'], 55 | 'attributes': prepareAttributes(it) 56 | }; 57 | }) 58 | }; 59 | return; 60 | } 61 | errors.push({ 62 | 'title': 'could not fetch objects', 63 | 'source': { 64 | 'parameter': 'className' 65 | } 66 | }); 67 | } 68 | this.body = { 69 | 'errors': errors 70 | }; 71 | } 72 | } 73 | 74 | /** 75 | * @param {Object} dataProvider 76 | * @param {Object} [logger] 77 | * @returns {Function} 78 | */ 79 | export function handleObjectCreate({ dataProvider, logger }) { 80 | return function *() { 81 | const errors = []; 82 | const body = this.request['body']; 83 | const { params } = this; 84 | if (logger) { 85 | logger.debug('handleObjectCreate()', params); 86 | } 87 | const incomingAttributes = (body['data'] && body['data']['attributes']) || {}; 88 | const className = params['className']; 89 | if (typeof className === 'undefined' || className === '') { 90 | errors.push({ 91 | 'title': 'required field is not specified', 92 | 'source': { 93 | 'parameter': 'className' 94 | } 95 | }); 96 | } 97 | if (errors.length === 0) { 98 | const data = Object.assign({}, incomingAttributes, { 99 | 'createdBy': this.session['userId'] 100 | }); 101 | const created = yield dataProvider.createObject(className, data); 102 | if (typeof created === 'object') { 103 | this.body = { 104 | 'data': { 105 | 'type': RESOURCE_TYPE_PREFIX + className, 106 | 'id': created['objectId'], 107 | 'attributes': prepareAttributes(created) 108 | } 109 | }; 110 | return; 111 | } 112 | errors.push({ 113 | 'title': 'could not create object', 114 | 'source': { 115 | 'parameter': 'objectId' 116 | } 117 | }); 118 | } 119 | this.body = { 120 | 'errors': errors 121 | }; 122 | } 123 | } 124 | 125 | /** 126 | * @param {Object} dataProvider 127 | * @param {Object} [logger] 128 | * @returns {Function} 129 | */ 130 | export function handleObjectFetch({ dataProvider, logger }) { 131 | return function *() { 132 | const errors = []; 133 | const { params } = this; 134 | if (logger) { 135 | logger.debug('handleObjectUpdate()', params); 136 | } 137 | const className = params['className']; 138 | if (typeof className === 'undefined' || className === '') { 139 | errors.push({ 140 | 'title': 'required field is missing', 141 | 'source': { 142 | 'parameter': 'className' 143 | } 144 | }); 145 | } 146 | const objectId = params['objectId']; 147 | if (typeof objectId === 'undefined' || objectId === '') { 148 | errors.push({ 149 | 'title': 'required field is missing', 150 | 'source': { 151 | 'parameter': 'objectId' 152 | } 153 | }); 154 | } 155 | if (errors.length === 0) { 156 | const fetched = yield dataProvider.fetchObjects({ 157 | 'className': className, 158 | 'objectId': objectId 159 | }, { 160 | 'limit': 1 161 | }); 162 | if (Array.isArray(fetched)) { 163 | if (fetched.length > 0) { 164 | this.body = { 165 | 'data': { 166 | 'type': RESOURCE_TYPE_PREFIX + className, 167 | 'id': fetched[0]['objectId'], 168 | 'attributes': prepareAttributes(fetched[0]) 169 | } 170 | }; 171 | return; 172 | } 173 | errors.push({ 174 | 'title': 'entry is not found', 175 | 'source': { 176 | 'parameter': 'objectId' 177 | } 178 | }); 179 | } else { 180 | errors.push({ 181 | 'title': 'could not fetch object', 182 | 'source': { 183 | 'parameter': 'objectId' 184 | } 185 | }); 186 | } 187 | } 188 | this.body = { 189 | 'errors': errors 190 | }; 191 | } 192 | } 193 | 194 | /** 195 | * @param {Object} dataProvider 196 | * @param {Object} [logger] 197 | * @returns {Function} 198 | */ 199 | export function handleObjectUpdate({ dataProvider, logger }) { 200 | return function *() { 201 | const errors = []; 202 | const body = this.request['body']; 203 | const { params } = this; 204 | if (logger) { 205 | logger.debug('handleObjectUpdate()', params); 206 | } 207 | const incomingAttributes = (body['data'] && body['data']['attributes']) || {}; 208 | const className = params['className']; 209 | if (typeof className === 'undefined' || className === '') { 210 | errors.push({ 211 | 'title': 'required field is missing', 212 | 'source': { 213 | 'parameter': 'className' 214 | } 215 | }); 216 | } 217 | const objectId = params['objectId']; 218 | if (typeof objectId === 'undefined' || objectId === '') { 219 | errors.push({ 220 | 'title': 'required field is missing', 221 | 'source': { 222 | 'parameter': 'objectId' 223 | } 224 | }); 225 | } 226 | if (errors.length === 0) { 227 | const updated = yield dataProvider.update({ 228 | 'className': className, 229 | 'objectId': objectId 230 | }, incomingAttributes); 231 | if (typeof updated === 'object' && updated !== null) { 232 | this.body = { 233 | 'data': { 234 | 'type': RESOURCE_TYPE_PREFIX + className, 235 | 'id': updated['objectId'], 236 | 'attributes': prepareAttributes(updated) 237 | } 238 | }; 239 | return; 240 | } 241 | errors.push({ 242 | 'title': 'could not update object', 243 | 'source': { 244 | 'parameter': 'objectId' 245 | } 246 | }); 247 | } 248 | this.body = { 249 | 'errors': errors 250 | }; 251 | } 252 | } 253 | 254 | /** 255 | * @param {Object} dataProvider 256 | * @param {Object} [logger] 257 | * @returns {Function} 258 | * @APIBlueprint 259 | * + Response 200 (application/json) 260 | * { 261 | * "data": { 262 | * "type": "object", 263 | * "attributes": { 264 | * "deletedAt": "2015-06-11T08:40:51.620Z" 265 | * } 266 | * } 267 | * } 268 | */ 269 | export function handleObjectDelete({ dataProvider, logger }) { 270 | return function *() { 271 | const errors = []; 272 | const { params } = this; 273 | if (logger) { 274 | logger.debug('handleObjectDelete()', params); 275 | } 276 | const className = params['className']; 277 | if (typeof className === 'undefined' || className === '') { 278 | errors.push({ 279 | 'title': 'required field is missing', 280 | 'source': { 281 | 'parameter': 'className' 282 | } 283 | }); 284 | } 285 | const objectId = params['objectId']; 286 | if (typeof objectId === 'undefined' || objectId === '') { 287 | errors.push({ 288 | 'title': 'required field is missing', 289 | 'source': { 290 | 'parameter': 'objectId' 291 | } 292 | }); 293 | } 294 | if (errors.length === 0) { 295 | const deleted = yield dataProvider.deleteObject({ 296 | 'className': className, 297 | 'objectId': objectId 298 | }); 299 | if (typeof deleted === 'object') { 300 | this.body = { 301 | 'data': { 302 | 'type': RESOURCE_TYPE_PREFIX + className, 303 | 'attributes': prepareAttributes(deleted) 304 | } 305 | }; 306 | return; 307 | } 308 | errors.push({ 309 | 'title': 'could not delete object', 310 | 'source': { 311 | 'parameter': 'objectId' 312 | } 313 | }); 314 | } 315 | this.body = { 316 | 'errors': errors 317 | }; 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Open Parse 2 | 3 | [![NPM Version](https://img.shields.io/npm/v/open-parse.svg)](https://npmjs.org/package/open-parse) 4 | [![NPM Downloads](https://img.shields.io/npm/dm/open-parse.svg)](https://npmjs.org/package/open-parse) 5 | [![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/StartupMakers/open-parse.svg)](http://isitmaintained.com/project/StartupMakers/open-parse "Average time to resolve an issue") 6 | [![Percentage of issues still open](http://isitmaintained.com/badge/open/StartupMakers/open-parse.svg)](http://isitmaintained.com/project/StartupMakers/open-parse "Percentage of issues still open") 7 | 8 | > Open Parse = [Parse.com](https://parse.com/docs/rest/guide) + [JSON API](http://jsonapi.org/format/) + [koa](https://github.com/koajs/koa) 9 | 10 | The collection of middleware which provides flexible RESTful API for accessing to application data store and schemas, users and security management. Save your time to bootstrap new web and mobile projects. 11 | 12 | Open Parse is open source BaaS (Backend as a Service). On the schema below that is "Data Proccessing / Management": 13 | 14 | ![BaaS](https://backendless.com/wp-content/uploads/2014/01/baas-apis.png) 15 | 16 | Out of the box **Open Parse** supports: 17 | 18 | * [bunyan-logger](https://github.com/trentm/node-bunyan) which could be connected to Logentries, Loggly, NewRelic and other cloud log management services just in a 15 seconds. 19 | 20 | * [MongoDB](https://github.com/gordonmleigh/promised-mongo) as default data provider. But you could implement custom data providers for any other databases (it takes ~20 min). 21 | 22 | Built with love to [Functional Principles](https://drboolean.gitbooks.io/mostly-adequate-guide/content/) and.. yes, koa. 23 | 24 | ## Content 25 | 26 | * [How it works?](#how-it-works) 27 | * [Installation](#installation) 28 | * [Basic usage](#basic-usage) 29 | * [FAQ](#faq) 30 | + [How to connect a Cloud Log Service?](#how-to-connect-a-cloud-log-service) 31 | * [Inspiration](#inspiration) 32 | * [Contribution](#contribution) 33 | * [Roadmap](#roadmap) 34 | 35 | 36 | ## How It Works? 37 | 38 | Open Parse is incredibly simple. It's just a glue which is connecting 2 pieces: 39 | 40 | * *Middleware* to get RESTful API end-point on your web server. It's implemented according to [JSON API](http://jsonapi.org/) specification. 41 | * *Data Providers* to work with any data storage (by default is MongoDB). 42 | 43 | You can extend any of those points. 44 | 45 | ## Installation 46 | 47 | ```bash 48 | npm install --save open-parse 49 | ``` 50 | 51 | ## Basic Usage 52 | 53 | The following example has been written with using [promised-mongo](https://github.com/gordonmleigh/promised-mongo) and [koa-router](https://github.com/alexmingoia/koa-router) packages. 54 | 55 | ### Setup the environment 56 | ```javascript 57 | import Router from 'koa-router'; 58 | import pmongo from 'promised-mongo'; 59 | 60 | const router = new Router(); 61 | const db = new pmongo('localhost/my-app'); 62 | 63 | const dataRequired = function *(next) { 64 | if (typeof this.request.body['data'] === 'object') { 65 | yield next; 66 | } else { 67 | this.throw(400, 'Request data is required'); 68 | } 69 | }; 70 | ``` 71 | 72 | ### Bring up Users API 73 | ```javascript 74 | const users = { 75 | dataProvider: new UsersDataProvider({ 76 | collection: db.collection('users') 77 | }) 78 | }; 79 | router.post('/users', dataRequired, handleUserSignUp(users)); 80 | router.get('/login', handleUserLogin(users)); 81 | router.post('/logout', handleUserLogout(users)); 82 | router.get('/users/me', handleUserFetch(users)); 83 | ``` 84 | 85 | ### Bring up Classes API 86 | 87 | In this example we're using a local data from JSON file. 88 | 89 | ```javascript 90 | const classes = { 91 | dataProvider: new ObjectsDataProvider({ 92 | collection: db.collection('objects'), 93 | initialCache: require('./cached-objects.json') 94 | }), 95 | }; 96 | router.post('/classes/:className', dataRequired, handleObjectCreate(classes)); 97 | router.get('/classes/:className', handleObjectsList(classes)); 98 | router.get('/classes/:className/:objectId', handleObjectFetch(classes)); 99 | router.patch('/classes/:className/:objectId', dataRequired, handleObjectUpdate(classes)); 100 | router.delete('/classes/:className/:objectId', handleObjectDelete(classes)); 101 | ``` 102 | 103 | For `ObjectsDataProvider` an initial cache should be specified as a `[className][objectId]` hash object: 104 | 105 | `cached-objects.json` 106 | ``` 107 | { 108 | "company": { 109 | "our": { 110 | "title": "Startup Makers", 111 | "about": "We are consulting and outsourcing a web-development with cutting-edge JavaScript technologies (ES6, Node.js, React, Redux, koa)" 112 | } 113 | } 114 | } 115 | ``` 116 | 117 | ### Bring up Schemas API 118 | 119 | ```javascript 120 | const schemas = { 121 | dataProvider: new SchemasDataProvider({ 122 | collection: db.collection('schemas') 123 | }) 124 | }; 125 | router.get('/schemas/:className', handleSchemaFetch(schemas)); 126 | ``` 127 | 128 | ### Connect the router to your application 129 | 130 | ```javascript 131 | import koa from 'koa'; 132 | import cors from 'kcors'; 133 | import qs from 'koa-qs'; 134 | import bodyParser from 'koa-bodyparser'; 135 | import mount from 'koa-mount'; 136 | 137 | // Create the server instance 138 | const app = koa(); 139 | app.use(cors()); 140 | qs(app); 141 | app.use(bodyParser()); 142 | 143 | // ...paste your routes here... 144 | 145 | // Connect API router 146 | app.use(mount('/api', router)); 147 | 148 | // Go LIVE 149 | app.listen(process.env['PORT'] || 3000); 150 | ``` 151 | 152 | ### Work with Open Parse API from the browser or mobile apps 153 | 154 | For example how to implement login in your browser scripts when you have connected Open Parse: 155 | 156 | ```javascript 157 | const login = (email, password) => { 158 | const loginURL = 159 | '/api/login' 160 | + '?email=' + encodeURIComponent(email) 161 | + '&password=' + encodeURIComponent(password); 162 | fetch(loginURL, { 163 | headers: { 164 | 'Accept': 'application/json', 165 | }, 166 | credentials: 'same-origin' 167 | }).then((response) => response.json()).then((body) => { 168 | if (body['data']) { 169 | const userId = body['data']['id']; 170 | const userName = body['data']['attributes']['name']; 171 | console.log('Logged as user %s (%s)', userName, userId); 172 | } else { 173 | body['errors'].forEach(error => 174 | console.error('Auth error: %s (%s)', error['title'], error['source']['parameter']) 175 | ); 176 | } 177 | }); 178 | }; 179 | ``` 180 | 181 | ## FAQ 182 | 183 | ### How To Connect a Cloud Log Service? 184 | 185 | It's really easy. Did you initialize a logger? If you didn't, let's do it right now: 186 | 187 | ```javascript 188 | import bunyan from 'bunyan'; 189 | import { LogentriesBunyanStream } from 'bunyan-logentries'; 190 | 191 | const logger = bunyan.createLogger({ 192 | name: 'awesome-app', 193 | streams: { 194 | stream: new LogentriesBunyanStream({ 195 | token: process.env['LOGENTRIES_TOKEN'] 196 | }), 197 | level: 'debug', 198 | type: 'raw' 199 | } 200 | }); 201 | ``` 202 | 203 | Add just a one line to your code 204 | 205 | ```javascript 206 | const users = { 207 | dataProvider: new UsersDataProvider({ 208 | collection: db.collection('users') 209 | }), 210 | logger // THIS LINE! 211 | }; 212 | router.post('/users', dataRequired, handleUserSignUp(users)); 213 | router.get('/login', handleUserLogin(users)); 214 | router.post('/logout', handleUserLogout(users)); 215 | router.get('/users/me', handleUserFetch(users)); 216 | ``` 217 | 218 | That's all. You will get a messages (about login, logout and fetching the data about users) in your Logentries account. 219 | 220 | # Inspiration 221 | 222 | * [Parse.com](https://parse.com/docs/rest/guide) - Commercial Backend-as-a-Service platform 223 | * [BaasBox API](http://www.baasbox.com/documentation/?shell#api) - Java-based open source Backend-as-a-Service solution 224 | * [DeployD API](http://docs.deployd.com/api/) - first generation open source BaaS platform 225 | * [Sails.js](http://sailsjs.org/documentation/concepts/) - first generation MVC framework for Node.js 226 | * [Reindex.io](https://www.reindex.io/docs/) - Commercial BaaS platform with GraphQL API 227 | * [Serverless](https://github.com/serverless/serverless) - Hmm? 228 | 229 | Would you like get some features from solutions above? [Ask me](https://github.com/StartupMakers/open-parse/issues/new) or create a Pull Request. 230 | 231 | # Contribution 232 | 233 | Are you ready to make the world better? 234 | 235 | **1.** Fork this repo 236 | 237 | **2.** Checkout your repo: 238 | 239 | ```bash 240 | git clone git@github.com:YourAccount/open-parse.git 241 | ``` 242 | 243 | **3.** Create your feature (or issue) branch: 244 | 245 | ```bash 246 | git checkout -b my-new-feature 247 | ``` 248 | 249 | **4.** Commit your changes: 250 | 251 | ```bash 252 | git commit -am 'Add some changes' 253 | ``` 254 | 255 | **5.** Push to the branch: 256 | 257 | ```bash 258 | git push origin my-new-feature 259 | ``` 260 | 261 | **6.** [Create new pull request](https://github.com/StartupMakers/open-parse/compare) 262 | 263 | Thank you very much. Your support is greatly appreciated. 264 | 265 | # Roadmap 266 | 267 | **Version 0.2** 268 | 269 | * Support access control layer (ACL) 270 | * Add real world example 271 | * Improve the documentation and architecture schema 272 | * Add 'Access-Control-Allow-Origin' header 273 | 274 | **Version 0.3** 275 | 276 | * Add Express middleware adapter 277 | * Support jobs feature 278 | * Support e-mail service 279 | 280 | **Version 0.4** 281 | 282 | * Add client SDK for JavaScript and React Native 283 | * Support files feature 284 | 285 | **Version 0.5** 286 | 287 | * Support web hooks 288 | --------------------------------------------------------------------------------