├── .gitignore ├── .npmignore ├── .travis.yml ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── repo ├── logo.jpg └── patreon.png ├── src ├── ParseCloudClass.ts ├── decorators │ ├── requireKey.ts │ └── requireLogin.ts ├── index.ts ├── test │ ├── cloud │ │ ├── AnalyticAddon.js │ │ ├── TestAddon.js │ │ └── main.js │ ├── index.integration.test.ts │ ├── index.test.ts │ └── util │ │ ├── requireKey.test.ts │ │ └── requireLogin.test.ts └── util │ └── requireLogin.ts ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | out/ 4 | npm-debug.log 5 | logs -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | node_modules/ 3 | test/ 4 | tsconfig.json 5 | tslint.json 6 | coverage/ 7 | .vscode/ 8 | logs 9 | .gitignore 10 | .travis.yml 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | services: mongodb 5 | cache: 6 | npm: true 7 | install: 8 | - npm install 9 | script: 10 | - npm install codecov -g 11 | - npm run build 12 | - npm t -- --coverage 13 | after_success: 14 | - codecov 15 | deploy: 16 | provider: npm 17 | email: jcguarinpenaranda@gmail.com 18 | api_key: $NPM_TOKEN 19 | on: 20 | branch: master 21 | tags: true -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "tslint.enable": true, 4 | "tslint.autoFixOnSave": true, 5 | "typescript.tsdk": "node_modules/typescript/lib" 6 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Otherwise SAS 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # REPO MOVED 2 | This repository has been moved to Otherwise's new monorepo :) https://github.com/owsas/opensource/tree/master/packages/parse-cloud-class Enjoy! 3 | 4 | # Parse Cloud Class 5 | 6 | ![Travis](https://travis-ci.org/owsas/parse-cloud-class.svg?branch=master) [![codecov](https://codecov.io/gh/owsas/parse-cloud-class/branch/master/graph/badge.svg)](https://codecov.io/gh/owsas/parse-cloud-class) 7 | 8 | ![Logo](./repo/logo.jpg) 9 | Photo by [chuttersnap](https://unsplash.com/photos/9AqIdzEc9pY?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com) 10 | 11 | Travis tests: https://travis-ci.org/owsas/parse-cloud-class/builds 12 | 13 | A new way to define Parse.Cloud events for your classes (DB tables). With this module you can easily: 14 | 15 | * Define minimum values for keys on your classes 16 | * Define maximum values for keys on your classes 17 | * Define default values 18 | * Define required keys 19 | * Define immutable keys (only editable with the master key) 20 | * Use addons to easily extend the functionality of your app 21 | * Create new addons and share them with the community 22 | * Customize the default behaviour to your own needs 23 | 24 | This module is meant to be used with [Parse](http://docs.parseplatform.org/) and [Parse Server](https://github.com/parse-community/parse-server) 25 | 26 | ## Installation 27 | `> npm install --save parse-server-addon-cloud-class parse` 28 | 29 | __Typescript__: This module comes bundled with Intellisense :) 30 | 31 | After installing, please make sure to install also `parse>1.11.0` 32 | 33 | ## Example 34 | A working example can be found here: https://github.com/owsas/parse-cloud-class-example 35 | 36 | ## Supported versions 37 | * Parse >1.10.0 38 | * Parse >=2.0 39 | * Parse >=3.0 40 | 41 | ## New: Configuration with objects 42 | Starting april 2019 (v1.1.0), it's possible to create classes with configuration objects 43 | 44 | Example: 45 | ```js 46 | const ParseCloudClass = require('parse-server-addon-cloud-class').ParseCloudClass; 47 | 48 | // Create a new configuration object to define the class behaviour. 49 | // All attributes are optional 50 | const gamePoint = { 51 | requiredKeys: ['points'], // all objects saved must have the points attribute 52 | defaultValues: { points: 20 }, // by default, all new objects will have 20 points (if it was not set at the time of creation) 53 | minimumValues: { points: 10 }, // minimum 10 points 54 | maximumValues: { points: 1000 }, // maximum 1000 points 55 | immutableKeys: ['points'], // once set, the points can't be changed (only master can do that) 56 | beforeFind: function(req) { 57 | // Do something here 58 | return req.query; 59 | }, 60 | processBeforeSave: async function(req) { 61 | // Do something here 62 | return req.object; 63 | }, 64 | afterSave: async function(req) { 65 | // Do something here 66 | return req.object; 67 | }, 68 | processBeforeDelete: async function(req) { 69 | // Do something here 70 | return req.object; 71 | }, 72 | afterDelete: async function(req) { 73 | // Do something here 74 | return req.object; 75 | } 76 | } 77 | 78 | // Create an instance 79 | const gamePointClass = ParseCloudClass.fromObject(gamePoint); 80 | 81 | // Configure the class in the main.js cloud file 82 | ParseCloudClass.configureClass(Parse, 'GamePoint', gamePointClass); 83 | ``` 84 | 85 | As you see, instead of defining `beforeSave`, we use `processBeforeSave`. This is because ParseCloudClass uses the `beforeSave` function to wrap up some extra logic that we may not want to rewrite each time. In the same fashion, we use `processBeforeDelete`. 86 | 87 | With this new functionality, the `this` keyword inside the `beforeFind`, `processBeforeSave`, `afterSave`, `processBeforeDelete` and `beforeDelete` functions refers to the instance itself, which means you can access for example `this.requiredKeys`, etc. 88 | 89 | ## Basic Usage 90 | ```js 91 | /* 92 | * This is the main cloud file for Parse 93 | * cloud/main.js 94 | */ 95 | 96 | // with normal ES5 97 | const ParseCloudClass = require('parse-server-addon-cloud-class').ParseCloudClass; 98 | 99 | // with typescript or ES6 100 | import { ParseCloudClass } from 'parse-server-addon-cloud-class'; 101 | 102 | const myConfig = new ParseCloudClass({ 103 | // New items will not be created if they have no 'name' set 104 | requiredKeys: ['name'], 105 | 106 | defaultValues: { 107 | // All new items will have active: true 108 | active: true, 109 | // By default, timesShared will be 0 110 | timesShared: 0, 111 | }, 112 | 113 | minimumValues: { 114 | // timesShared cannot go below 0 115 | timesShared: 0, 116 | }, 117 | 118 | // Keys that are only editable by the master key. 119 | // Trying to edit apiKey without the master key will throw an error 120 | immutableKeys: ['apiKey'], 121 | }); 122 | 123 | // Configure your class to use the configuration 124 | ParseCloudClass.configureClass(Parse, 'MyClass', myConfig); 125 | ``` 126 | 127 | When you configure your classes to work with ParseCloudClass, they will be attached the following events 128 | * `beforeFind` 129 | * `beforeSave` 130 | * `beforeDelete` 131 | * `afterSave` 132 | * `afterDelete` 133 | 134 | By default, the only event that is going to do something is the `beforeSave`, that is going to check the `minimumValues`, `defaultValues` and `requiredKeys` 135 | 136 | ## Extending ParseCloudClass 137 | 138 | You can easily extend ParseCloudClass in order to define your custom behaviours. In this case, you must have into account the following two extra methods of a ParseCloudClass: 139 | * `processBeforeSave`: Here you would define your custom behaviour for `beforeSave` 140 | * `processBeforeDelete`: Here you would define your custom behaviour for `beforeDelete` 141 | 142 | ```js 143 | // myCustomFile.js 144 | import { ParseCloudClass } from 'parse-server-addon-cloud-class'; 145 | 146 | export class MyCustomClass extends ParseCloudClass { 147 | /* 148 | * Here you can define your custom minimumValues, 149 | * defaultValues and requiredKeys 150 | */ 151 | requiredKeys = ['title'] 152 | 153 | /** 154 | * @param req {Parse.Cloud.BeforeSaveRequest} 155 | */ 156 | async processBeforeSave(req) { 157 | // Make sure the super class validates the required keys, 158 | // minimum values, executes the addons, etc 159 | const object = await super.processBeforeSave(req); 160 | 161 | // write your own code here 162 | .... 163 | 164 | // make sure to return req.object 165 | return object; 166 | } 167 | } 168 | ``` 169 | 170 | You can change the implementation of any method to your needs, but please, call the super class' processBeforeSave if you expect to have requiredKeys checking, minimum values checking, addon functionalities, etcetera. 171 | 172 | ### Decorators 173 | 174 | Parse Cloud Class comes with two decorators that you may use in your own applications. Please keep in mind that you must activate `enableExperimentalDecorators`. 175 | 176 | #### requireLogin decorator 177 | It requires all `beforeSave` and `beforeDelete` requests to be made by a registered user or by the master key 178 | 179 | 180 | 181 | #### requireKey decorator 182 | It pushes required keys to the given class when it is initialized 183 | 184 | Example: 185 | 186 | ```ts 187 | @requireKey('myRequiredKey') 188 | export default class MyClass extends ParseClass { 189 | } 190 | ``` 191 | 192 | This is different from defining the required keys in the class' body, because 193 | in that way the previously set required keys would be overriden. 194 | 195 | Example: 196 | 197 | ``` ts 198 | default class MyClass extends ParseClass { 199 | public requiredKeys: string[] = ['a', 'b'] 200 | } 201 | 202 | default class MyOtherClass extends MyClass { 203 | public requiredKeys: string[] = ['c'] // 'a', 'b' are not set anymore 204 | } 205 | 206 | // With requireKey: 207 | @requireKey('c') 208 | export default class MyOtherClass2 extends MyClass { 209 | // requiredKeys are 'a', 'b', 'c' 210 | } 211 | ``` 212 | 213 | ### All the possibilities 214 | 215 | ```ts 216 | interface IParseCloudClass { 217 | 218 | beforeFind( 219 | req: Parse.Cloud.BeforeFindRequest, 220 | ): Parse.Query; 221 | 222 | processBeforeSave ( 223 | req: Parse.Cloud.BeforeSaveRequest | IProcessRequest, 224 | ): Promise; 225 | 226 | beforeSave( 227 | req: Parse.Cloud.BeforeSaveRequest | IProcessRequest, 228 | // parse sdk > 2.0 does not have the res parameter 229 | res?: Parse.Cloud.BeforeSaveResponse | IProcessResponse, 230 | ): Promise; 231 | 232 | afterSave ( 233 | req: Parse.Cloud.BeforeDeleteRequest | IProcessRequest, 234 | ): Promise; 235 | 236 | processBeforeDelete ( 237 | req: Parse.Cloud.BeforeDeleteRequest | IProcessRequest, 238 | ): Promise; 239 | 240 | beforeDelete( 241 | req: Parse.Cloud.BeforeDeleteRequest | IProcessRequest, 242 | // parse sdk > 2.0 does not have the res parameter 243 | res?: Parse.Cloud.BeforeDeleteResponse | IProcessResponse, 244 | ): Promise; 245 | 246 | afterDelete ( 247 | req: Parse.Cloud.BeforeDeleteRequest | IProcessRequest, 248 | ): Promise; 249 | 250 | } 251 | ``` 252 | 253 | Note: IProcessRequest is an interface that allows you to do testing 254 | 255 | ```ts 256 | interface IProcessRequest { 257 | object: Parse.Object; 258 | user?: Parse.User; 259 | master?: boolean; 260 | } 261 | ``` 262 | 263 | 264 | ## Using addons 265 | 266 | To use an addon, you would first import it, and then configure your class 267 | to use that addon. Example: 268 | 269 | ```js 270 | // with typescript or ES6 271 | import { ParseCloudClass } from 'parse-server-addon-cloud-class'; 272 | import { SomeAddon } from 'some-addon-module'; 273 | 274 | const myConfig = new ParseCloudClass(); 275 | 276 | // use the addon 277 | myConfig.useAddon(SomeAddon); 278 | 279 | // you can use any number of addons 280 | myConfig.useAddon(SomeOtherAddon); 281 | 282 | // Configure your class to use the configuration 283 | ParseCloudClass.configureClass(Parse, 'MyClass', myConfig); 284 | ``` 285 | 286 | Take into account that addons are executed in the order in which they were added. 287 | 288 | ## Creating addons 289 | 290 | Addons can be created by extending ParseCloudClass and defining new behaviours on: 291 | * `beforeFind` 292 | * `beforeSave` 293 | * `beforeDelete` 294 | * `afterSave` 295 | * `afterDelete` 296 | * `processBeforeSave` 297 | * `processBeforeDelete` 298 | 299 | ### Example addon: 300 | 301 | ```js 302 | // In Javascript 303 | class Addon1 extends ParseCloudClass { 304 | async processBeforeSave(req) { 305 | req.object.set('addon1', true); 306 | return req.object; 307 | } 308 | } 309 | ``` 310 | 311 | ```ts 312 | // In Typescript 313 | class Addon1 extends ParseCloudClass { 314 | async processBeforeSave(req: Parse.Cloud.BeforeSaveRequest) { 315 | req.object.set('addon1', true); 316 | return req.object; 317 | } 318 | } 319 | ``` 320 | 321 | Now you can also create addons using the new configuration objects, for example: 322 | 323 | ```js 324 | const dbAddon = { 325 | afterSave: async function(req) { 326 | // replicate data to the other db 327 | return req.object; 328 | }, 329 | afterDelete: async function(req) { 330 | // replicate data to the other db 331 | return req.object; 332 | } 333 | } 334 | 335 | const addonInstance = ParseCloudClass.fromObject(dbAddon); 336 | ``` 337 | 338 | 339 | ## Addons 340 | 341 | * Algolia Search: https://github.com/owsas/parse-server-addon-cloud-algolia 342 | 343 | 344 | ## Credits 345 | 346 | Developed by Juan Camilo Guarín Peñaranda, 347 | Otherwise SAS, Colombia 348 | 2017 349 | 350 | ## License 351 | 352 | MIT. 353 | 354 | ## Support us on Patreon 355 | [![patreon](./repo/patreon.png)](https://patreon.com/owsas) 356 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parse-server-addon-cloud-class", 3 | "version": "1.1.1", 4 | "description": "A new way to define Parse.Cloud events for your classes", 5 | "main": "out/index.js", 6 | "types": "out/index.d.ts", 7 | "scripts": { 8 | "test": "jest --ci --maxWorkers=2", 9 | "build": "tsc", 10 | "watch": "tsc -w", 11 | "prepublishOnly": "tsc" 12 | }, 13 | "keywords": [], 14 | "author": "Juan Camilo Guarín P", 15 | "license": "MIT", 16 | "devDependencies": { 17 | "@types/express": "^4.16.1", 18 | "@types/jest": "^24.0.16", 19 | "@types/jquery": "^3.3.30", 20 | "@types/parse": "^2.2.7", 21 | "delay": "^4.0.1", 22 | "express": "^4.16.4", 23 | "jest": "^24.7.1", 24 | "node-fetch": "^2.2.0", 25 | "parse": "^2.6.0", 26 | "parse-server": "^4.5.0", 27 | "ts-jest": "^24.0.2", 28 | "tslint": "^5.16.0", 29 | "tslint-config-airbnb": "^5.11.1", 30 | "typescript": "^3.5.3" 31 | }, 32 | "peerDependencies": { 33 | "parse": ">=1.11.0" 34 | }, 35 | "dependencies": { 36 | "is": "^3.2.1" 37 | }, 38 | "jest": { 39 | "roots": [ 40 | "/src", 41 | "/test" 42 | ], 43 | "transform": { 44 | "^.+\\.tsx?$": "ts-jest" 45 | }, 46 | "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", 47 | "moduleFileExtensions": [ 48 | "ts", 49 | "tsx", 50 | "js", 51 | "jsx", 52 | "json" 53 | ], 54 | "modulePathIgnorePatterns": [ 55 | "/out" 56 | ] 57 | }, 58 | "repository": { 59 | "type": "git", 60 | "url": "git+https://github.com/owsas/parse-cloud-class.git" 61 | }, 62 | "bugs": { 63 | "url": "https://github.com/owsas/parse-cloud-class/issues" 64 | }, 65 | "homepage": "https://github.com/owsas/parse-cloud-class#readme" 66 | } 67 | -------------------------------------------------------------------------------- /repo/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owsas/parse-cloud-class/c41ac14a44b92c86ed6593c33b40929c88f069d4/repo/logo.jpg -------------------------------------------------------------------------------- /repo/patreon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owsas/parse-cloud-class/c41ac14a44b92c86ed6593c33b40929c88f069d4/repo/patreon.png -------------------------------------------------------------------------------- /src/ParseCloudClass.ts: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import * as Parse from 'parse/node'; 3 | import * as is from 'is'; 4 | 5 | /** 6 | * Interface that allows easy testing 7 | * without having to create a Parse.Cloud.BeforeSaveRequest or 8 | * others 9 | */ 10 | export interface IProcessRequest { 11 | object: Parse.Object; 12 | user?: Parse.User; 13 | master?: boolean; 14 | } 15 | 16 | /** 17 | * Mock before find request for testing 18 | */ 19 | export interface IBeforeFindRequest { 20 | query: Parse.Query; 21 | } 22 | 23 | /** 24 | * Interface that allows easy testing 25 | * without having to create a Parse Cloud Response 26 | */ 27 | export interface IProcessResponse { 28 | success: ((obj: any) => void); 29 | error: ((e: any) => void); 30 | } 31 | 32 | export interface IConstructorParams { 33 | requiredKeys?: string[]; 34 | defaultValues?: {[key: string]: any}; 35 | minimumValues?: {[key: string]: number}; 36 | maximumValues?: {[key: string]: number}; 37 | immutableKeys?: string[]; 38 | } 39 | 40 | /** 41 | * Interface that describes a configuration object 42 | */ 43 | export interface ICloudClassObject extends IConstructorParams { 44 | addons?: ParseCloudClass[]; 45 | 46 | beforeFind?: ( 47 | req: Parse.Cloud.BeforeFindRequest, 48 | ) => Parse.Query; 49 | 50 | processBeforeSave?: ( 51 | req: Parse.Cloud.BeforeSaveRequest, 52 | ) => Promise; 53 | 54 | afterSave?: ( 55 | req: Parse.Cloud.AfterSaveRequest, 56 | ) => Promise; 57 | 58 | processBeforeDelete?: ( 59 | req: Parse.Cloud.BeforeSaveRequest, 60 | ) => Promise; 61 | 62 | afterDelete?: ( 63 | req: Parse.Cloud.AfterSaveRequest, 64 | ) => Promise; 65 | } 66 | 67 | /** 68 | * Defines the methods every ParseCloudClass should have 69 | */ 70 | export interface IParseCloudClass { 71 | 72 | beforeFind( 73 | req: Parse.Cloud.BeforeFindRequest, 74 | ): Parse.Query; 75 | 76 | processBeforeSave ( 77 | req: Parse.Cloud.BeforeSaveRequest | IProcessRequest, 78 | ): Promise; 79 | 80 | beforeSave( 81 | req: Parse.Cloud.BeforeSaveRequest | IProcessRequest, 82 | res?: Parse.Cloud.BeforeSaveResponse | IProcessResponse, 83 | ): Promise; 84 | 85 | afterSave ( 86 | req: Parse.Cloud.BeforeDeleteRequest | IProcessRequest, 87 | ): Promise; 88 | 89 | processBeforeDelete ( 90 | req: Parse.Cloud.BeforeDeleteRequest | IProcessRequest, 91 | ): Promise; 92 | 93 | beforeDelete( 94 | req: Parse.Cloud.BeforeDeleteRequest | IProcessRequest, 95 | res?: Parse.Cloud.BeforeDeleteResponse | IProcessResponse, 96 | ): Promise; 97 | 98 | afterDelete ( 99 | req: Parse.Cloud.BeforeDeleteRequest | IProcessRequest, 100 | ): Promise; 101 | 102 | } 103 | 104 | /** 105 | * Handles beforeSave and beforeDelete execution logic for any Parse class 106 | * on the database 107 | */ 108 | export default class ParseCloudClass implements IParseCloudClass { 109 | 110 | requiredKeys: string[] = []; 111 | defaultValues: {[key: string]: any} = {}; 112 | minimumValues: {[key: string]: number} = {}; 113 | maximumValues: {[key: string]: number} = {}; 114 | addons: ParseCloudClass [] = []; 115 | immutableKeys: string[] = []; 116 | 117 | constructor (params?: IConstructorParams) { 118 | if (params) { 119 | if (params.requiredKeys) { 120 | this.requiredKeys = params.requiredKeys; 121 | } 122 | 123 | if (params.defaultValues) { 124 | this.defaultValues = params.defaultValues; 125 | } 126 | 127 | if (params.minimumValues) { 128 | this.minimumValues = params.minimumValues; 129 | } 130 | 131 | if (params.maximumValues) { 132 | this.maximumValues = params.maximumValues; 133 | } 134 | 135 | if (params.immutableKeys) { 136 | this.immutableKeys = params.immutableKeys; 137 | } 138 | } 139 | 140 | this.afterSave = this.afterSave.bind(this); 141 | this.afterDelete = this.afterDelete.bind(this); 142 | this.beforeDelete = this.beforeDelete.bind(this); 143 | this.beforeFind = this.beforeFind.bind(this); 144 | this.beforeSave = this.beforeSave.bind(this); 145 | this.useAddon = this.useAddon.bind(this); 146 | } 147 | 148 | /** 149 | * Get a class configuration based 150 | * on a JSON object 151 | * @param object 152 | */ 153 | static fromObject (object: ICloudClassObject): ParseCloudClass { 154 | // Create a class that extends the ParseCloudClass and adds behaviours 155 | // set in the object 156 | class ExtendedCloudClass extends ParseCloudClass { 157 | beforeFind(req: Parse.Cloud.BeforeFindRequest): Parse.Query { 158 | let query = super.beforeFind(req); 159 | 160 | if (object.beforeFind) { 161 | query = object.beforeFind.bind(this)(req); 162 | } 163 | 164 | return query; 165 | } 166 | 167 | async processBeforeSave ( 168 | req: Parse.Cloud.BeforeSaveRequest | IProcessRequest, 169 | ): Promise { 170 | let obj = await super.processBeforeSave(req); 171 | 172 | if (object.processBeforeSave) { 173 | obj = await object.processBeforeSave.bind(this)(req); 174 | } 175 | 176 | return obj; 177 | } 178 | 179 | async afterSave(req: Parse.Cloud.AfterSaveRequest): Promise { 180 | let obj = await super.afterSave(req); 181 | 182 | if (object.afterSave) { 183 | obj = await object.afterSave.bind(this)(req); 184 | } 185 | 186 | return obj; 187 | } 188 | 189 | async processBeforeDelete ( 190 | req: Parse.Cloud.BeforeDeleteRequest | IProcessRequest, 191 | ): Promise { 192 | let obj = await super.processBeforeDelete(req); 193 | 194 | if (object.processBeforeDelete) { 195 | obj = await object.processBeforeDelete.bind(this)(req); 196 | } 197 | 198 | return obj; 199 | } 200 | 201 | async afterDelete( 202 | req: Parse.Cloud.AfterDeleteRequest, 203 | ): Promise { 204 | let obj = await super.afterDelete(req); 205 | 206 | if (object.afterDelete) { 207 | obj = await object.afterDelete.bind(this)(req); 208 | } 209 | 210 | return obj; 211 | } 212 | } 213 | 214 | const cloudClass = new ExtendedCloudClass(object); 215 | 216 | // Attach the addons 217 | if (object.addons && object.addons.length) { 218 | object.addons.forEach((addon) => { 219 | cloudClass.useAddon(addon); 220 | }); 221 | } 222 | 223 | return cloudClass; 224 | } 225 | 226 | /** 227 | * Configures a class for working on Parse Cloud 228 | * @param P The Parse Cloud Object 229 | * @param className The name of the class 230 | * @param cloudClass The extended class to configure 231 | */ 232 | static configureClass (P: any, className: string, instance: ParseCloudClass): void { 233 | P.Cloud.beforeFind(className, instance.beforeFind); 234 | 235 | P.Cloud.beforeSave(className, instance.beforeSave); 236 | 237 | P.Cloud.afterSave(className, instance.afterSave); 238 | 239 | P.Cloud.beforeDelete(className, instance.beforeDelete); 240 | 241 | P.Cloud.afterDelete(className, instance.afterDelete); 242 | } 243 | 244 | /** 245 | * Checks that an object has the required keys, and 246 | * throws an error if not 247 | * @param obj 248 | * @param requiredKeys 249 | * @throws {Error} If any of the required keys is not met 250 | */ 251 | static checkRequiredKeys (obj: Parse.Object, requiredKeys: string[]): void { 252 | requiredKeys.forEach((requiredParam) => { 253 | const currentValue = obj.get(requiredParam); 254 | if (! currentValue 255 | || is.array(currentValue) && !currentValue.length 256 | ) { 257 | throw new Parse.Error(-1, `Params ${requiredKeys.join(', ')} are needed`); 258 | } 259 | }); 260 | } 261 | 262 | /** 263 | * Sets the default values to the object given 264 | * @param obj 265 | * @param defaultValues 266 | * @return {Parse.Object} 267 | */ 268 | static setDefaultValues (obj: Parse.Object, defaultValues: any): Parse.Object { 269 | const object = obj.clone(); 270 | 271 | for (const key of Object.keys(defaultValues)) { 272 | if (is.undefined(object.get(key))) { 273 | object.set(key, defaultValues[key]); 274 | } 275 | } 276 | 277 | return object; 278 | } 279 | 280 | /** 281 | * Checks that the object has certain minimum values 282 | * @param object 283 | * @param minimumValues 284 | */ 285 | static checkAndCorrectMinimumValues ( 286 | object: Parse.Object, 287 | minimumValues: {[key: string]: number} = {}, 288 | ): Parse.Object { 289 | const obj = object.clone(); 290 | 291 | for (const key in minimumValues) { 292 | if (is.undefined(obj.get(key)) || (obj.get(key) < minimumValues[key])) { 293 | obj.set(key, minimumValues[key]); 294 | } 295 | } 296 | 297 | return obj; 298 | } 299 | 300 | /** 301 | * Checks that the object has certain minimum values 302 | * @param object 303 | * @param maximumValues 304 | */ 305 | static checkAndCorrectMaximumValues ( 306 | object: Parse.Object, 307 | maximumValues: {[key: string]: number} = {}, 308 | ): Parse.Object { 309 | const obj = object.clone(); 310 | 311 | for (const key in maximumValues) { 312 | if ((obj.get(key) > maximumValues[key])) { 313 | obj.set(key, maximumValues[key]); 314 | } 315 | } 316 | 317 | return obj; 318 | } 319 | 320 | /** 321 | * Checks keys that should not be editable 322 | * if they are not explicitly changed with the master key 323 | * @param obj 324 | * @param isMaster 325 | */ 326 | checkImmutableKeys(obj: Parse.Object, isMaster: boolean) { 327 | this.immutableKeys.forEach((key) => { 328 | if (obj.dirtyKeys().indexOf(key) !== -1 && !isMaster) { 329 | throw new Parse.Error(-1, `${key} cannot be modified`); 330 | } 331 | }); 332 | } 333 | 334 | /** 335 | * Pushes an addon to the addon list 336 | * @param addon 337 | */ 338 | useAddon(addon: ParseCloudClass) { 339 | this.addons.push(addon); 340 | } 341 | 342 | /** 343 | * Executes some code before finding 344 | * elements of this class 345 | * @param req 346 | */ 347 | beforeFind ( 348 | req: Parse.Cloud.BeforeFindRequest | IBeforeFindRequest, 349 | ): Parse.Query { 350 | return req.query; 351 | } 352 | 353 | /** 354 | * Executes the instance processBefore save function 355 | * and handles the success or errors that may occur 356 | * @param req 357 | * @param res 358 | * @return A promise that says if everything went fine or not 359 | */ 360 | async beforeSave ( 361 | req: Parse.Cloud.BeforeSaveRequest | IProcessRequest, 362 | res?: Parse.Cloud.BeforeSaveResponse | IProcessResponse, 363 | ): Promise { 364 | try { 365 | // Trigger the addons to determine if the object can be saved 366 | for (const addon of this.addons) { 367 | req.object = await addon.processBeforeSave(req); 368 | } 369 | 370 | req.object = await this.processBeforeSave(req); 371 | if (res && res.success) { 372 | (res as any).success(req.object); 373 | } else { 374 | return req.object; 375 | } 376 | } catch (e) { 377 | const message = e.message || JSON.stringify(e); 378 | if (res && res.error) { 379 | res.error(message); 380 | } else { 381 | throw e; 382 | } 383 | } 384 | } 385 | 386 | /** 387 | * Does all the processing to determine if a certain object 388 | * can be saved or not 389 | * @param req 390 | * @return The object, altered with the default values, minimum values, and others 391 | */ 392 | async processBeforeSave ( 393 | req: Parse.Cloud.BeforeSaveRequest | IProcessRequest, 394 | ): Promise< Parse.Object> { 395 | let obj = req.object; 396 | obj = ParseCloudClass.setDefaultValues(obj, this.defaultValues); 397 | obj = ParseCloudClass.checkAndCorrectMinimumValues(obj, this.minimumValues || {}); 398 | obj = ParseCloudClass.checkAndCorrectMaximumValues(obj, this.minimumValues || {}); 399 | ParseCloudClass.checkRequiredKeys(obj, this.requiredKeys); 400 | this.checkImmutableKeys(obj, req.master); 401 | 402 | return obj; 403 | } 404 | 405 | /** 406 | * Default afterSave saves an analytic of creation that references the object 407 | * @param req 408 | * @return The object that was saved 409 | */ 410 | async afterSave ( 411 | req: Parse.Cloud.BeforeDeleteRequest | IProcessRequest, 412 | ): Promise< Parse.Object > { 413 | 414 | // Trigger the addons for the beforeSave process 415 | for (const addon of this.addons) { 416 | req.object = await addon.afterSave(req); 417 | } 418 | 419 | return req.object; 420 | } 421 | 422 | /** 423 | * Does all the processing to determine if this 424 | * object can be deleted or not 425 | * @param req 426 | * @return The object that is about to be deleted 427 | */ 428 | async processBeforeDelete ( 429 | req: Parse.Cloud.BeforeDeleteRequest | IProcessRequest, 430 | ): Promise< Parse.Object > { 431 | return req.object; 432 | } 433 | 434 | /** 435 | * Executes the processBeforeDelete function 436 | * and returns if it was ok or not 437 | * @param req 438 | * @param res 439 | * @return A promise that states if everything went fine or not 440 | */ 441 | async beforeDelete ( 442 | req: Parse.Cloud.BeforeDeleteRequest | IProcessRequest, 443 | res?: Parse.Cloud.BeforeDeleteResponse | IProcessResponse, 444 | ): Promise { 445 | try { 446 | // Trigger the addons to determine if the object can be deleted 447 | for (const addon of this.addons) { 448 | req.object = await addon.processBeforeDelete(req); 449 | } 450 | 451 | req.object = await this.processBeforeDelete(req); 452 | if (res) { 453 | (res as any).success(req.object); 454 | } else { 455 | return req.object; 456 | } 457 | } catch (e) { 458 | const message = e.message || JSON.stringify(e); 459 | if (res && res.error) { 460 | res.error(message); 461 | } else { 462 | throw e; 463 | } 464 | } 465 | } 466 | 467 | /** 468 | * Executes something after the object was deleted 469 | * successfully 470 | * @param req 471 | */ 472 | async afterDelete ( 473 | req: Parse.Cloud.BeforeDeleteRequest | IProcessRequest, 474 | ): Promise { 475 | // Trigger the addons to determine what happens after 476 | // the object has been deleted 477 | for (const addon of this.addons) { 478 | req.object = await addon.afterDelete(req); 479 | } 480 | 481 | return req.object; 482 | } 483 | 484 | } 485 | -------------------------------------------------------------------------------- /src/decorators/requireKey.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Adds keys to the array of requiredKeys in the class 3 | * @param keys The key or keys that are required 4 | */ 5 | export default function requireKey(...keys: string[]) { 6 | // tslint:disable-next-line 7 | return function (constructor: T) { 8 | return class extends constructor { 9 | constructor(...args: any[]) { 10 | super(...args); 11 | this.requiredKeys.push(...keys); 12 | } 13 | }; 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /src/decorators/requireLogin.ts: -------------------------------------------------------------------------------- 1 | import ParseCloudClass from '../ParseCloudClass'; 2 | import requireLogin from '../util/requireLogin'; 3 | 4 | /** 5 | * Adds keys to the array of requiredKeys in the class 6 | * @param keys The key or keys that are required 7 | */ 8 | export default function requireLoginDecorator() { 9 | // tslint:disable-next-line 10 | return function (ExtendedClass: T) { 11 | return class extends ExtendedClass { 12 | public async processBeforeSave(req: Parse.Cloud.BeforeSaveRequest) { 13 | // require login 14 | requireLogin(req); 15 | 16 | // return the request's object 17 | return super.processBeforeSave(req); 18 | } 19 | 20 | public async processBeforeDelete(req: Parse.Cloud.BeforeDeleteRequest) { 21 | // require login 22 | requireLogin(req); 23 | 24 | // return the request's object 25 | return super.processBeforeDelete(req); 26 | } 27 | }; 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import ParseCloudClass, { 2 | IBeforeFindRequest, 3 | IParseCloudClass, 4 | IProcessRequest, 5 | IProcessResponse, 6 | } from './ParseCloudClass'; 7 | 8 | export { 9 | ParseCloudClass, 10 | IBeforeFindRequest, 11 | IParseCloudClass, 12 | IProcessRequest, 13 | IProcessResponse, 14 | }; 15 | -------------------------------------------------------------------------------- /src/test/cloud/AnalyticAddon.js: -------------------------------------------------------------------------------- 1 | const Parse = require('parse/node'); 2 | const { ParseCloudClass } = require('../../'); 3 | 4 | class AnalyticAddon extends ParseCloudClass { 5 | async afterSave(req) { 6 | const obj = req.object; 7 | 8 | const analytic = new Parse.Object('Analytic'); 9 | analytic.set(`pointer_${obj.className.toLowerCase()}`, obj); 10 | 11 | if(obj.get('testId')) { 12 | analytic.set('testId', obj.get('testId')); 13 | } 14 | 15 | await analytic.save(null, { useMasterKey: true }); 16 | 17 | return obj; 18 | } 19 | } 20 | 21 | module.exports = AnalyticAddon; 22 | -------------------------------------------------------------------------------- /src/test/cloud/TestAddon.js: -------------------------------------------------------------------------------- 1 | const { ParseCloudClass } = require('../../'); 2 | 3 | /** 4 | * Sets the key 'testAddonProcessed' to true 5 | */ 6 | class TestAddon extends ParseCloudClass { 7 | async processBeforeSave(request) { 8 | const obj = await super.processBeforeSave(request); 9 | obj.set('testAddonProcessed', true); 10 | return obj; 11 | } 12 | } 13 | 14 | module.exports = TestAddon; 15 | -------------------------------------------------------------------------------- /src/test/cloud/main.js: -------------------------------------------------------------------------------- 1 | const { ParseCloudClass } = require('../../'); 2 | const TestAddon = require('./TestAddon'); 3 | const AnalyticAddon = require('./AnalyticAddon'); 4 | 5 | class Test extends ParseCloudClass { 6 | async processBeforeSave(req) { 7 | const obj = await super.processBeforeSave(req); 8 | obj.set('nice', true); 9 | return obj; 10 | } 11 | } 12 | 13 | const instance = new Test(); 14 | instance.useAddon(new AnalyticAddon()); 15 | instance.useAddon(new TestAddon()); 16 | 17 | ParseCloudClass.configureClass(Parse, 'Test', instance); 18 | -------------------------------------------------------------------------------- /src/test/index.integration.test.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import * as fetch from 'node-fetch'; 3 | import * as Parse from 'parse/node'; 4 | import delay from 'delay'; 5 | import * as path from 'path'; 6 | 7 | const { ParseServer } = require('parse-server'); 8 | 9 | const appId = 'myAppId'; 10 | const javascriptKey = 'jsKey'; 11 | const masterKey = 'myMasterKey'; 12 | const serverURL = 'http://localhost:1337/parse'; 13 | 14 | Parse.initialize(appId, javascriptKey, masterKey); 15 | (Parse as any).serverURL = serverURL; 16 | 17 | const api = new ParseServer({ 18 | appId, 19 | javascriptKey, 20 | masterKey, // Keep this key secret! 21 | serverURL, // Don't forget to change to https if needed 22 | databaseURI: 'mongodb://localhost:27017/dev', // Connection string for your MongoDB database 23 | cloud: path.join(__dirname, 'cloud', 'main.js'), // Absolute path to your Cloud Code 24 | fileKey: 'optionalFileKey', 25 | }); 26 | 27 | const app = express(); 28 | app.use('/parse', api); 29 | 30 | const testId = Math.floor(Math.random() * 200000); 31 | 32 | const obj = new Parse.Object('Test'); 33 | obj.set('number', 123); 34 | obj.set('testId', testId); 35 | 36 | beforeAll(async () => { 37 | return new Promise((resolve, reject) => { 38 | app.listen(1337, () => { 39 | console.log('running'); // tslint:disable-line 40 | resolve(); 41 | }); 42 | }); 43 | }); 44 | 45 | test('should have a server running', async () => { 46 | await fetch('http://localhost:1337', {}); 47 | }); 48 | 49 | test('server should be able to save analytics', async () => { 50 | const analytic = new Parse.Object('Analytic'); 51 | analytic.set('test', true); 52 | await analytic.save(null, { useMasterKey: true }); 53 | }); 54 | 55 | test('saving the Test object should work', async () => { 56 | await obj.save(null, { useMasterKey: true }); 57 | }); 58 | 59 | test('fetching the object', async () => { 60 | await obj.fetch({ useMasterKey: true }); 61 | }); 62 | 63 | test('the object should have changed', async () => { 64 | expect(obj.get('nice')).toBe(true); 65 | }); 66 | 67 | test('the test addon should have been ', async () => { 68 | expect(obj.get('testAddonProcessed')).toBe(true); 69 | }); 70 | 71 | test('waiting a few seconds', async () => { 72 | await delay(3000); 73 | }); 74 | 75 | test('the analytic must have been created', async () => { 76 | const query = new Parse.Query('Analytic'); 77 | query.equalTo('testId', testId); 78 | const first = await query.first({ useMasterKey: true }); 79 | 80 | expect(first).toBeDefined(); 81 | }); 82 | -------------------------------------------------------------------------------- /src/test/index.test.ts: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import * as Parse from 'parse/node'; 3 | import { ParseCloudClass } from '../index'; 4 | import { ICloudClassObject } from '../ParseCloudClass'; 5 | 6 | class ExtendedParseClass extends ParseCloudClass { 7 | requiredKeys = ['name', 'creditCard', 'description']; 8 | defaultValues = { 9 | views: 0, 10 | otherValue: ['a', 'b', 'c'], 11 | }; 12 | } 13 | 14 | const OBJ_OK = new Parse.Object('Test'); 15 | OBJ_OK.set('name', 'hello'); 16 | OBJ_OK.set('creditCard', '213123123'); 17 | OBJ_OK.set('description', 'hola como estas este es UN Mensaje!!!'); 18 | 19 | describe('#configureClass', () => { 20 | const parseMock = { 21 | Cloud: { 22 | beforeFind: jest.fn(), 23 | beforeSave: jest.fn(), 24 | afterSave: jest.fn(), 25 | beforeDelete: jest.fn(), 26 | afterDelete: jest.fn(), 27 | }, 28 | }; 29 | 30 | const instance = new ExtendedParseClass(); 31 | 32 | // Now we configure the class 33 | ParseCloudClass.configureClass(parseMock, 'Test', instance); 34 | 35 | test('should configure #beforeFind correctly', () => { 36 | expect(parseMock.Cloud.beforeFind) 37 | .toHaveBeenCalledWith('Test', instance.beforeFind); 38 | }); 39 | 40 | test('should configure #beforeSave correctly', () => { 41 | expect(parseMock.Cloud.beforeSave) 42 | .toHaveBeenCalledWith('Test', instance.beforeSave); 43 | }); 44 | 45 | test('should configure #afterSave correctly', () => { 46 | expect(parseMock.Cloud.afterSave) 47 | .toHaveBeenCalledWith('Test', instance.afterSave); 48 | }); 49 | 50 | test('should configure #beforeDelete correctly', () => { 51 | expect(parseMock.Cloud.beforeDelete) 52 | .toHaveBeenCalledWith('Test', instance.beforeDelete); 53 | }); 54 | 55 | test('should configure #afterDelete correctly', () => { 56 | expect(parseMock.Cloud.afterDelete) 57 | .toHaveBeenCalledWith('Test', instance.afterDelete); 58 | }); 59 | 60 | }); 61 | 62 | describe('#useAddon', () => { 63 | const instance = new ExtendedParseClass(); 64 | const addon = new ParseCloudClass(); 65 | 66 | test('should add the addon correctly to the list of addons', () => { 67 | instance.useAddon(addon); 68 | expect(instance.addons[0]).toEqual(addon); 69 | }); 70 | 71 | }); 72 | 73 | describe('#checkRequiredKeys', () => { 74 | test('should throw if keys are not set', () => { 75 | expect(() => { 76 | const obj = new Parse.Object('Test'); 77 | ParseCloudClass.checkRequiredKeys(obj, ['name', 'description']); 78 | }).toThrow(); 79 | }); 80 | 81 | test('should not throw if keys are set', () => { 82 | expect(() => { 83 | const obj = new Parse.Object('Test'); 84 | obj.set('name', 'hola'); 85 | obj.set('description', 'como vas'); 86 | ParseCloudClass.checkRequiredKeys(obj, ['name', 'description']); 87 | }).not.toThrow(); 88 | }); 89 | }); 90 | 91 | describe('#setDefaultValues', () => { 92 | test('should set the default values', () => { 93 | const obj = new Parse.Object('Test'); 94 | const defaultValues = { 95 | name: 'abc', 96 | description: 'long description...', 97 | categories: ['abc', 'def'], 98 | }; 99 | 100 | const result = ParseCloudClass.setDefaultValues(obj, defaultValues); 101 | 102 | expect(result.get('name')).toEqual(defaultValues.name); 103 | expect(result.get('description')).toEqual(defaultValues.description); 104 | expect(result.get('categories')).toEqual(defaultValues.categories); 105 | }); 106 | }); 107 | 108 | describe('#checkAndCorrectMinimumValues', () => { 109 | const obj = new Parse.Object('Test'); 110 | const minimumValues = { 111 | views: 0, 112 | likes: 1, 113 | }; 114 | 115 | it('should set the minimum values if not set', () => { 116 | const response = ParseCloudClass.checkAndCorrectMinimumValues(obj, minimumValues); 117 | expect(response.get('views')).toEqual(0); 118 | expect(response.get('likes')).toEqual(1); 119 | }); 120 | 121 | it('should correct the values', () => { 122 | const obj = new Parse.Object('Test'); 123 | obj.set('views', -10); 124 | obj.set('likes', -30); 125 | 126 | const response = ParseCloudClass.checkAndCorrectMinimumValues(obj, minimumValues); 127 | 128 | expect(response.get('views')).toEqual(0); 129 | expect(response.get('likes')).toEqual(1); 130 | }); 131 | 132 | }); 133 | 134 | describe('#checkAndCorrectMaximumValues', () => { 135 | test('should set the right maximum values', () => { 136 | const instance = new ParseCloudClass({ 137 | maximumValues: { a: 200, b: 300, c: 100 }, 138 | }); 139 | 140 | const obj = new Parse.Object('Test'); 141 | obj.set('a', 400); 142 | obj.set('b', 1000); 143 | 144 | const response = ParseCloudClass.checkAndCorrectMaximumValues(obj, instance.maximumValues); 145 | 146 | expect(response.get('a')).toEqual(200); 147 | expect(response.get('b')).toEqual(300); 148 | expect(response.get('c')).not.toBeDefined(); 149 | }); 150 | }); 151 | 152 | describe('#beforeFind', () => { 153 | test('should not alter the query by default', () => { 154 | const query = new Parse.Query('Test'); 155 | const instance = new ExtendedParseClass(); 156 | const result = instance.beforeFind({ query }); 157 | expect(result).toEqual(query); 158 | }); 159 | }); 160 | 161 | describe('#beforeSave', () => { 162 | 163 | test('should save with no problems', async () => { 164 | const CLASSNAME = 'Test'; 165 | const obj = new ParseCloudClass({ 166 | requiredKeys: ['name'], 167 | defaultValues: { 168 | name: 'hello', 169 | }, 170 | }); 171 | 172 | expect(obj.beforeSave).toBeTruthy(); 173 | 174 | await obj.beforeSave({ 175 | object: new Parse.Object(CLASSNAME), 176 | user: new Parse.User(), 177 | }, { 178 | success: (returnedObject: Parse.Object) => { 179 | expect(returnedObject.className).toEqual(CLASSNAME); 180 | }, 181 | error: (e) => { 182 | expect(e).not.toBeDefined(); 183 | }, 184 | }); 185 | }); 186 | 187 | // tslint:disable-next-line 188 | test('should save with no problems and return the object if there is no response parameter', async () => { 189 | const CLASSNAME = 'Test'; 190 | const obj = new ParseCloudClass({ 191 | requiredKeys: ['name'], 192 | defaultValues: { 193 | name: 'hello', 194 | }, 195 | }); 196 | 197 | expect(obj.beforeSave).toBeTruthy(); 198 | 199 | const returnedObject = await obj.beforeSave({ 200 | object: new Parse.Object(CLASSNAME), 201 | user: new Parse.User(), 202 | }); 203 | 204 | expect(returnedObject.className).toEqual(CLASSNAME); 205 | }); 206 | 207 | test('should not save if required keys are not met', async () => { 208 | const CLASSNAME = 'Test'; 209 | const obj = new ParseCloudClass({ 210 | requiredKeys: ['name'], 211 | defaultValues: { 212 | }, 213 | }); 214 | 215 | await obj.beforeSave({ 216 | object: new Parse.Object(CLASSNAME), 217 | user: new Parse.User(), 218 | }, { 219 | success: (returnedObject: Parse.Object) => { 220 | expect(returnedObject).not.toBeDefined(); 221 | }, 222 | error: (e) => { 223 | expect(e).toBeDefined(); 224 | }, 225 | }); 226 | }); 227 | 228 | // tslint:disable-next-line 229 | test('should not save if required keys are not met, and should throw if there is no response parameter', async () => { 230 | const CLASSNAME = 'Test'; 231 | const obj = new ParseCloudClass({ 232 | requiredKeys: ['name'], 233 | defaultValues: { 234 | }, 235 | }); 236 | let error; 237 | 238 | try { 239 | await obj.beforeSave({ 240 | object: new Parse.Object(CLASSNAME), 241 | user: new Parse.User(), 242 | }); 243 | } catch (e) { 244 | error = e; 245 | } finally { 246 | expect(error).toBeDefined(); 247 | } 248 | }); 249 | 250 | test('should have called the addon\'s processBeforeSave', async () => { 251 | const classInstance = new ParseCloudClass(); 252 | const addon = new ParseCloudClass(); 253 | 254 | // Here we set the class to use the given addon 255 | classInstance.useAddon(addon); 256 | 257 | const spyAddon = jest.spyOn(addon, 'processBeforeSave'); 258 | await classInstance.beforeSave({ 259 | object: OBJ_OK, 260 | }); 261 | expect(spyAddon).toHaveBeenCalledTimes(1); 262 | }); 263 | 264 | }); 265 | 266 | describe('#beforeDelete', () => { 267 | test('should delete with no problems', async () => { 268 | const CLASSNAME = 'Test'; 269 | const obj = new ParseCloudClass({ 270 | requiredKeys: ['name'], 271 | defaultValues: { 272 | name: 'hello', 273 | }, 274 | }); 275 | 276 | expect(obj.beforeDelete).toBeTruthy(); 277 | 278 | await obj.beforeDelete({ 279 | object: new Parse.Object(CLASSNAME), 280 | user: new Parse.User(), 281 | }, { 282 | success: (returnedObject: Parse.Object) => { 283 | expect(returnedObject.className).toEqual(CLASSNAME); 284 | }, 285 | error: (e) => { 286 | expect(e).not.toBeDefined(); 287 | }, 288 | }); 289 | }); 290 | 291 | // tslint:disable-next-line 292 | test('should delete with no problems and return the object if there is no response parameter', async () => { 293 | const CLASSNAME = 'Test'; 294 | const obj = new ParseCloudClass({ 295 | requiredKeys: ['name'], 296 | defaultValues: { 297 | name: 'hello', 298 | }, 299 | }); 300 | 301 | expect(obj.beforeDelete).toBeTruthy(); 302 | 303 | const returnedObject = await obj.beforeDelete({ 304 | object: new Parse.Object(CLASSNAME), 305 | user: new Parse.User(), 306 | }); 307 | 308 | expect(returnedObject.className).toEqual(CLASSNAME); 309 | }); 310 | 311 | test('should not delete if something goes wrong', async () => { 312 | const CLASSNAME = 'Test'; 313 | const obj = new ParseCloudClass({ 314 | requiredKeys: ['name'], 315 | defaultValues: { 316 | }, 317 | }); 318 | 319 | const spyDelete = jest.spyOn(obj, 'processBeforeDelete'); 320 | spyDelete.mockImplementation(async () => { throw new Error(); }); 321 | 322 | await obj.beforeDelete({ 323 | object: new Parse.Object(CLASSNAME), 324 | user: new Parse.User(), 325 | }, { 326 | success: (returnedObject: Parse.Object) => { 327 | expect(returnedObject).not.toBeDefined(); 328 | }, 329 | error: (e) => { 330 | expect(e).toBeDefined(); 331 | }, 332 | }); 333 | }); 334 | 335 | // tslint:disable-next-line 336 | test('should not delete if something goes wrong, and throw if there is no response parameter', async () => { 337 | const CLASSNAME = 'Test'; 338 | const obj = new ParseCloudClass({ 339 | requiredKeys: ['name'], 340 | defaultValues: { 341 | }, 342 | }); 343 | let error; 344 | 345 | const spyDelete = jest.spyOn(obj, 'processBeforeDelete'); 346 | spyDelete.mockImplementation(async () => { throw new Error(); }); 347 | 348 | try { 349 | await obj.beforeDelete({ 350 | object: new Parse.Object(CLASSNAME), 351 | user: new Parse.User(), 352 | }); 353 | } catch (e) { 354 | error = e; 355 | } finally { 356 | expect(error).toBeDefined(); 357 | } 358 | }); 359 | 360 | test('should have called the addon\'s processBeforeDelete', async () => { 361 | const classInstance = new ParseCloudClass(); 362 | const addon = new ParseCloudClass(); 363 | 364 | // Here we set the class to use the given addon 365 | classInstance.useAddon(addon); 366 | 367 | const spyAddon = jest.spyOn(addon, 'processBeforeDelete'); 368 | await classInstance.beforeDelete({ 369 | object: OBJ_OK, 370 | }); 371 | expect(spyAddon).toHaveBeenCalledTimes(1); 372 | }); 373 | 374 | }); 375 | 376 | describe('#processBeforeSave', () => { 377 | 378 | const classInstance = new ParseCloudClass({ 379 | requiredKeys: ['creditCard', 'name', 'description'], 380 | defaultValues: { 381 | name: 'hello', 382 | }, 383 | minimumValues: {}, 384 | }); 385 | 386 | afterEach(() => { 387 | jest.clearAllMocks(); 388 | }); 389 | 390 | test('should restrict creation without required creditCard', () => { 391 | const obj = new Parse.Object('Test'); 392 | obj.set('description', '123123'); 393 | obj.set('name', 'hello'); 394 | 395 | return classInstance.processBeforeSave({ 396 | object: obj, 397 | }).catch((e) => { 398 | expect(e).toBeTruthy(); 399 | }); 400 | }); 401 | 402 | test('should restrict creation without required name', () => { 403 | const obj = new Parse.Object('Test'); 404 | obj.set('creditCard', '123123'); 405 | obj.set('description', '123123'); 406 | 407 | return classInstance.processBeforeSave({ 408 | object: obj, 409 | }).catch((e) => { 410 | expect(e).toBeTruthy(); 411 | }); 412 | }); 413 | 414 | test('should restrict creation without required description', () => { 415 | const obj = new Parse.Object('Test'); 416 | obj.set('creditCard', '123123'); 417 | obj.set('name', 'hello'); 418 | 419 | return classInstance.processBeforeSave({ 420 | object: obj, 421 | }).catch((e) => { 422 | expect(e).toBeTruthy(); 423 | }); 424 | }); 425 | 426 | test('should allow creation with all required keys set', async () => { 427 | const response = await classInstance.processBeforeSave({ 428 | object: OBJ_OK, 429 | }); 430 | expect(response).toBeTruthy(); 431 | }); 432 | 433 | test('should have set the default values', async () => { 434 | const response = await classInstance.processBeforeSave({ 435 | object: OBJ_OK, 436 | }); 437 | 438 | for (const key in classInstance.defaultValues) { 439 | const actual = response.get(key); 440 | expect(actual).toEqual(classInstance.defaultValues[key]); 441 | } 442 | }); 443 | 444 | test('should throw an error if an immutable key is changed', async () => { 445 | const obj = new Parse.Object('Test'); 446 | obj.set('test', 1); 447 | obj.set('test2', 2); 448 | 449 | const instance = new ParseCloudClass({ immutableKeys: ['test', 'test2'] }); 450 | let error; 451 | 452 | try { 453 | await instance.processBeforeSave({ object: obj }); 454 | } catch (e) { 455 | error = e; 456 | } finally { 457 | expect(error).toBeDefined(); 458 | expect(() => { 459 | throw error; 460 | }).toThrow('test cannot be modified'); 461 | } 462 | }); 463 | 464 | test('should not throw an error if an immutable key is changed with the master key', async () => { 465 | const obj = new Parse.Object('Test'); 466 | obj.set('test', 1); 467 | obj.set('test2', 2); 468 | 469 | const instance = new ParseCloudClass({ immutableKeys: ['test', 'test2'] }); 470 | await instance.processBeforeSave({ object: obj, master: true }); 471 | }); 472 | }); 473 | 474 | describe('#afterSave', () => { 475 | const classInstance = new ParseCloudClass({ 476 | requiredKeys: ['name'], 477 | defaultValues: { 478 | name: 'hello', 479 | }, 480 | }); 481 | 482 | const addon = new ParseCloudClass(); 483 | 484 | // Here we set the class to use the given addon 485 | classInstance.useAddon(addon); 486 | 487 | afterEach(() => { 488 | jest.clearAllMocks(); 489 | }); 490 | 491 | test('should return the same object by default', async () => { 492 | const response = await classInstance.afterSave({ 493 | object: OBJ_OK, 494 | }); 495 | 496 | expect(response.toJSON()).toEqual(OBJ_OK.toJSON()); 497 | }); 498 | 499 | test('should have called the addon\'s afterSave', async () => { 500 | const spyAddon = jest.spyOn(addon, 'afterSave'); 501 | await classInstance.afterSave({ 502 | object: OBJ_OK, 503 | }); 504 | expect(spyAddon).toHaveBeenCalledTimes(1); 505 | }); 506 | }); 507 | 508 | describe('#processBeforeDelete', () => { 509 | const classInstance = new ExtendedParseClass(); 510 | 511 | test('should return the same object by default', async () => { 512 | const response = await classInstance.processBeforeDelete({ 513 | object: OBJ_OK, 514 | }); 515 | 516 | expect(response.toJSON()).toEqual(OBJ_OK.toJSON()); 517 | }); 518 | }); 519 | 520 | describe('#afterDelete', () => { 521 | const classInstance = new ParseCloudClass({ 522 | requiredKeys: ['name'], 523 | defaultValues: { 524 | name: 'hello', 525 | }, 526 | }); 527 | 528 | const addon = new ParseCloudClass(); 529 | 530 | // Here we set the class to use the given addon 531 | classInstance.useAddon(addon); 532 | 533 | test('should return the same object by default', async () => { 534 | const response = await classInstance.afterDelete({ 535 | object: OBJ_OK, 536 | }); 537 | 538 | expect(response.toJSON()).toEqual(OBJ_OK.toJSON()); 539 | }); 540 | 541 | test('should have called the addon\'s afterDelete', async () => { 542 | const spyAddon = jest.spyOn(addon, 'afterDelete'); 543 | const response = await classInstance.afterDelete({ 544 | object: OBJ_OK, 545 | }); 546 | expect(spyAddon).toHaveBeenCalledTimes(1); 547 | }); 548 | }); 549 | 550 | describe('Working with addons', () => { 551 | const testFn = jest.fn(); 552 | 553 | class ExtendedClass2 extends ParseCloudClass { 554 | async processBeforeSave(req: Parse.Cloud.BeforeSaveRequest) { 555 | const object = await super.processBeforeSave(req); 556 | testFn(3); 557 | object.set('test', true); 558 | return object; 559 | } 560 | } 561 | 562 | class Addon1 extends ParseCloudClass { 563 | async processBeforeSave(req: Parse.Cloud.BeforeSaveRequest) { 564 | req.object.set('addon1', true); 565 | testFn(1); 566 | return req.object; 567 | } 568 | } 569 | 570 | class Addon2 extends ParseCloudClass { 571 | async processBeforeSave(req: Parse.Cloud.BeforeSaveRequest) { 572 | req.object.set('addon2', true); 573 | testFn(2); 574 | return req.object; 575 | } 576 | } 577 | 578 | const instance = new ExtendedClass2(); 579 | const addon1 = new Addon1(); 580 | const addon2 = new Addon2(); 581 | 582 | // Create the addons 583 | instance.useAddon(addon1); 584 | instance.useAddon(addon2); 585 | 586 | // Create the object 587 | let obj = new Parse.Object('TestObject'); 588 | 589 | test('executing an instance function', async () => { 590 | obj = await instance.beforeSave({ object: obj } as any); 591 | }); 592 | 593 | test('instance should have both addons', () => { 594 | expect(instance.addons).toEqual([addon1, addon2]); 595 | }); 596 | 597 | test('should have called the testFn 3 times', () => { 598 | expect(testFn).toHaveBeenCalledTimes(3); 599 | expect(testFn).toHaveBeenCalledWith(1); 600 | expect(testFn).toHaveBeenCalledWith(2); 601 | expect(testFn).toHaveBeenCalledWith(3); 602 | }); 603 | 604 | test('should have executed all addon functions', () => { 605 | // Expect the object to have been mutated 606 | expect(obj.get('test')).toBe(true); 607 | 608 | // Expect all addons to have been called 609 | expect(obj.get('addon1')).toBe(true); 610 | expect(obj.get('addon2')).toBe(true); 611 | }); 612 | 613 | }); 614 | 615 | describe('Given a configuration object', () => { 616 | const spyFind = jest.fn(); 617 | const spyBeforeSave = jest.fn(); 618 | const spyAfterSave = jest.fn(); 619 | const spyBeforeDelete = jest.fn(); 620 | const spyAfterDelete = jest.fn(); 621 | 622 | const object: ICloudClassObject = { 623 | addons: [new ParseCloudClass()], 624 | defaultValues: { 625 | test: true, 626 | }, 627 | beforeFind: function (req) { 628 | spyFind(this); 629 | return req.query; 630 | }, 631 | processBeforeSave: async function (req) { 632 | spyBeforeSave(this); 633 | return req.object; 634 | }, 635 | afterSave: async function (req) { 636 | spyAfterSave(this); 637 | return req.object; 638 | }, 639 | processBeforeDelete: async function (req) { 640 | spyBeforeDelete(this); 641 | return req.object; 642 | }, 643 | afterDelete: async function (req) { 644 | spyAfterDelete(this); 645 | return req.object; 646 | }, 647 | }; 648 | 649 | const instance = ParseCloudClass.fromObject(object); 650 | 651 | test('should return a instance of ParseCloudClass', () => { 652 | expect(instance).toBeInstanceOf(ParseCloudClass); 653 | }); 654 | 655 | describe('Calling beforeFind', () => { 656 | test('should call the mock function', () => { 657 | instance.beforeFind({ query: new Parse.Query('Test') }); 658 | expect(spyFind).toHaveBeenCalledTimes(1); 659 | }); 660 | 661 | test('"this" must reference the instance', () => { 662 | expect(spyFind.mock.calls[0][0].defaultValues).toEqual(object.defaultValues); 663 | }); 664 | }); 665 | 666 | describe('Calling processBeforeSave', () => { 667 | test('should call the mock function', async () => { 668 | await instance.processBeforeSave({ object: new Parse.Object('Test') }); 669 | expect(spyBeforeSave).toHaveBeenCalledTimes(1); 670 | }); 671 | 672 | test('"this" must reference the instance', () => { 673 | expect(spyBeforeSave.mock.calls[0][0].defaultValues).toEqual(object.defaultValues); 674 | }); 675 | }); 676 | 677 | describe('Calling afterSave', () => { 678 | test('should call the mock function', async () => { 679 | await instance.afterSave({ object: new Parse.Object('Test') }); 680 | expect(spyAfterSave).toHaveBeenCalledTimes(1); 681 | }); 682 | 683 | test('"this" must reference the instance', () => { 684 | expect(spyAfterSave.mock.calls[0][0].defaultValues).toEqual(object.defaultValues); 685 | }); 686 | }); 687 | 688 | describe('Calling processBeforeDelete', () => { 689 | test('should call the mock function', async () => { 690 | await instance.processBeforeDelete({ object: new Parse.Object('Test') }); 691 | expect(spyBeforeDelete).toHaveBeenCalledTimes(1); 692 | }); 693 | 694 | test('"this" must reference the instance', () => { 695 | expect(spyBeforeDelete.mock.calls[0][0].defaultValues).toEqual(object.defaultValues); 696 | }); 697 | }); 698 | 699 | describe('Calling afterDelete', () => { 700 | test('should call the mock function', async () => { 701 | await instance.afterDelete({ object: new Parse.Object('Test') }); 702 | expect(spyAfterDelete).toHaveBeenCalledTimes(1); 703 | }); 704 | 705 | test('"this" must reference the instance', () => { 706 | expect(spyAfterDelete.mock.calls[0][0].defaultValues).toEqual(object.defaultValues); 707 | }); 708 | }); 709 | }); 710 | -------------------------------------------------------------------------------- /src/test/util/requireKey.test.ts: -------------------------------------------------------------------------------- 1 | import { ParseCloudClass } from '../../'; 2 | import requireKey from '../../decorators/requireKey'; 3 | 4 | @requireKey('testKey', 'testKey38') 5 | class TestClass extends ParseCloudClass { 6 | } 7 | 8 | test('Given a new instance: should require the expected keys', () => { 9 | const instance = new TestClass(); 10 | expect(instance.requiredKeys).toEqual(['testKey', 'testKey38']); 11 | }); 12 | -------------------------------------------------------------------------------- /src/test/util/requireLogin.test.ts: -------------------------------------------------------------------------------- 1 | import * as Parse from 'parse/node'; 2 | import requireLogin, { PLEASE_LOGIN } from '../../util/requireLogin'; 3 | 4 | test('Given a request with no user nor master key: should throw', () => { 5 | expect(() => { 6 | (requireLogin as any)({}); 7 | }).toThrow(PLEASE_LOGIN); 8 | }); 9 | 10 | test('Given a request with master key: should not throw', () => { 11 | expect(() => { 12 | (requireLogin as any)({ master: true }); 13 | }).not.toThrow(); 14 | }); 15 | 16 | test('Given a request with user: should not throw', () => { 17 | expect(() => { 18 | const user = new Parse.User(); 19 | user.id = '123'; 20 | 21 | (requireLogin as any)({ user }); 22 | }).not.toThrow(); 23 | }); 24 | -------------------------------------------------------------------------------- /src/util/requireLogin.ts: -------------------------------------------------------------------------------- 1 | export const PLEASE_LOGIN = 'Please login'; 2 | 3 | /** 4 | * Requires the request to have ben sent by an user or by a master key 5 | * @param req 6 | */ 7 | export default function requireLogin(req: Parse.Cloud.BeforeSaveRequest) { 8 | if (!req.user && !req.master) { 9 | throw new Error(PLEASE_LOGIN); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": { 4 | "lib": [ 5 | "es2015", 6 | "dom" 7 | ], 8 | "outDir": "out", 9 | "declaration": true, 10 | "declarationDir": "out", 11 | "rootDir": "src", 12 | "experimentalDecorators": true, 13 | "sourceMap": true 14 | }, 15 | "exclude": [ 16 | "node_modules", 17 | "out" 18 | ] 19 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint-config-airbnb" 4 | ], 5 | "rules": { 6 | "no-switch-case-fall-through": false, 7 | "no-console": [ 8 | true 9 | ], 10 | "object-literal-shorthand": false, 11 | "prefer-for-of": true, 12 | "prefer-const": true, 13 | "no-unused-variable": [ 14 | true 15 | ], 16 | "no-undef": [true] 17 | } 18 | } 19 | --------------------------------------------------------------------------------